mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
test: stabilize PR handoff gate
This commit is contained in:
1 parent
a2a1b9d8cc
commit
9b72007c58
5 files changed
+103
-31
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user