diff --git a/cli/src/__tests__/network-bind.test.ts b/cli/src/__tests__/network-bind.test.ts index 513e3853a10..b4c59224efa 100644 --- a/cli/src/__tests__/network-bind.test.ts +++ b/cli/src/__tests__/network-bind.test.ts @@ -1,8 +1,12 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import { resolveRuntimeBind, validateConfiguredBindMode } from "@paperclipai/shared"; import { buildPresetServerConfig } from "../config/server-bind.js"; -const ORIGINAL_PATH = process.env.PATH; +const ORIGINAL_ENV = { ...process.env }; + +afterEach(() => { + process.env = { ...ORIGINAL_ENV }; +}); describe("network bind helpers", () => { it("rejects non-loopback bind modes in local_trusted", () => { @@ -54,16 +58,12 @@ describe("network bind helpers", () => { delete process.env.PAPERCLIP_TAILNET_BIND_HOST; process.env.PATH = ""; - try { - const preset = buildPresetServerConfig("tailnet", { - port: 3100, - allowedHostnames: [], - serveUi: true, - }); + const preset = buildPresetServerConfig("tailnet", { + port: 3100, + allowedHostnames: [], + serveUi: true, + }); - expect(preset.server.host).toBe("127.0.0.1"); - } finally { - process.env.PATH = ORIGINAL_PATH; - } + expect(preset.server.host).toBe("127.0.0.1"); }); }); diff --git a/scripts/run-vitest-stable.mjs b/scripts/run-vitest-stable.mjs index a3ef4ba4c0b..fccb37e00ea 100644 --- a/scripts/run-vitest-stable.mjs +++ b/scripts/run-vitest-stable.mjs @@ -20,18 +20,36 @@ const nonServerProjects = [ ]; const routeTestPattern = /[^/]*(?:route|routes|authz)[^/]*\.test\.ts$/; const additionalSerializedServerTests = new Set([ + "server/src/__tests__/access-service.test.ts", + "server/src/__tests__/activity-service.test.ts", "server/src/__tests__/approval-routes-idempotency.test.ts", "server/src/__tests__/assets.test.ts", "server/src/__tests__/authz-company-access.test.ts", + "server/src/__tests__/cleanup-removal-service.test.ts", + "server/src/__tests__/codex-local-execute.test.ts", + "server/src/__tests__/codex-local-skill-injection.test.ts", + "server/src/__tests__/codex-local-skill-sync.test.ts", "server/src/__tests__/companies-route-path-guard.test.ts", + "server/src/__tests__/company-search-service.test.ts", + "server/src/__tests__/company-skills-detail.test.ts", "server/src/__tests__/company-portability.test.ts", "server/src/__tests__/costs-service.test.ts", + "server/src/__tests__/documents-service.test.ts", + "server/src/__tests__/environment-runtime-driver-contract.test.ts", + "server/src/__tests__/environment-runtime.test.ts", + "server/src/__tests__/environment-service.test.ts", + "server/src/__tests__/execution-workspaces-service.test.ts", "server/src/__tests__/express5-auth-wildcard.test.ts", + "server/src/__tests__/feedback-service.test.ts", "server/src/__tests__/health-dev-server-token.test.ts", "server/src/__tests__/health.test.ts", + "server/src/__tests__/heartbeat-accepted-plan-workspace-refresh.test.ts", + "server/src/__tests__/heartbeat-comment-wake-batching.test.ts", "server/src/__tests__/heartbeat-dependency-scheduling.test.ts", "server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts", + "server/src/__tests__/heartbeat-list.test.ts", "server/src/__tests__/heartbeat-process-recovery.test.ts", + "server/src/__tests__/inbox-dismissals.test.ts", "server/src/__tests__/invite-accept-existing-member.test.ts", "server/src/__tests__/invite-accept-gateway-defaults.test.ts", "server/src/__tests__/invite-accept-replay.test.ts", @@ -40,10 +58,20 @@ const additionalSerializedServerTests = new Set([ "server/src/__tests__/invite-onboarding-text.test.ts", "server/src/__tests__/issues-checkout-wakeup.test.ts", "server/src/__tests__/issues-service.test.ts", + "server/src/__tests__/issue-recovery-actions.test.ts", + "server/src/__tests__/issue-references-service.test.ts", + "server/src/__tests__/issue-thread-interactions-service.test.ts", + "server/src/__tests__/issue-tree-control-service.test.ts", "server/src/__tests__/opencode-local-adapter-environment.test.ts", + "server/src/__tests__/plugin-database.test.ts", + "server/src/__tests__/plugin-managed-agents.test.ts", + "server/src/__tests__/plugin-managed-routines.test.ts", + "server/src/__tests__/plugin-orchestration-apis.test.ts", "server/src/__tests__/project-routes-env.test.ts", + "server/src/__tests__/qa-routine-secrets-e2e.test.ts", "server/src/__tests__/redaction.test.ts", "server/src/__tests__/routines-e2e.test.ts", + "server/src/__tests__/secrets-service.test.ts", ]); let invocationIndex = 0; const serializedModeName = "serialized"; @@ -241,6 +269,7 @@ function runVitest(args, label) { // Keep per-run paths compact so Unix socket fixtures stay under macOS path limits. const env = { ...process.env, + NODE_ENV: "test", PAPERCLIP_HOME: path.join(testRoot, "h"), PAPERCLIP_INSTANCE_ID: `vt-${process.pid}-${invocationIndex}`, TMPDIR: path.join(testRoot, "t"), diff --git a/server/src/__tests__/better-auth.test.ts b/server/src/__tests__/better-auth.test.ts index 2e2821d7ec9..07b5db025be 100644 --- a/server/src/__tests__/better-auth.test.ts +++ b/server/src/__tests__/better-auth.test.ts @@ -33,17 +33,22 @@ describe("Better Auth cookie scoping", () => { cookiePrefix: "paperclip-sat-worktree", }); expect(getCookies({ advanced } as BetterAuthOptions).sessionToken.name).toMatch( - /paperclip-sat-worktree\.session_token$/, + /^(?:__Secure-)?paperclip-sat-worktree\.session_token$/, ); }); it("keeps local http auth cookies non-secure while preserving the scoped prefix", () => { process.env.PAPERCLIP_INSTANCE_ID = "pap-worktree"; - expect(buildBetterAuthAdvancedOptions({ disableSecureCookies: true })).toEqual({ + const advanced = buildBetterAuthAdvancedOptions({ disableSecureCookies: true }); + + expect(advanced).toEqual({ cookiePrefix: "paperclip-pap-worktree", useSecureCookies: false, }); + expect(getCookies({ advanced } as BetterAuthOptions).sessionToken.name).toBe( + "paperclip-pap-worktree.session_token", + ); }); it("disables secure cookies when no canonical public auth URL is configured", () => { diff --git a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts index 606f69bca95..65b30947575 100644 --- a/server/src/__tests__/heartbeat-comment-wake-batching.test.ts +++ b/server/src/__tests__/heartbeat-comment-wake-batching.test.ts @@ -29,14 +29,17 @@ async function closeDbClient(db: ReturnType | undefined) { await db?.$client?.end?.({ timeout: 0 }); } -async function createControlledGatewayServer() { +async function createControlledGatewayServer(options: { heldWaits?: number } = {}) { const server = createServer(); const wss = new WebSocketServer({ server }); const agentPayloads: Array> = []; - let firstWaitRelease: (() => void) | null = null; - let firstWaitGate = new Promise((resolve) => { - firstWaitRelease = resolve; - }); + const heldWaits = options.heldWaits ?? 1; + const waitReleases: Array<() => void> = []; + const waitGates = Array.from({ length: heldWaits }, () => + new Promise((resolve) => { + waitReleases.push(resolve); + }) + ); let waitCount = 0; wss.on("connection", (socket) => { @@ -102,8 +105,8 @@ async function createControlledGatewayServer() { if (frame.method === "agent.wait") { waitCount += 1; - if (waitCount === 1) { - await firstWaitGate; + if (waitCount <= waitGates.length) { + await waitGates[waitCount - 1]; } socket.send( JSON.stringify({ @@ -134,10 +137,16 @@ async function createControlledGatewayServer() { return { url: `ws://127.0.0.1:${address.port}`, getAgentPayloads: () => agentPayloads, + releaseNextWait: () => { + waitReleases.shift()?.(); + }, releaseFirstWait: () => { - firstWaitRelease?.(); - firstWaitRelease = null; - firstWaitGate = Promise.resolve(); + waitReleases.shift()?.(); + }, + releaseAllWaits: () => { + while (waitReleases.length > 0) { + waitReleases.shift()?.(); + } }, close: async () => { await new Promise((resolve) => wss.close(() => resolve())); @@ -271,7 +280,7 @@ describe("heartbeat comment wake batching", () => { }); it("batches deferred comment wakes and forwards the ordered batch to the next run", async () => { - const gateway = await createControlledGatewayServer(); + const gateway = await createControlledGatewayServer({ heldWaits: 2 }); const companyId = randomUUID(); const agentId = randomUUID(); const issueId = randomUUID(); @@ -442,9 +451,22 @@ describe("heartbeat comment wake batching", () => { gateway.releaseFirstWait(); await waitFor(() => gateway.getAgentPayloads().length === 2); + let promotedRunId: string | null = null; await waitFor(async () => { const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId)); - return runs.length === 2 && runs.every((run) => run.status === "succeeded"); + const promotedRun = runs.find((run) => run.id !== firstRun?.id && run.status === "running"); + promotedRunId = promotedRun?.id ?? null; + return Boolean(promotedRunId); + }); + await db + .update(issues) + .set({ status: "done", updatedAt: new Date() }) + .where(eq(issues.id, issueId)); + gateway.releaseNextWait(); + await waitFor(async () => { + const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId)); + const batchedRuns = runs.filter((run) => run.id === firstRun?.id || run.id === promotedRunId); + return batchedRuns.length === 2 && batchedRuns.every((run) => run.status === "succeeded"); }, 90_000); const secondPayload = gateway.getAgentPayloads()[1] ?? {}; @@ -458,13 +480,13 @@ describe("heartbeat comment wake batching", () => { expect(String(secondPayload.message ?? "")).toContain("Third comment"); expect(String(secondPayload.message ?? "")).not.toContain("First comment"); } finally { - gateway.releaseFirstWait(); + gateway.releaseAllWaits(); await gateway.close(); } }, 120_000); it("promotes deferred comment wakes with their comments after the active run is cancelled", async () => { - const gateway = await createControlledGatewayServer(); + const gateway = await createControlledGatewayServer({ heldWaits: 2 }); const companyId = randomUUID(); const agentId = randomUUID(); const issueId = randomUUID(); @@ -614,13 +636,25 @@ describe("heartbeat comment wake batching", () => { }); expect(String(promotedPayload.message ?? "")).toContain("Queued follow-up"); - gateway.releaseFirstWait(); + let promotedRunId: string | null = null; await waitFor(async () => { const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId)); - return runs.length === 2 && runs.every((run) => ["cancelled", "succeeded"].includes(run.status)); + const promotedRun = runs.find((run) => run.id !== firstRun?.id && run.status === "running"); + promotedRunId = promotedRun?.id ?? null; + return Boolean(promotedRunId); + }); + await db + .update(issues) + .set({ status: "done", updatedAt: new Date() }) + .where(eq(issues.id, issueId)); + gateway.releaseAllWaits(); + await waitFor(async () => { + const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, agentId)); + const promotedRuns = runs.filter((run) => run.id === firstRun?.id || run.id === promotedRunId); + return promotedRuns.length === 2 && promotedRuns.every((run) => ["cancelled", "succeeded"].includes(run.status)); }, 90_000); } finally { - gateway.releaseFirstWait(); + gateway.releaseAllWaits(); await gateway.close(); } }, 120_000); diff --git a/server/src/__tests__/worktree-config.test.ts b/server/src/__tests__/worktree-config.test.ts index 69a9c5eeef5..16fd15f0a38 100644 --- a/server/src/__tests__/worktree-config.test.ts +++ b/server/src/__tests__/worktree-config.test.ts @@ -508,6 +508,8 @@ describe("worktree config repair", () => { process.env.PAPERCLIP_HOME = isolatedHome; process.env.PAPERCLIP_INSTANCE_ID = "pap-878-create-a-mine-tab-in-inbox"; process.env.PAPERCLIP_CONFIG = configPath; + delete process.env.PORT; + delete process.env.DATABASE_URL; maybePersistWorktreeRuntimePorts({ serverPort: 3103, @@ -590,6 +592,8 @@ describe("worktree config repair", () => { process.env.PAPERCLIP_HOME = isolatedHome; process.env.PAPERCLIP_INSTANCE_ID = "pap-125-public-base-url"; process.env.PAPERCLIP_CONFIG = configPath; + delete process.env.PORT; + delete process.env.DATABASE_URL; maybePersistWorktreeRuntimePorts({ serverPort: 3103,