fix: unify codex queues json shape
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { codexQueuesQueryForTest } from "./src/code-queue";
|
||||
|
||||
type JsonRecord = Record<string, 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 fixtureResponse(): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
ok: true,
|
||||
queue: {
|
||||
total: 4,
|
||||
queueCount: 3,
|
||||
activeQueueIds: ["alpha"],
|
||||
activeTaskIds: ["task-running"],
|
||||
queuedTaskIds: ["task-queued"],
|
||||
counts: { running: 1, queued: 2, succeeded: 1 },
|
||||
unreadTerminal: 1,
|
||||
executionDiagnostics: {
|
||||
state: "split-brain",
|
||||
splitBrain: true,
|
||||
heartbeatFreshTaskIds: ["task-running"],
|
||||
databaseActiveTaskCount: 1,
|
||||
databaseActiveTaskIds: ["task-running"],
|
||||
schedulerActiveRunSlotCount: 1,
|
||||
schedulerActiveTaskIds: ["task-running"],
|
||||
},
|
||||
},
|
||||
queues: [
|
||||
{
|
||||
id: "alpha",
|
||||
name: "Alpha",
|
||||
total: 1,
|
||||
counts: { running: 1, queued: 0 },
|
||||
unreadTerminal: 0,
|
||||
activeTaskId: "task-running",
|
||||
runnableTaskId: null,
|
||||
updatedAt: "2026-05-20T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "beta",
|
||||
name: "Beta",
|
||||
total: 2,
|
||||
counts: { running: 0, queued: 2 },
|
||||
unreadTerminal: 0,
|
||||
activeTaskId: null,
|
||||
runnableTaskId: "task-queued",
|
||||
updatedAt: "2026-05-20T00:01:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "gamma",
|
||||
name: "Gamma",
|
||||
total: 1,
|
||||
counts: { succeeded: 1 },
|
||||
unreadTerminal: 1,
|
||||
activeTaskId: null,
|
||||
runnableTaskId: null,
|
||||
updatedAt: "2026-05-20T00:02:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assertQueuesShape(label: string, result: unknown, expectedView: string): void {
|
||||
const data = asRecord(result);
|
||||
const queues = asRecord(data.queues);
|
||||
assertCondition(queues.view === expectedView, `${label} view mismatch`, queues);
|
||||
const items = asArray(queues.items);
|
||||
assertCondition(items.length === 3, `${label} must expose queue rows at data.queues.items[]`, queues);
|
||||
const first = asRecord(items[0]);
|
||||
assertCondition(first.id === "alpha", `${label} first item id mismatch`, first);
|
||||
const firstCounts = asRecord(first.counts);
|
||||
assertCondition(firstCounts.running === 1, `${label} item counts should be preserved`, first);
|
||||
const counts = asRecord(queues.counts);
|
||||
assertCondition(counts.running === 1 && counts.queued === 2, `${label} global counts should be preserved`, counts);
|
||||
const diagnostics = asRecord(queues.executionDiagnostics);
|
||||
assertCondition(diagnostics.state === "split-brain", `${label} executionDiagnostics should be preserved`, diagnostics);
|
||||
assertCondition(diagnostics.effectiveLiveness === "live", `${label} diagnostics should retain derived liveness`, diagnostics);
|
||||
assertCondition(Array.isArray(queues.activeTaskIds), `${label} activeTaskIds should be present`, queues);
|
||||
assertCondition(Array.isArray(queues.queuedTaskIds), `${label} queuedTaskIds should be present`, queues);
|
||||
}
|
||||
|
||||
export function runCodeQueueQueuesShapeContract(): JsonRecord {
|
||||
const fetcher = (path: string): JsonRecord => {
|
||||
assertCondition(path === "/api/microservices/code-queue/proxy/api/queues", "codex queues should use stable proxy path", { path });
|
||||
return fixtureResponse();
|
||||
};
|
||||
|
||||
const summary = codexQueuesQueryForTest([], fetcher);
|
||||
assertQueuesShape("summary", summary, "summary");
|
||||
const summaryQueues = asRecord(asRecord(summary).queues);
|
||||
assertCondition(summaryQueues.deprecatedFullArray === undefined, "summary should not expose deprecated full array compatibility field", summaryQueues);
|
||||
|
||||
const full = codexQueuesQueryForTest(["--full"], fetcher);
|
||||
assertQueuesShape("full", full, "full");
|
||||
const fullQueues = asRecord(asRecord(full).queues);
|
||||
assertCondition(!Array.isArray(fullQueues), "full queues payload must be an object, not the deprecated array shape", fullQueues);
|
||||
assertCondition(fullQueues.bounded === false, "full without --limit should preserve complete queue listing semantics", fullQueues);
|
||||
const deprecatedFullArray = asArray(fullQueues.deprecatedFullArray);
|
||||
assertCondition(deprecatedFullArray.length === 3, "full should expose deprecated array only under a compatibility field", fullQueues);
|
||||
const compatibility = asRecord(fullQueues.compatibility);
|
||||
assertCondition(compatibility.stablePath === "data.queues.items[]", "compatibility metadata should document stable path", compatibility);
|
||||
assertCondition(compatibility.deprecated === true, "compatibility metadata should mark old array path deprecated", compatibility);
|
||||
|
||||
const limitedFull = codexQueuesQueryForTest(["--full", "--limit", "2"], fetcher);
|
||||
const limitedFullQueues = asRecord(asRecord(limitedFull).queues);
|
||||
assertCondition(limitedFullQueues.bounded === true, "full with explicit --limit should be bounded", limitedFullQueues);
|
||||
assertCondition(asArray(limitedFullQueues.items).length === 2, "full with explicit --limit should limit data.queues.items[]", limitedFullQueues);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"summary data.queues.items[] shape",
|
||||
"summary queue metadata",
|
||||
"full data.queues.items[] shape",
|
||||
"full queue metadata",
|
||||
"deprecated full array compatibility field",
|
||||
"full explicit limit remains bounded",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(runCodeQueueQueuesShapeContract(), null, 2)}\n`);
|
||||
}
|
||||
@@ -144,6 +144,7 @@ interface CodexTasksDegraded {
|
||||
interface CodexQueuesOptions {
|
||||
full: boolean;
|
||||
limit: number;
|
||||
limitExplicit: boolean;
|
||||
}
|
||||
|
||||
type CodexRequestInit = { method?: string; body?: unknown };
|
||||
@@ -989,6 +990,7 @@ function parseQueuesOptions(args: string[]): CodexQueuesOptions {
|
||||
return {
|
||||
full: hasFlag(args, "--full") || hasFlag(args, "--all"),
|
||||
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
|
||||
limitExplicit: args.includes("--limit"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1675,13 +1677,14 @@ function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueu
|
||||
const runnableQueues = queues.filter((row) => row.runnableTaskId !== null && row.runnableTaskId !== undefined);
|
||||
const activeQueues = queues.filter((row) => typeof row.id === "string" && activeIds.includes(row.id));
|
||||
const selected = options.full ? queues : Array.from(new Map([...activeQueues, ...unreadQueues, ...runnableQueues, ...nonemptyQueues].map((row) => [String(row.id), row])).values());
|
||||
const visible = selected.slice(0, options.limit);
|
||||
const limitApplied = !options.full || options.limitExplicit;
|
||||
const visible = limitApplied ? selected.slice(0, options.limit) : selected;
|
||||
const diagnostics = compactExecutionDiagnostics(queue.executionDiagnostics);
|
||||
return {
|
||||
upstream,
|
||||
queues: {
|
||||
view: options.full ? "full" : "summary",
|
||||
bounded: true,
|
||||
bounded: limitApplied,
|
||||
count: selected.length,
|
||||
returned: visible.length,
|
||||
hasMore: selected.length > visible.length,
|
||||
@@ -1700,6 +1703,17 @@ function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueu
|
||||
unreadTerminal: queue.unreadTerminal ?? 0,
|
||||
executionDiagnostics: diagnostics,
|
||||
items: visible,
|
||||
...(options.full
|
||||
? {
|
||||
deprecatedFullArray: asArray(body.queues),
|
||||
compatibility: {
|
||||
deprecated: true,
|
||||
deprecatedPath: "data.queues.deprecatedFullArray[]",
|
||||
stablePath: "data.queues.items[]",
|
||||
message: "Use data.queues.items[] for both codex queues and codex queues --full.",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
commands: {
|
||||
refresh: `bun scripts/cli.ts codex queues${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
|
||||
full: `bun scripts/cli.ts codex queues --full${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
|
||||
@@ -1711,13 +1725,16 @@ function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueu
|
||||
};
|
||||
}
|
||||
|
||||
function codeQueues(optionArgs: string[] = []): unknown {
|
||||
export function codexQueuesQueryForTest(optionArgs: string[], fetcher: CodexResponseFetcher): unknown {
|
||||
const options = parseQueuesOptions(optionArgs);
|
||||
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
|
||||
if (options.full) return { upstream: response.upstream, queues: response.body.queues ?? [], queue: compactQueueMutationSummary(response.body.queue), commands: { summary: "bun scripts/cli.ts codex queues" } };
|
||||
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath("/api/queues")));
|
||||
return compactQueuesResponse(response.body, options, response.upstream);
|
||||
}
|
||||
|
||||
function codeQueues(optionArgs: string[] = []): unknown {
|
||||
return codexQueuesQueryForTest(optionArgs, coreInternalFetch);
|
||||
}
|
||||
|
||||
function codexCreateQueue(queueId: string): unknown {
|
||||
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user