Files
pikasTech-unidesk/scripts/host-codex-commander-prompt-lint-contract-test.ts
T
2026-06-11 00:41:20 +00:00

159 lines
9.0 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { lintCommanderPrompt } from "./src/commander-prompt-lint";
type JsonRecord = Record<string, unknown>;
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
function asRecord(value: unknown, label: string): JsonRecord {
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
return value as JsonRecord;
}
function asStringArray(value: unknown, label: string): string[] {
assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be string array`, value);
return value as string[];
}
function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; envelope: JsonRecord } {
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
cwd: process.cwd(),
input: stdin,
encoding: "utf8",
maxBuffer: 4 * 1024 * 1024,
});
assertCondition(String(result.stdout || "").trim().length > 0, `command produced no stdout: ${args.join(" ")}`, {
status: result.status,
stderr: String(result.stderr || ""),
});
return {
status: result.status,
stdout: String(result.stdout || ""),
stderr: String(result.stderr || ""),
envelope: asRecord(JSON.parse(String(result.stdout || "")) as unknown, "cli envelope"),
};
}
function dataOf(envelope: JsonRecord): JsonRecord {
return asRecord(envelope.data, "data");
}
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; envelope: JsonRecord }, command: string): void {
assertCondition(result.status !== 0 && result.envelope.ok === false, `${command} should be frozen`, result.envelope);
const data = dataOf(result.envelope);
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);
const replacement = asRecord(data.replacement, "replacement");
assertCondition(String(replacement.queueSubmit || "").includes("agentrun queue submit"), `${command} should point to AgentRun queue submit`, replacement);
}
const completePrompt = `
UniDesk#20 #118 / commander prompt boundary lint
You are a D601 Code Queue GPT-5.5 runner. Work in UniDesk only.
PR and Git authorization:
- You may create and update a head branch and PR, rebase/update from origin/master, resolve conflicts, and self-merge/close the ordinary PR when checks pass and the task boundary is satisfied.
Artifact authorization:
- You may use repo-owned CI/CD, publish, or equivalent controlled build paths to build/publish DEV images or artifacts, and must report commit, image tag, digest, artifact report, and validation evidence.
Rollout boundary:
- DEV deploy apply, rollout, and live health verification are owned by the host commander unless this prompt explicitly contains ROLLOUT_OK.
- Without explicit ROLLOUT_OK, do not acquire the DEV CD lock, run deploy apply, run rollout restart, or compete with host commander live verification.
Forbidden:
- No PROD mutation, no reading or printing secrets/tokens/credentials, no manual database/DB writes, and no destructive rollback.
`;
const incompletePromptWithSecret = `
UniDesk#20 #118
Task: implement the lint. token=ghp_prompt_lint_contract_secret
Please make code changes and tests.
`;
const lint = lintCommanderPrompt(completePrompt);
assertCondition(lint.ok === true, "complete GPT-5.5 PR prompt should pass lint", lint);
assertCondition(lint.missingClauses.length === 0, "complete prompt should have no missing clauses", lint);
assertCondition(lint.suggestedPatchSnippet === "", "passing prompt should not include patch snippet", lint);
assertCondition(lint.policy.advisoryOnly === true, "lint must be advisory only", lint);
assertCondition(lint.policy.changesCodexSubmitDefault === false, "lint must not change codex submit default", lint);
assertCondition(JSON.stringify(lint).includes("promptShape"), "lint should expose prompt shape metadata", lint);
assertCondition(!JSON.stringify(lint).includes("self-merge/close the ordinary PR"), "direct lint result must not echo full prompt", lint);
const failingLint = lintCommanderPrompt(incompletePromptWithSecret);
assertCondition(failingLint.ok === false, "incomplete prompt should fail lint", failingLint);
assertCondition(failingLint.riskLevel === "high", "incomplete prompt should be high risk", failingLint);
for (const expected of [
"pr-self-merge-rebase-authorization",
"artifact-build-publish-authorization",
"host-owned-dev-rollout",
"runner-rollout-forbidden-without-rollout-ok",
"prod-secret-db-rollback-boundary",
]) {
assertCondition(failingLint.missingClauses.includes(expected), `missing expected clause id ${expected}`, failingLint);
}
assertCondition(failingLint.suggestedPatchSnippet.includes("ROLLOUT_OK"), "snippet should mention ROLLOUT_OK", failingLint);
assertCondition(!JSON.stringify(failingLint).includes("ghp_prompt_lint_contract_secret"), "lint output must not echo secret-like prompt text", failingLint);
const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-prompt-lint-"));
try {
const promptFile = join(tmp, "prompt.md");
writeFileSync(promptFile, incompletePromptWithSecret, "utf8");
const fileRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--prompt-file", promptFile]);
assertCondition(fileRun.status === 0, "commander prompt-lint is advisory and should exit 0 even when lint ok=false", fileRun);
assertCondition(fileRun.envelope.ok === true, "CLI envelope should remain ok for advisory lint result", fileRun.envelope);
const fileData = dataOf(fileRun.envelope);
assertCondition(fileData.ok === false, "lint data ok should reflect missing clauses", fileData);
assertCondition(asStringArray(fileData.missingClauses, "missingClauses").includes("host-owned-dev-rollout"), "file lint should report rollout clause", fileData);
assertCondition(String(fileData.suggestedPatchSnippet || "").includes("DEV deploy apply"), "file lint should include bounded patch snippet", fileData);
assertCondition(!fileRun.stdout.includes("ghp_prompt_lint_contract_secret"), "file lint stdout must not echo prompt secret", fileRun.stdout);
const stdinRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--stdin"], completePrompt);
assertCondition(stdinRun.status === 0 && stdinRun.envelope.ok === true, "stdin lint should exit successfully", stdinRun.envelope);
const stdinData = dataOf(stdinRun.envelope);
assertCondition(stdinData.ok === true, "stdin lint data should pass for complete prompt", stdinData);
assertCondition(asStringArray(stdinData.missingClauses, "missingClauses").length === 0, "stdin lint should have no missing clauses", stdinData);
assertCondition(!stdinRun.stdout.includes("Artifact authorization:"), "stdin lint must not echo full prompt", stdinRun.stdout);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-lint-contract", "--dry-run"], incompletePromptWithSecret);
assertLegacyFrozenWrite(submitDryRun, "codex submit");
assertCondition(!submitDryRun.stdout.includes("ghp_prompt_lint_contract_secret"), "frozen submit must not echo prompt secret", submitDryRun.stdout);
const helpRun = runCli(["commander", "--help"]);
assertCondition(helpRun.status === 0 && helpRun.envelope.ok === true, "commander help should succeed", helpRun.envelope);
const helpData = dataOf(helpRun.envelope);
assertCondition(asStringArray(helpData.usage, "help usage").some((line) => line.includes("commander prompt-lint")), "commander help should list prompt-lint", helpData);
assertCondition(asRecord(helpData.promptLint, "promptLint").gate === "advisory-only; not a business PR gate and not a Code Queue submit admission change", "help should document advisory-only gate", helpData);
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
for (const snippet of [
"commander prompt-lint --kind gpt55-pr",
"missingClauses",
"suggestedPatchSnippet",
"不是业务 PR 门禁",
]) {
assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
}
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"complete GPT-5.5 PR prompt passes commander prompt-lint",
"missing PR/artifact/DEV rollout/ROLLOUT_OK/PROD-secret-DB-rollback clauses are reported as high risk",
"prompt-lint supports --prompt-file and --stdin",
"prompt-lint output does not echo full prompt or secret-like prompt text",
"commander prompt-lint remains advisory while legacy codex submit stays frozen",
"commander help and host commander reference document the advisory lint entry",
],
}, null, 2)}\n`);