164 lines
9.3 KiB
TypeScript
164 lines
9.3 KiB
TypeScript
import { codexUnreadTriageForTest } from "./src/code-queue";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
type RequestRecord = { path: string; method: string; body: unknown };
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function asRecord(value: unknown): JsonRecord {
|
|
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), "expected JSON object", { value });
|
|
return value as JsonRecord;
|
|
}
|
|
|
|
function asArray(value: unknown): unknown[] {
|
|
assertCondition(Array.isArray(value), "expected JSON array", { value });
|
|
return value as unknown[];
|
|
}
|
|
|
|
function task(id: string, queueId: string, status: string, updatedAt: string, readAt: string | null, prompt: string): JsonRecord {
|
|
return {
|
|
id,
|
|
queueId,
|
|
status,
|
|
currentAttempt: 1,
|
|
updatedAt,
|
|
finishedAt: status === "running" ? null : updatedAt,
|
|
readAt,
|
|
displayPrompt: prompt,
|
|
basePrompt: prompt,
|
|
prompt,
|
|
cwd: "/root/unidesk",
|
|
referenceTaskIds: [],
|
|
lastError: null,
|
|
};
|
|
}
|
|
|
|
function fixtureResponse(path: string, init?: { method?: string; body?: unknown }, requests: RequestRecord[] = []): JsonRecord {
|
|
const method = init?.method ?? "GET";
|
|
requests.push({ path, method, body: init?.body });
|
|
if (path.endsWith("/read")) {
|
|
const taskId = decodeURIComponent(path.split("/api/tasks/")[1]?.split("/")[0] ?? "unknown");
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
ok: true,
|
|
task: task(taskId, "default", "succeeded", "2026-05-22T00:10:00.000Z", "2026-05-22T00:10:01.000Z", "read response prompt should stay compact"),
|
|
queue: { counts: { succeeded: 1 }, unreadTerminal: 0 },
|
|
},
|
|
};
|
|
}
|
|
assertCondition(path.startsWith("/api/microservices/code-queue/proxy/api/tasks/overview"), "unexpected path", { path, method });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
ok: true,
|
|
queue: { unreadTerminal: 4 },
|
|
pagination: {
|
|
limit: 200,
|
|
returned: 6,
|
|
total: 6,
|
|
hasMore: false,
|
|
nextBeforeId: null,
|
|
includeActive: true,
|
|
},
|
|
tasks: [
|
|
task("task-new-1", "default", "succeeded", "2026-05-22T00:05:00.000Z", null, "pikasTech/unidesk#20 closeout RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
task("task-new-2", "review", "failed", "2026-05-22T00:04:00.000Z", null, "https://github.com/pikasTech/unidesk/issues/20 failed task RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
task("task-new-3", "review", "canceled", "2026-05-22T00:03:00.000Z", null, "pikasTech/unidesk#21 canceled task RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
task("task-unknown", "misc", "succeeded", "2026-05-22T00:02:00.000Z", null, "no repo marker RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
task("task-read", "default", "succeeded", "2026-05-22T00:01:00.000Z", "2026-05-22T00:01:01.000Z", "pikasTech/unidesk#20 already read RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
task("task-running", "default", "running", "2026-05-22T00:00:00.000Z", null, "pikasTech/unidesk#20 running RAW_PROMPT_SHOULD_NOT_LEAK"),
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function countItems(bucket: JsonRecord): JsonRecord[] {
|
|
return asArray(bucket.items).map(asRecord);
|
|
}
|
|
|
|
function itemCount(bucket: JsonRecord, key: string): number {
|
|
const row = countItems(bucket).find((item) => item.key === key);
|
|
return typeof row?.count === "number" ? row.count : 0;
|
|
}
|
|
|
|
export function runCodeQueueUnreadTriageContract(): JsonRecord {
|
|
const requests: RequestRecord[] = [];
|
|
const fetcher = (path: string, init?: { method?: string; body?: unknown }): JsonRecord => fixtureResponse(path, init, requests);
|
|
|
|
const summary = codexUnreadTriageForTest(["--limit", "2"], fetcher);
|
|
const summaryBody = JSON.stringify(summary);
|
|
const triage = asRecord(asRecord(summary).unreadTriage);
|
|
const counts = asRecord(triage.counts);
|
|
const newest = asRecord(triage.newest);
|
|
const newestItems = asArray(newest.items).map(asRecord);
|
|
const commands = asRecord(triage.commands);
|
|
|
|
assertCondition(asRecord(summary).ok === true, "default unread triage should succeed", asRecord(summary));
|
|
assertCondition(triage.readOnly === true && triage.bounded === true, "default unread triage must be read-only and bounded", triage);
|
|
assertCondition(counts.totalUnreadTerminal === 4, "counts should include only unread terminal tasks", counts);
|
|
assertCondition(itemCount(asRecord(counts.byRepo), "pikasTech/unidesk") === 3, "repo counts should group owner/name refs", counts);
|
|
assertCondition(itemCount(asRecord(counts.byIssue), "#20") === 2, "issue counts should group issue refs", counts);
|
|
assertCondition(itemCount(asRecord(counts.byStatus), "succeeded") === 2, "status counts should include terminal statuses", counts);
|
|
assertCondition(itemCount(asRecord(counts.byQueue), "review") === 2, "queue counts should include queues", counts);
|
|
assertCondition(newest.returned === 2 && newest.hasMore === true, "newest items should obey --limit and expose pagination", newest);
|
|
assertCondition(newestItems.every((item) => item.commands === undefined && typeof item.nextStep === "string"), "default unread rows must stay compact without repeated per-task command blocks", { newestItems });
|
|
assertCondition(typeof commands.perTaskRead === "string" && String(commands.perTaskRead).includes("codex read <taskId>"), "triage should preserve per-task read drill-down", commands);
|
|
assertCondition(typeof commands.full === "string" && String(commands.full).includes("codex unread") && String(commands.full).includes("--full"), "triage should expose one full-view expansion command", commands);
|
|
assertCondition(!summaryBody.includes("RAW_PROMPT_SHOULD_NOT_LEAK"), "triage output must not dump raw prompt text", { summaryBody });
|
|
assertCondition(!requests.some((request) => request.path.includes("/summary")), "triage must not fetch per-task summaries by default", { requests });
|
|
assertCondition(!requests.some((request) => request.method === "POST"), "default triage must not mutate", { requests });
|
|
|
|
const full = codexUnreadTriageForTest(["--view", "full", "--limit", "2"], fetcher);
|
|
const fullBody = JSON.stringify(full);
|
|
const fullTriage = asRecord(asRecord(full).unreadTriage);
|
|
const fullNewest = asRecord(fullTriage.newest);
|
|
const fullItems = asArray(fullNewest.items).map(asRecord);
|
|
const fullItemCommands = fullItems.map((item) => asRecord(item.commands));
|
|
assertCondition(asRecord(fullTriage.filters).view === "full", "codex unread --view full should disclose full view", fullTriage);
|
|
assertCondition(fullItems.length === 2 && fullItemCommands.every((item) => typeof item.detail === "string" && typeof item.read === "string"), "explicit full unread view should expand per-task commands", { fullItems });
|
|
assertCondition(summaryBody.length < fullBody.length, "default unread summary should stay smaller than explicit full view", { summaryChars: summaryBody.length, fullChars: fullBody.length });
|
|
|
|
const guardStart = requests.length;
|
|
const guarded = codexUnreadTriageForTest(["mark-read", "--repo", "pikasTech/unidesk", "--issue", "20", "--limit", "2"], fetcher);
|
|
const guardedTriage = asRecord(asRecord(guarded).unreadTriage);
|
|
const guardedMutation = asRecord(guardedTriage.mutation);
|
|
assertCondition(asRecord(guarded).ok === false, "batch mark-read without --confirm should fail closed", asRecord(guarded));
|
|
assertCondition(guardedMutation.blocked === true && guardedMutation.confirmed === false, "confirm guard should describe blocked mutation", guardedMutation);
|
|
assertCondition(!requests.slice(guardStart).some((request) => request.method === "POST"), "missing --confirm must not POST read calls", { requests: requests.slice(guardStart) });
|
|
|
|
const confirmStart = requests.length;
|
|
const confirmed = codexUnreadTriageForTest(["mark-read", "--repo", "pikasTech/unidesk", "--issue", "20", "--limit", "2", "--confirm"], fetcher);
|
|
const confirmedTriage = asRecord(asRecord(confirmed).unreadTriage);
|
|
const confirmedMutation = asRecord(confirmedTriage.mutation);
|
|
const readPosts = requests.slice(confirmStart).filter((request) => request.method === "POST" && request.path.endsWith("/read"));
|
|
assertCondition(asRecord(confirmed).ok === true, "confirmed batch mark-read should succeed", asRecord(confirmed));
|
|
assertCondition(confirmedTriage.readOnly === false && confirmedMutation.confirmed === true, "confirmed mutation should be explicit", confirmedMutation);
|
|
assertCondition(confirmedMutation.attempted === 2 && confirmedMutation.succeeded === 2, "confirmed mutation should respect filters and limit", confirmedMutation);
|
|
assertCondition(readPosts.length === 2 && readPosts[0]?.path.includes("task-new-1") && readPosts[1]?.path.includes("task-new-2"), "confirmed mutation should POST newest filtered task reads only", { readPosts });
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"default unread triage is read-only",
|
|
"repo/issue/status/queue counts are present",
|
|
"newest items are bounded",
|
|
"default rows avoid repeated per-task command blocks",
|
|
"--view full expands per-task commands",
|
|
"raw prompt text is omitted",
|
|
"batch mark-read requires --confirm",
|
|
"confirmed batch read respects filters and limit",
|
|
],
|
|
summaryChars: summaryBody.length,
|
|
fullChars: fullBody.length,
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runCodeQueueUnreadTriageContract(), null, 2)}\n`);
|
|
}
|