59 lines
3.9 KiB
TypeScript
59 lines
3.9 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
import postgres from "postgres";
|
|
import { describe, expect, it } from "vitest";
|
|
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
|
|
|
|
const support = await getEmbeddedPostgresTestSupport();
|
|
const migrations = [
|
|
"0240_pink_fantastic_four.sql", "0241_conscious_adam_destine.sql",
|
|
"0242_wide_lightspeed.sql", "0243_sleepy_metal_master.sql", "0244_organic_meltdown.sql", "0245_misty_nightshade.sql",
|
|
].map((name) => readFileSync(new URL(`./migrations/${name}`, import.meta.url), "utf8"));
|
|
|
|
(support.supported ? describe : describe.skip)("execution identity migration", () => {
|
|
it("can replay without inventing historical authorship or losing accepted contexts", async () => {
|
|
const database = await startEmbeddedPostgresTestDatabase("paperclip-identity-migration-");
|
|
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
|
|
try {
|
|
const companyId = randomUUID(), agentId = randomUUID(), historicalRunId = randomUUID(), runId = randomUUID(), contextId = randomUUID();
|
|
await sql`INSERT INTO companies (id, name, issue_prefix) VALUES (${companyId}, 'Identity migration', 'MIG')`;
|
|
await sql`INSERT INTO agents (id, company_id, name, role, adapter_type) VALUES (${agentId}, ${companyId}, 'Shared', 'engineer', 'codex_local')`;
|
|
await sql`INSERT INTO heartbeat_runs (id, company_id, agent_id, status) VALUES
|
|
(${historicalRunId}, ${companyId}, ${agentId}, 'succeeded'), (${runId}, ${companyId}, ${agentId}, 'running')`;
|
|
await sql`INSERT INTO run_identity_contexts (id, company_id, run_id, revision, responsible_user_id, cause, correlation_id, accepted_at)
|
|
VALUES (${contextId}, ${companyId}, ${runId}, 1, 'person-a', 'instruction', 'dispatch', now())`;
|
|
await sql`UPDATE heartbeat_runs SET active_identity_context_id = ${contextId}, responsible_user_id = 'person-a' WHERE id = ${runId}`;
|
|
for (let pass = 0; pass < 2; pass++) {
|
|
for (const migration of migrations) {
|
|
for (const statement of migration.split('--> statement-breakpoint')) {
|
|
if (statement.trim()) await sql.unsafe(statement);
|
|
}
|
|
}
|
|
}
|
|
const [historical] = await sql`SELECT active_identity_context_id, responsible_user_id FROM heartbeat_runs WHERE id = ${historicalRunId}`;
|
|
expect(historical).toEqual({ active_identity_context_id: null, responsible_user_id: null });
|
|
const contexts = await sql`SELECT id, responsible_user_id FROM run_identity_contexts WHERE company_id = ${companyId}`;
|
|
expect(contexts).toEqual([{ id: contextId, responsible_user_id: 'person-a' }]);
|
|
const [active] = await sql`SELECT active_identity_context_id FROM heartbeat_runs WHERE id = ${runId}`;
|
|
expect(active.active_identity_context_id).toBe(contextId);
|
|
// Agent removal deletes its run rows, but surviving task continuations
|
|
// must retain attribution. Even replaying the migration set must be safe.
|
|
await sql`DELETE FROM heartbeat_runs WHERE id = ${runId}`;
|
|
for (const migration of migrations) {
|
|
for (const statement of migration.split('--> statement-breakpoint')) {
|
|
if (statement.trim()) await sql.unsafe(statement);
|
|
}
|
|
}
|
|
const [retained] = await sql`SELECT run_id, responsible_user_id FROM run_identity_contexts WHERE id = ${contextId}`;
|
|
expect(retained).toEqual({ run_id: runId, responsible_user_id: 'person-a' });
|
|
await sql`DELETE FROM heartbeat_runs WHERE company_id = ${companyId}`;
|
|
await sql`DELETE FROM agents WHERE company_id = ${companyId}`;
|
|
await sql`DELETE FROM companies WHERE id = ${companyId}`;
|
|
expect(await sql`SELECT id FROM run_identity_contexts WHERE id = ${contextId}`).toHaveLength(0);
|
|
} finally {
|
|
await sql.end();
|
|
await database.cleanup();
|
|
}
|
|
}, 30_000);
|
|
});
|