155 lines
8.3 KiB
TypeScript
155 lines
8.3 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { compactSubmitSuccessResponseForTest } from "./src/code-queue";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
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 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 stringArray(value: unknown): string[] {
|
|
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
}
|
|
|
|
function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
|
assertCondition(response.ok === true, "submit dry-run should succeed", response);
|
|
const data = nestedRecord(response.data, []);
|
|
assertCondition(data.dryRun === true, "submit dry-run should expose dryRun=true", data);
|
|
const request = nestedRecord(response.data, ["request"]);
|
|
const prompt = nestedRecord(request, ["prompt"]);
|
|
assertCondition(prompt.text === expectedText, "submit dry-run prompt text mismatch", prompt);
|
|
assertCondition(prompt.chars === expectedText.length, "submit dry-run prompt char count mismatch", prompt);
|
|
assertCondition(prompt.truncated === false, "submit dry-run prompt must expose the full prompt", prompt);
|
|
}
|
|
|
|
export function runCodeQueueCliSubmitPromptContract(): JsonRecord {
|
|
const multilinePrompt = [
|
|
"Goal: verify stdin prompt path",
|
|
"JSON: {\"quote\":\"'single' and \\\"double\\\"\"}",
|
|
"Markdown: `code` | table | value",
|
|
"Backslash: C:\\tmp\\prompt",
|
|
"",
|
|
].join("\n");
|
|
|
|
const stdin = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-contract", "--dry-run"], multilinePrompt);
|
|
assertDryRunPrompt(stdin.json ?? {}, multilinePrompt);
|
|
assertCondition(String(stdin.json?.command || "") === "codex submit --prompt-stdin --queue prompt-contract --dry-run", "stdin command should list flags without echoing prompt", stdin.json ?? {});
|
|
|
|
const tmp = mkdtempSync(join(tmpdir(), "unidesk-code-queue-submit-"));
|
|
const promptFile = join(tmp, "prompt.md");
|
|
const filePrompt = `${multilinePrompt}file prompt tail\n`;
|
|
writeFileSync(promptFile, filePrompt, "utf8");
|
|
try {
|
|
const fromFile = runCli(["codex", "submit", "--prompt-file", promptFile, "--queue", "prompt-contract", "--dry-run"]);
|
|
assertDryRunPrompt(fromFile.json ?? {}, filePrompt);
|
|
assertCondition(String(fromFile.json?.command || "").includes(`--prompt-file ${promptFile}`), "prompt-file command should retain file path for review", fromFile.json ?? {});
|
|
assertCondition(!String(fromFile.json?.command || "").includes("file prompt tail"), "prompt-file command must not echo file prompt text", fromFile.json ?? {});
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
|
|
const positional = runCli(["codex", "submit", "short smoke prompt", "--dry-run"]);
|
|
assertDryRunPrompt(positional.json ?? {}, "short smoke prompt");
|
|
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional submit prompt", positional.json ?? {});
|
|
assertCondition(!String(positional.json?.command || "").includes("short smoke prompt"), "outer command must not echo positional submit prompt", positional.json ?? {});
|
|
|
|
const duplicateSource = runCli(["codex", "submit", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
|
|
assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout });
|
|
const duplicateMessage = String(nestedRecord(duplicateSource.json, ["error"]).message || "");
|
|
assertCondition(duplicateMessage.includes("exactly one prompt source"), "duplicate prompt source error should be explicit", { duplicateMessage });
|
|
|
|
const longSubmittedPrompt = `${multilinePrompt}${"submitted prompt body must not be echoed\n".repeat(80)}`;
|
|
const submitSuccess = compactSubmitSuccessResponseForTest({
|
|
tasks: [{
|
|
id: "codex_submit_success_contract",
|
|
queueId: "prompt-contract",
|
|
status: "queued",
|
|
providerId: "D601",
|
|
model: "gpt-5.5",
|
|
cwd: "/workspace",
|
|
prompt: longSubmittedPrompt,
|
|
maxAttempts: 99,
|
|
createdAt: "2026-05-22T00:00:00.000Z",
|
|
updatedAt: "2026-05-22T00:00:00.000Z",
|
|
}],
|
|
queue: {
|
|
total: 1,
|
|
queueCount: 1,
|
|
counts: { queued: 1 },
|
|
queuedTaskIds: ["codex_submit_success_contract"],
|
|
},
|
|
}, { ok: true, status: 200 }, { mode: "local-atomic-directory-submit-serialization", acquiredAfterMs: 1, heldMs: 2, throttleMs: 2000 });
|
|
const submitSuccessJson = JSON.stringify(submitSuccess);
|
|
const submitted = nestedRecord(submitSuccess, ["submitted"]);
|
|
assertCondition(submitted.accepted === true, "submit success should confirm accepted write", submitSuccess);
|
|
assertCondition((submitted.taskIds as unknown[]).includes("codex_submit_success_contract"), "submit success should expose task id", submitSuccess);
|
|
assertCondition(submitSuccessJson.includes("promptOmitted"), "submit success should explicitly mark prompt omitted", submitSuccess);
|
|
assertCondition(!submitSuccessJson.includes("submitted prompt body must not be echoed"), "submit success must not echo prompt text", submitSuccess);
|
|
assertCondition(!submitSuccessJson.includes("promptPreview"), "submit success must not include promptPreview", submitSuccess);
|
|
|
|
const help = runCli(["codex", "submit", "--help"]);
|
|
assertCondition(help.status === 0 && help.json?.ok === true, "codex submit help should succeed", help.json ?? { stdout: help.stdout });
|
|
const data = nestedRecord(help.json?.data, []);
|
|
const usage = stringArray(data.usage);
|
|
const promptInput = nestedRecord(data, ["promptInput"]);
|
|
const recommended = stringArray(promptInput.recommended);
|
|
const examples = nestedRecord(data, ["examples"]);
|
|
assertCondition(usage.some((line) => line.includes("--prompt-stdin")), "help usage should include --prompt-stdin", { usage });
|
|
assertCondition(usage.some((line) => line.includes("--prompt-file")), "help usage should include --prompt-file", { usage });
|
|
assertCondition(usage.some((line) => line.includes("cat <<'PROMPT'")), "help usage should include a quoted heredoc example", { usage });
|
|
assertCondition(recommended.includes("--prompt-stdin") && recommended.includes("--prompt-file"), "help should recommend stdin and file prompt sources", promptInput);
|
|
assertCondition(String(promptInput.sourceRule || "").includes("Exactly one prompt source"), "help should document exact prompt source rule", promptInput);
|
|
assertCondition(stringArray(examples.stdin).some((line) => line.includes("--prompt-stdin")), "help examples should include stdin command", examples);
|
|
assertCondition(String(examples.file || "").includes("--prompt-file"), "help examples should include file command", examples);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"submit --prompt-stdin preserves multiline quotes and newlines",
|
|
"submit --prompt-file preserves reviewed file contents",
|
|
"submit positional prompt is redacted from the outer command envelope",
|
|
"duplicate submit prompt source fails explicitly",
|
|
"submit success confirms write without echoing prompt",
|
|
"codex submit help documents stdin/file recommendations and copyable examples",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runCodeQueueCliSubmitPromptContract(), null, 2)}\n`);
|
|
}
|