mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.
Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
153 lines
5.7 KiB
TypeScript
153 lines
5.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { fileURLToPath } from "node:url";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { eq } from "drizzle-orm";
|
|
import {
|
|
agents,
|
|
authUsers,
|
|
companies,
|
|
companyMemberships,
|
|
completionContracts,
|
|
createDb,
|
|
heartbeatRuns,
|
|
nativeRunFinalizations,
|
|
nativeRunResults,
|
|
projectWorkspaces,
|
|
projects,
|
|
statusDecisions,
|
|
workAssessments,
|
|
issues,
|
|
} from "@paperclipai/db";
|
|
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
|
import { drainHeartbeatRunsToQuiescence } from "./helpers/drain-heartbeat-runs.js";
|
|
|
|
const adapterExecute = vi.hoisted(() => vi.fn(async () => ({
|
|
exitCode: 0,
|
|
signal: null,
|
|
timedOut: false,
|
|
summary: "Legacy adapter completed through the flag-off heartbeat.",
|
|
resultJson: { summary: "Legacy bytes", nested: { count: 1, ok: true } },
|
|
provider: "test",
|
|
model: "legacy-test",
|
|
})));
|
|
|
|
vi.mock("../adapters/index.js", () => ({
|
|
getServerAdapter: () => ({
|
|
type: "codex_local",
|
|
execute: adapterExecute,
|
|
supportsLocalAgentJwt: false,
|
|
}),
|
|
findActiveServerAdapter: () => ({
|
|
type: "codex_local",
|
|
execute: adapterExecute,
|
|
supportsLocalAgentJwt: false,
|
|
}),
|
|
runningProcesses: new Map(),
|
|
}));
|
|
|
|
import { heartbeatService } from "../services/heartbeat.js";
|
|
import { instanceSettingsService } from "../services/instance-settings.js";
|
|
import { reconcileNativeFinalizations } from "../services/native-runtime/native-finalization-reconciler.js";
|
|
|
|
describe("P6-32 legacy finalization regression", () => {
|
|
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
|
|
let db: ReturnType<typeof createDb>;
|
|
const companyId = randomUUID();
|
|
const agentId = randomUUID();
|
|
const projectId = randomUUID();
|
|
const projectWorkspaceId = randomUUID();
|
|
const issueId = randomUUID();
|
|
|
|
beforeAll(async () => {
|
|
temporary = await startEmbeddedPostgresTestDatabase("paperclip-native-legacy-");
|
|
db = createDb(temporary.connectionString);
|
|
await instanceSettingsService(db).updateExperimental({ enableNativeRunner: false });
|
|
await db.insert(companies).values({
|
|
id: companyId,
|
|
name: "Legacy snapshot",
|
|
issuePrefix: "LGC",
|
|
status: "active",
|
|
defaultResponsibleUserId: "responsible-user",
|
|
});
|
|
await db.insert(authUsers).values({
|
|
id: "responsible-user", name: "Responsible user", email: "legacy-finalization@example.test",
|
|
createdAt: new Date(), updatedAt: new Date(),
|
|
});
|
|
await db.insert(companyMemberships).values({
|
|
companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member",
|
|
});
|
|
await db.insert(projects).values({ id: projectId, companyId, name: "Legacy project", status: "active" });
|
|
await db.insert(projectWorkspaces).values({
|
|
id: projectWorkspaceId,
|
|
companyId,
|
|
projectId,
|
|
name: "Primary",
|
|
cwd: fileURLToPath(new URL("../../../", import.meta.url)),
|
|
isPrimary: true,
|
|
});
|
|
await db.insert(agents).values({
|
|
id: agentId,
|
|
companyId,
|
|
name: "Legacy agent",
|
|
adapterType: "codex_local",
|
|
status: "idle",
|
|
runtimeConfig: { heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 } },
|
|
});
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId,
|
|
projectId,
|
|
projectWorkspaceId,
|
|
title: "Complete through the legacy adapter",
|
|
status: "in_progress",
|
|
workMode: "standard",
|
|
assigneeAgentId: agentId,
|
|
});
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
if (temporary) {
|
|
await drainHeartbeatRunsToQuiescence(db, heartbeatService(db));
|
|
await temporary.cleanup();
|
|
}
|
|
});
|
|
|
|
it("executes a flag-off heartbeat through the legacy adapter with byte-stable reads and zero native rows", async () => {
|
|
const execute = adapterExecute.getMockImplementation()!;
|
|
adapterExecute.mockImplementationOnce(async () => {
|
|
await db.update(issues).set({ status: "done" }).where(eq(issues.id, issueId));
|
|
return execute();
|
|
});
|
|
const heartbeat = heartbeatService(db);
|
|
const queued = await heartbeat.wakeup(agentId, {
|
|
source: "automation",
|
|
triggerDetail: "system",
|
|
reason: "issue_commented",
|
|
payload: { issueId },
|
|
contextSnapshot: { issueId, taskId: issueId, skipIssueComment: true },
|
|
});
|
|
expect(queued).not.toBeNull();
|
|
await drainHeartbeatRunsToQuiescence(db, heartbeat);
|
|
const runId = queued!.id;
|
|
const publicBefore = await heartbeat.getRun(runId);
|
|
|
|
expect(adapterExecute).toHaveBeenCalledOnce();
|
|
expect(publicBefore).toMatchObject({
|
|
id: runId,
|
|
status: "succeeded",
|
|
runtimeMode: "legacy",
|
|
runtimeModeReason: "direct_adapter",
|
|
resultJson: { summary: "Legacy bytes", nested: { count: 1, ok: true } },
|
|
});
|
|
await expect(reconcileNativeFinalizations(db, [runId])).resolves.toEqual([]);
|
|
expect(JSON.stringify(await heartbeat.getRun(runId))).toBe(JSON.stringify(publicBefore));
|
|
|
|
await expect(db.select().from(completionContracts).where(eq(completionContracts.companyId, companyId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, runId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, runId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(workAssessments).where(eq(workAssessments.runId, runId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(statusDecisions).where(eq(statusDecisions.companyId, companyId))).resolves.toHaveLength(0);
|
|
await expect(db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId))).resolves.toHaveLength(1);
|
|
}, 30_000);
|
|
});
|