196 lines
5.9 KiB
TypeScript
196 lines
5.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { purgeClosedBugReport } from "../lib/bug-retention";
|
|
import { LinearSupportClient } from "../lib/linear-support-client";
|
|
import type { BugReportRecord } from "../shared/bug-reports";
|
|
|
|
const id = "00000000-0000-4000-8000-000000000001",
|
|
team = "00000000-0000-4000-8000-000000000002";
|
|
const closedAt = new Date(1000).toISOString(),
|
|
due = 1000 + 3 * 86400000;
|
|
const report = {
|
|
id,
|
|
state: "done",
|
|
closed_at: closedAt,
|
|
delete_after: new Date(due).toISOString(),
|
|
linear_done_synced: true,
|
|
} as BugReportRecord;
|
|
test("report retention never purges early, open reports, or an older closure generation", async () => {
|
|
for (const [record, now] of [
|
|
[report, due - 1],
|
|
[{ ...report, state: "open" }, due],
|
|
[{ ...report, closed_at: new Date(2000).toISOString() }, due],
|
|
[null, due],
|
|
] as const) {
|
|
await purgeClosedBugReport(id, closedAt, {
|
|
record: async () => record as BugReportRecord | null,
|
|
now: () => now,
|
|
linear: {
|
|
issue: async () => {
|
|
throw new Error("Must not query Linear");
|
|
},
|
|
remove: async () => assert.fail(),
|
|
},
|
|
begin: async () => {
|
|
assert.fail();
|
|
},
|
|
reopen: async () => assert.fail(),
|
|
removeFiles: async () => assert.fail(),
|
|
finish: async () => assert.fail(),
|
|
});
|
|
}
|
|
});
|
|
test("reopening a Linear issue cancels local cleanup and completed reports purge in a retry-safe order", async () => {
|
|
for (const state of ["started", "completed"]) {
|
|
const actions: string[] = [];
|
|
await purgeClosedBugReport(id, closedAt, {
|
|
record: async () => report,
|
|
now: () => due,
|
|
linear: {
|
|
issue: async () => ({
|
|
id,
|
|
identifier: "TEST-1",
|
|
team: { id: team },
|
|
state: { id: team, name: state, type: state },
|
|
}),
|
|
remove: async () => {
|
|
actions.push("linear");
|
|
},
|
|
},
|
|
begin: async () => {
|
|
actions.push("lock");
|
|
return true;
|
|
},
|
|
reopen: async () => {
|
|
actions.push("reopen");
|
|
},
|
|
removeFiles: async (paths) => {
|
|
assert.deepEqual(paths, [`${id}/input`, `${id}/screenshot.webp`]);
|
|
actions.push("storage");
|
|
},
|
|
finish: async () => {
|
|
actions.push("database");
|
|
},
|
|
});
|
|
assert.deepEqual(
|
|
actions,
|
|
state === "started"
|
|
? ["reopen"]
|
|
: ["lock", "linear", "storage", "database"],
|
|
);
|
|
}
|
|
});
|
|
test("failed external deletion preserves the local retry record", async () => {
|
|
for (const failure of ["linear", "storage"]) {
|
|
let finished = false;
|
|
await assert.rejects(
|
|
purgeClosedBugReport(id, closedAt, {
|
|
record: async () => ({ ...report, purge_started_at: closedAt }),
|
|
now: () => due,
|
|
linear: {
|
|
issue: async () => assert.fail(),
|
|
remove: async () => {
|
|
if (failure === "linear") throw new Error("retry");
|
|
},
|
|
},
|
|
begin: async () => true,
|
|
reopen: async () => assert.fail(),
|
|
removeFiles: async () => {
|
|
if (failure === "storage") throw new Error("retry");
|
|
},
|
|
finish: async () => {
|
|
finished = true;
|
|
},
|
|
}),
|
|
/retry/,
|
|
);
|
|
assert.equal(finished, false);
|
|
}
|
|
});
|
|
test("Linear Done uses the report team's Done state, validates acknowledgment and deletes idempotently", async () => {
|
|
let current = "started",
|
|
deleted = false;
|
|
const calls: string[] = [];
|
|
const client = new LinearSupportClient(
|
|
"private",
|
|
team,
|
|
undefined,
|
|
async (_url, init) => {
|
|
const { query, variables } = JSON.parse(String(init?.body));
|
|
calls.push(query.split("(")[0]);
|
|
if (query.startsWith("query SupportIssue"))
|
|
return Response.json({
|
|
data: {
|
|
issue: deleted
|
|
? null
|
|
: {
|
|
id,
|
|
identifier: "TEST-1",
|
|
team: { id: team },
|
|
state: {
|
|
id: current,
|
|
name: current === "done" ? "Done" : "In Progress",
|
|
type: current === "done" ? "completed" : "started",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
if (query.startsWith("query SupportDoneState")) {
|
|
assert.equal(variables.team, team);
|
|
return Response.json({
|
|
data: {
|
|
team: {
|
|
states: {
|
|
nodes: [{ id: "done", name: "Done", type: "completed" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
if (query.startsWith("mutation SupportDone")) {
|
|
assert.equal(variables.input.stateId, "done");
|
|
current = "done";
|
|
return Response.json({
|
|
data: {
|
|
issueUpdate: {
|
|
success: true,
|
|
issue: { id, state: { id: "done" } },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
if (query.startsWith("mutation SupportDelete")) {
|
|
deleted = true;
|
|
return Response.json({ data: { issueDelete: { success: true } } });
|
|
}
|
|
throw new Error("Unexpected request");
|
|
},
|
|
);
|
|
await client.done(id);
|
|
await client.done(id);
|
|
await client.remove(id);
|
|
await client.remove(id);
|
|
assert.equal(calls.filter((c) => c === "mutation SupportDone").length, 1);
|
|
assert.equal(calls.filter((c) => c === "mutation SupportDelete").length, 1);
|
|
const wrongTeam = new LinearSupportClient(
|
|
"secret",
|
|
team,
|
|
undefined,
|
|
async () =>
|
|
Response.json({ data: { issue: { id, team: { id: "other" } } } }),
|
|
);
|
|
await assert.rejects(wrongTeam.remove(id), /team mismatch/);
|
|
const failed = new LinearSupportClient(
|
|
"secret",
|
|
team,
|
|
undefined,
|
|
async (_url, init) =>
|
|
Response.json({
|
|
data: JSON.parse(String(init?.body)).query.startsWith("query")
|
|
? { issue: { id, team: { id: team } } }
|
|
: { issueDelete: { success: false } },
|
|
}),
|
|
);
|
|
await assert.rejects(failed.remove(id), /not confirmed/);
|
|
});
|