325 lines
18 KiB
TypeScript
325 lines
18 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { writeFileSync, unlinkSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { codexResumeTaskForTest } from "./src/code-queue";
|
|
import { findResumeTraceConfirmation, resumeDuplicateDecision, resumeTraceText } from "../src/components/microservices/code-queue/src/resume-confirmation";
|
|
import type { QueueTask } from "../src/components/microservices/code-queue/src/types";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function nestedRecord(value: unknown, path: string[]): JsonRecord {
|
|
let current: unknown = value;
|
|
for (const key of path) {
|
|
assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected object while traversing JSON", { path, key, current });
|
|
current = (current as JsonRecord)[key];
|
|
}
|
|
assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected nested object", { path, current });
|
|
return current as JsonRecord;
|
|
}
|
|
|
|
function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; json: JsonRecord | null } {
|
|
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
|
cwd: process.cwd(),
|
|
input: stdin,
|
|
encoding: "utf8",
|
|
});
|
|
const stdout = String(result.stdout || "");
|
|
let json: JsonRecord | null = null;
|
|
try {
|
|
json = JSON.parse(stdout) as JsonRecord;
|
|
} catch {
|
|
json = null;
|
|
}
|
|
return { status: result.status, stdout, stderr: String(result.stderr || ""), json };
|
|
}
|
|
|
|
function fixtureTask(): QueueTask {
|
|
const at = "2026-05-23T00:00:00.000Z";
|
|
return {
|
|
id: "codex_resume_fixture",
|
|
queueId: "default",
|
|
queueEnteredAt: at,
|
|
prompt: "base",
|
|
basePrompt: "base",
|
|
referenceTaskIds: [],
|
|
referenceInjection: null,
|
|
providerId: "D601",
|
|
cwd: "/workspace/unidesk",
|
|
model: "gpt-5.5",
|
|
reasoningEffort: null,
|
|
executionMode: "default",
|
|
maxAttempts: 99,
|
|
status: "succeeded",
|
|
createdAt: at,
|
|
updatedAt: at,
|
|
startedAt: at,
|
|
finishedAt: "2026-05-23T00:10:00.000Z",
|
|
readAt: null,
|
|
currentAttempt: 1,
|
|
currentMode: "initial",
|
|
codexThreadId: "thread_resume_fixture",
|
|
activeTurnId: null,
|
|
finalResponse: "done",
|
|
lastError: null,
|
|
lastJudge: null,
|
|
judgeFailCount: 0,
|
|
promptHistory: [],
|
|
output: [],
|
|
events: [],
|
|
attempts: [],
|
|
cancelRequested: false,
|
|
nextPrompt: null,
|
|
nextMode: null,
|
|
};
|
|
}
|
|
|
|
function deterministicResumeId(taskId: string, prompt: string): string {
|
|
return `resume_${Bun.SHA256.hash(`unidesk-code-queue-resume:v1\0${taskId}\0${prompt}`, "hex").slice(0, 24)}`;
|
|
}
|
|
|
|
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
|
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
|
const data = nestedRecord(result.json?.data, []);
|
|
assertCondition(data.ok === false, `${command} frozen payload should be ok=false`, data);
|
|
assertCondition(data.frozen === true, `${command} frozen payload should expose frozen=true`, data);
|
|
assertCondition(data.mutation === false, `${command} frozen payload should be non-mutating`, data);
|
|
assertCondition(data.degradedReason === "legacy-code-queue-frozen", `${command} should use the legacy frozen reason`, data);
|
|
assertCondition(data.command === command, `${command} frozen payload should identify the command`, data);
|
|
const replacement = nestedRecord(data, ["replacement"]);
|
|
assertCondition(String(replacement.sessionsSteer || "").includes("agentrun sessions steer"), `${command} should point to AgentRun sessions steer`, replacement);
|
|
const legacy = nestedRecord(data, ["legacy"]);
|
|
assertCondition(legacy.noDoubleWrite === true, `${command} should document no double-write`, legacy);
|
|
}
|
|
|
|
function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
|
assertCondition(response.ok === true, "CLI dry-run should succeed", response);
|
|
const data = nestedRecord(response.data, []);
|
|
assertCondition(data.dryRun === true, "dry-run response should expose dryRun=true", data);
|
|
const request = nestedRecord(response.data, ["request"]);
|
|
assertCondition(request.method === "POST", "dry-run should expose request method", request);
|
|
assertCondition(request.path === "/api/tasks/codex_test_task/resume", "dry-run should expose resume path", request);
|
|
assertCondition(request.stableProxyPath === "/api/microservices/code-queue/proxy/api/tasks/codex_test_task/resume", "dry-run should expose stable proxy path", request);
|
|
assertCondition(request.resumeId === deterministicResumeId("codex_test_task", expectedText), "dry-run should expose deterministic resumeId", request);
|
|
const prompt = nestedRecord(response.data, ["request", "body", "prompt"]);
|
|
assertCondition(prompt.text === expectedText, "dry-run prompt text mismatch", prompt);
|
|
assertCondition(prompt.chars === expectedText.length, "dry-run prompt char count mismatch", prompt);
|
|
const commands = nestedRecord(response.data, ["commands"]);
|
|
assertCondition(String(commands.run || "").includes(`--resume-id ${String(request.resumeId)}`), "dry-run should expose same resumeId run command", commands);
|
|
}
|
|
|
|
export function runCodeQueueResumeContract(): JsonRecord {
|
|
const positional = runCli(["codex", "resume", "codex_test_task", "fix the PR conflict", "--dry-run"]);
|
|
assertLegacyFrozenWrite(positional, "codex resume");
|
|
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional resume prompt", positional.json ?? {});
|
|
assertCondition(!String(positional.json?.command || "").includes("fix the PR conflict"), "outer command must not echo positional resume prompt", positional.json ?? {});
|
|
|
|
const stdin = runCli(["codex", "resume", "codex_test_task", "--prompt-stdin", "--dry-run"], "stdin resume prompt\n");
|
|
assertLegacyFrozenWrite(stdin, "codex resume");
|
|
assertCondition(!stdin.stdout.includes("stdin resume prompt"), "frozen resume must not echo stdin prompt", { stdout: stdin.stdout });
|
|
|
|
const promptFile = join(tmpdir(), `unidesk-code-queue-resume-${process.pid}.txt`);
|
|
writeFileSync(promptFile, "file resume prompt", "utf8");
|
|
try {
|
|
const fromFile = runCli(["codex", "resume", "codex_test_task", "--prompt-file", promptFile, "--dry-run"]);
|
|
assertLegacyFrozenWrite(fromFile, "codex resume");
|
|
assertCondition(!fromFile.stdout.includes("file resume prompt"), "frozen resume must not echo file prompt", { stdout: fromFile.stdout });
|
|
} finally {
|
|
unlinkSync(promptFile);
|
|
}
|
|
|
|
const duplicateSource = runCli(["codex", "resume", "codex_test_task", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
|
|
assertLegacyFrozenWrite(duplicateSource, "codex resume");
|
|
|
|
const help = runCli(["codex", "help"]);
|
|
const usage = Array.isArray(nestedRecord(help.json?.data, []).usage) ? nestedRecord(help.json?.data, []).usage as unknown[] : [];
|
|
assertCondition(usage.some((line) => String(line).includes("codex resume <taskId>")), "codex help should list resume", { usage: usage.map(String) });
|
|
|
|
let dryRunFetchCount = 0;
|
|
const dryRunDirect = codexResumeTaskForTest("direct_task", ["do not send", "--dry-run"], () => {
|
|
dryRunFetchCount += 1;
|
|
return { ok: true, status: 200, body: { ok: true } };
|
|
});
|
|
assertCondition(dryRunFetchCount === 0, "dry-run must not call stable proxy helper", { dryRunFetchCount, dryRunDirect });
|
|
|
|
const longPrompt = `${"x".repeat(480)}-tail-secret-marker`;
|
|
const longDryRun = codexResumeTaskForTest("direct_task", [longPrompt, "--dry-run"], () => {
|
|
throw new Error("dry-run should not fetch");
|
|
}) as JsonRecord;
|
|
const longPreview = nestedRecord(longDryRun, ["request", "body", "prompt"]);
|
|
assertCondition(longPreview.truncated === true, "long dry-run prompt should be truncated", longPreview);
|
|
assertCondition(!String(longPreview.text || "").includes("tail-secret-marker"), "long dry-run must not leak prompt tail", longPreview);
|
|
|
|
let fetchPath = "";
|
|
let fetchMethod = "";
|
|
let fetchPrompt = "";
|
|
let fetchResumeId = "";
|
|
const success = codexResumeTaskForTest("direct_task", ["resume this context"], (path, init) => {
|
|
fetchPath = path;
|
|
fetchMethod = String(init?.method || "");
|
|
fetchPrompt = String((init?.body as JsonRecord | undefined)?.prompt || "");
|
|
fetchResumeId = String((init?.body as JsonRecord | undefined)?.resumeId || "");
|
|
return {
|
|
ok: true,
|
|
status: 202,
|
|
body: {
|
|
ok: true,
|
|
accepted: true,
|
|
duplicateSuppressed: false,
|
|
deliveryState: "queued_for_existing_thread",
|
|
resumeId: fetchResumeId,
|
|
turnId: 9,
|
|
reuseOriginalThread: true,
|
|
originalCodexThreadId: "thread_original",
|
|
codexThreadId: "thread_original",
|
|
traceConfirmation: {
|
|
taskId: "direct_task",
|
|
resumeId: fetchResumeId,
|
|
found: true,
|
|
accepted: true,
|
|
deliveryState: "queued_for_existing_thread",
|
|
matchCount: 1,
|
|
trace: { seq: 9, at: "2026-05-23T00:00:09.000Z", method: "turn/resume", resumeId: fetchResumeId, promptChars: 19, promptHash: "hash", promptOmitted: true, source: "output" },
|
|
duplicateSuppressionKey: fetchResumeId,
|
|
promptOmitted: true,
|
|
},
|
|
task: { id: "direct_task", status: "queued", codexThreadId: "thread_original", currentAttempt: 1, currentMode: "initial", prompt: "hidden" },
|
|
queue: { activeTaskIds: [], queuedTaskIds: ["direct_task"] },
|
|
},
|
|
};
|
|
}) as JsonRecord;
|
|
assertCondition(fetchPath === "/api/microservices/code-queue/proxy/api/tasks/direct_task/resume", "non-dry-run should use stable resume path", { fetchPath });
|
|
assertCondition(fetchMethod === "POST", "non-dry-run should POST", { fetchMethod });
|
|
assertCondition(fetchPrompt === "resume this context", "non-dry-run should send raw prompt in body", { fetchPrompt });
|
|
assertCondition(fetchResumeId === deterministicResumeId("direct_task", "resume this context"), "non-dry-run should send deterministic resumeId", { fetchResumeId });
|
|
assertCondition(nestedRecord(success, ["resume"]).accepted === true, "successful resume should report accepted=true", success);
|
|
assertCondition(nestedRecord(success, ["resume"]).reusedCodexThread === true, "successful resume should report thread reuse", success);
|
|
assertCondition(nestedRecord(success, ["resume"]).promptOmitted === true, "successful resume should mark prompt omitted", success);
|
|
assertCondition(nestedRecord(success, ["resume"]).deliveryState === "queued_for_existing_thread", "successful resume should expose delivery state", success);
|
|
assertCondition(!JSON.stringify(success).includes("resume this context"), "successful resume must not echo prompt text", success);
|
|
|
|
const explicitResumeId = "resume_manual_12345";
|
|
const duplicateSuppressed = codexResumeTaskForTest("direct_task", ["same prompt", "--resume-id", explicitResumeId], (_path, init) => {
|
|
assertCondition((init?.body as JsonRecord | undefined)?.resumeId === explicitResumeId, "explicit resumeId should be sent unchanged", (init?.body as JsonRecord | undefined) ?? {});
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
ok: true,
|
|
accepted: true,
|
|
duplicateSuppressed: true,
|
|
deliveryState: "duplicate_suppressed",
|
|
resumeId: explicitResumeId,
|
|
reuseOriginalThread: true,
|
|
originalCodexThreadId: "thread_original",
|
|
codexThreadId: "thread_original",
|
|
traceConfirmation: {
|
|
taskId: "direct_task",
|
|
resumeId: explicitResumeId,
|
|
found: true,
|
|
accepted: true,
|
|
deliveryState: "queued_for_existing_thread",
|
|
matchCount: 1,
|
|
trace: { seq: 11, at: "2026-05-23T00:00:11.000Z", method: "turn/resume", resumeId: explicitResumeId, promptChars: 11, promptHash: "hash2", promptOmitted: true, source: "output" },
|
|
duplicateSuppressionKey: explicitResumeId,
|
|
},
|
|
task: { id: "direct_task", status: "queued", codexThreadId: "thread_original", prompt: "hidden" },
|
|
queue: { queuedTaskIds: ["direct_task"] },
|
|
},
|
|
};
|
|
}) as JsonRecord;
|
|
assertCondition(nestedRecord(duplicateSuppressed, ["resume"]).status === "duplicate_suppressed", "duplicate resume should expose suppression status", duplicateSuppressed);
|
|
assertCondition(nestedRecord(duplicateSuppressed, ["resume"]).duplicateSuppressed === true, "duplicate resume should expose duplicateSuppressed", duplicateSuppressed);
|
|
|
|
const conflictPrompt = "changed resume request requested-secret-marker";
|
|
const conflict = codexResumeTaskForTest("direct_task", [conflictPrompt, "--resume-id", explicitResumeId], () => ({
|
|
ok: false,
|
|
status: 409,
|
|
body: {
|
|
ok: false,
|
|
error: "resumeId already exists with a different prompt hash",
|
|
accepted: false,
|
|
deliveryState: "not_accepted",
|
|
resumeId: explicitResumeId,
|
|
existingPromptHash: "old",
|
|
requestedPromptHash: "new",
|
|
traceConfirmation: {
|
|
taskId: "direct_task",
|
|
resumeId: explicitResumeId,
|
|
found: true,
|
|
accepted: true,
|
|
deliveryState: "queued_for_existing_thread",
|
|
matchCount: 1,
|
|
trace: { seq: 11, at: "2026-05-23T00:00:11.000Z", method: "turn/resume", resumeId: explicitResumeId, promptChars: 11, promptHash: "old", promptOmitted: true, source: "output" },
|
|
},
|
|
task: { id: "direct_task", status: "queued", prompt: `${"hidden ".repeat(80)}task-secret-marker` },
|
|
},
|
|
})) as JsonRecord;
|
|
assertCondition(conflict.ok === false, "resumeId conflict should fail", conflict);
|
|
assertCondition(nestedRecord(conflict, ["resume"]).status === "not_accepted", "conflict should expose not_accepted", conflict);
|
|
assertCondition(!JSON.stringify(conflict).includes("requested-secret-marker"), "conflict must not echo requested prompt", conflict);
|
|
assertCondition(!JSON.stringify(conflict).includes("task-secret-marker"), "conflict must not echo full task prompt by default", conflict);
|
|
|
|
const runningRejection = codexResumeTaskForTest("running_task", ["use steer instead"], () => ({
|
|
ok: false,
|
|
status: 409,
|
|
body: {
|
|
ok: false,
|
|
error: "task is active: running",
|
|
accepted: false,
|
|
deliveryState: "not_accepted",
|
|
disposition: "use-steer-for-active-task",
|
|
resumeId: deterministicResumeId("running_task", "use steer instead"),
|
|
task: { id: "running_task", status: "running", terminalStatus: null, prompt: "hidden active task" },
|
|
},
|
|
})) as JsonRecord;
|
|
assertCondition(runningRejection.ok === false, "running task resume should fail closed", runningRejection);
|
|
assertCondition(nestedRecord(runningRejection, ["resume"]).reason === "use-steer-for-active-task", "running resume should route to steer", runningRejection);
|
|
assertCondition(String(nestedRecord(runningRejection, ["commands"]).steer || "").includes("codex steer running_task"), "running rejection should provide steer command", runningRejection);
|
|
|
|
const notFound = codexResumeTaskForTest("missing_task", ["prompt"], () => ({ ok: false, status: 404, body: { ok: false, error: "task not found" } })) as JsonRecord;
|
|
assertCondition(notFound.ok === false, "missing task resume should fail", notFound);
|
|
assertCondition(nestedRecord(notFound, ["resume"]).status === "not_accepted", "missing task should be not accepted", notFound);
|
|
|
|
const task = fixtureTask();
|
|
const resumeId = "resume_contract_12345";
|
|
const prompt = "continue the same PR";
|
|
task.output.push({ seq: 9, at: "2026-05-23T00:00:09.000Z", channel: "user", method: "turn/resume", itemId: resumeId, text: resumeTraceText(resumeId, prompt) });
|
|
const confirmation = findResumeTraceConfirmation(task, resumeId);
|
|
assertCondition(confirmation.found === true && confirmation.accepted === true, "confirmation should find resume trace by resumeId", confirmation as unknown as JsonRecord);
|
|
assertCondition(confirmation.trace?.promptChars === prompt.length, "confirmation should expose prompt chars without prompt text", (confirmation.trace ?? {}) as unknown as JsonRecord);
|
|
assertCondition(!JSON.stringify(confirmation).includes(prompt), "confirmation must not echo prompt text", confirmation as unknown as JsonRecord);
|
|
const duplicate = resumeDuplicateDecision(task, resumeId, prompt);
|
|
assertCondition(duplicate.duplicate === true && duplicate.conflict === false, "same resumeId and prompt should be duplicate-suppressed", duplicate as unknown as JsonRecord);
|
|
const localConflict = resumeDuplicateDecision(task, resumeId, "changed prompt");
|
|
assertCondition(localConflict.duplicate === false && localConflict.conflict === true, "same resumeId with changed prompt should conflict", localConflict as unknown as JsonRecord);
|
|
const newlineResumeId = "resume_contract_newline";
|
|
const newlinePrompt = "keep trailing newline\n";
|
|
task.output.push({ seq: 10, at: "2026-05-23T00:00:10.000Z", channel: "user", method: "turn/resume", itemId: newlineResumeId, text: resumeTraceText(newlineResumeId, newlinePrompt) });
|
|
const newlineDuplicate = resumeDuplicateDecision(task, newlineResumeId, newlinePrompt);
|
|
assertCondition(newlineDuplicate.duplicate === true && newlineDuplicate.conflict === false, "duplicate suppression should use exact prompt hash, including trailing newline", newlineDuplicate as unknown as JsonRecord);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"legacy resume positional/stdin/file dry-runs are frozen",
|
|
"bounded disclosure and outer command redaction",
|
|
"non-dry-run sends resumeId and omits prompt from output",
|
|
"terminal resume accepted with thread reuse metadata",
|
|
"duplicate suppression and conflict behavior",
|
|
"running task fails closed with steer command",
|
|
"missing task fails closed",
|
|
"local resume trace confirmation helpers",
|
|
"exact prompt hash survives trailing newline",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runCodeQueueResumeContract(), null, 2)}\n`);
|
|
}
|