test: stabilize PR handoff gate

This commit is contained in:
Dotta committed 2026-05-24 02:34:12 +00:00
1 parent a2a1b9d8cc
commit 9b72007c58
5 files changed
+103 -31

No files matched your search

+12 -12
View File
@@ -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");
});
});
+29
View File
@@ -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"),
+7 -2
View File
@@ -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", () => {
@@ -29,14 +29,17 @@ async function closeDbClient(db: ReturnType<typeof createDb> | 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<Record<string, unknown>> = [];
let firstWaitRelease: (() => void) | null = null;
let firstWaitGate = new Promise<void>((resolve) => {
firstWaitRelease = resolve;
});
const heldWaits = options.heldWaits ?? 1;
const waitReleases: Array<() => void> = [];
const waitGates = Array.from({ length: heldWaits }, () =>
new Promise<void>((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<void>((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);
@@ -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,