154 lines
7.4 KiB
TypeScript
154 lines
7.4 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
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 a string array`, value);
|
|
return value as string[];
|
|
}
|
|
|
|
function runCli(args: string[], expectStatus: number): JsonRecord {
|
|
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
assertCondition(result.status === expectStatus, `status mismatch for ${args.join(" ")}`, {
|
|
status: result.status,
|
|
stdout: result.stdout.slice(-2000),
|
|
stderr: result.stderr.slice(-2000),
|
|
});
|
|
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
|
|
}
|
|
|
|
function dataOf(envelope: JsonRecord): JsonRecord {
|
|
return asRecord(envelope.data, "data");
|
|
}
|
|
|
|
const contract = dataOf(runCli(["commander", "contract"], 0));
|
|
assertCondition(contract.phase === "source-contract", "contract must identify source-contract phase", contract);
|
|
assertCondition(contract.serviceId === "host-codex-commander", "contract must expose service id", contract);
|
|
assertCondition(contract.daemonImplemented === false, "daemon must not be implemented in phase one", contract);
|
|
assertCondition(contract.liveOperationsImplemented === false, "live operations must not be implemented in phase one", contract);
|
|
const capabilities = asStringArray(contract.requiredCapabilities, "requiredCapabilities");
|
|
for (const expected of [
|
|
"host-codex-process-discovery",
|
|
"ssh-bridge-contract",
|
|
"pty-bridge-contract",
|
|
"stdio-bridge-contract",
|
|
"prompt-guidance-plan",
|
|
"trace-summary-plan",
|
|
"issue-20-board-read-write-entry",
|
|
"issue-46-brief-read-write-entry",
|
|
"pr-closeout-boundary-plan",
|
|
"claudeqq-high-risk-approval-entry",
|
|
]) {
|
|
assertCondition(capabilities.includes(expected), `missing required capability ${expected}`, capabilities);
|
|
}
|
|
|
|
const safety = asRecord(contract.safetyBoundary, "safetyBoundary");
|
|
assertCondition(safety.phaseOneMutationAllowed === false, "phase one must forbid mutation", safety);
|
|
const forbidden = asStringArray(safety.forbiddenWithoutExplicitUserApproval, "forbiddenWithoutExplicitUserApproval");
|
|
assertCondition(forbidden.includes("code-queue-backend-restart"), "backend restart must require approval", forbidden);
|
|
assertCondition(forbidden.includes("code-queue-task-interrupt"), "task interrupt must require approval", forbidden);
|
|
assertCondition(forbidden.includes("code-queue-task-cancel"), "task cancel must require approval", forbidden);
|
|
const alwaysForbidden = asStringArray(safety.alwaysForbidden, "alwaysForbidden");
|
|
assertCondition(alwaysForbidden.includes("print-token-values"), "contract must forbid token output", alwaysForbidden);
|
|
|
|
const plan = dataOf(runCli(["commander", "plan", "--dry-run", "--session-id", "primary"], 0));
|
|
assertCondition(plan.mutation === false, "plan must be non-mutating", plan);
|
|
assertCondition(asRecord(asRecord(plan.processDiscovery, "processDiscovery").startPlan, "startPlan").enabled === false, "start plan must be disabled", plan);
|
|
assertCondition(asRecord(plan.bridge, "bridge").mutation === false, "bridge plan must not open bridges", plan);
|
|
assertCondition(asRecord(plan.traceSummary, "traceSummary").mutation === false, "trace summary plan must be non-mutating", plan);
|
|
assertCondition(asRecord(plan.issueEntries, "issueEntries").mutation === false, "issue entry plan must be non-mutating", plan);
|
|
const prCloseout = asRecord(plan.prCloseout, "prCloseout");
|
|
assertCondition(prCloseout.mutation === false, "PR closeout plan must be non-mutating", prCloseout);
|
|
assertCondition(asRecord(prCloseout.runnerBoundary, "runnerBoundary").maySelfCloseOrMergeOrdinaryPrWithinTaskBoundary === true, "ordinary PR runner self-close/merge boundary must be explicit", prCloseout);
|
|
assertCondition(asRecord(prCloseout.unideskCliBoundary, "unideskCliBoundary").mergeSupported === true, "UniDesk REST gh pr merge must be guarded and supported", prCloseout);
|
|
assertCondition(asRecord(plan.claudeqqApproval, "claudeqqApproval").mutation === false, "approval plan must be non-mutating", plan);
|
|
|
|
const planWithoutDryRun = dataOf(runCli(["commander", "plan"], 1));
|
|
assertCondition(planWithoutDryRun.error === "dry-run-required", "plan must require dry-run", planWithoutDryRun);
|
|
|
|
const approval = dataOf(runCli([
|
|
"commander",
|
|
"approval",
|
|
"request",
|
|
"--action",
|
|
"code-queue-task-interrupt",
|
|
"--task-id",
|
|
"task-123",
|
|
"--reason",
|
|
"heartbeat expired",
|
|
"--dry-run",
|
|
], 0));
|
|
assertCondition(approval.mutation === false, "approval request must be non-mutating", approval);
|
|
assertCondition(approval.requiresExplicitUserApproval === true, "approval request must require explicit user approval", approval);
|
|
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
|
|
assertCondition(claudeqq.mutation === false, "ClaudeQQ preview must not send", claudeqq);
|
|
assertCondition(claudeqq.sendImplemented === false, "ClaudeQQ send must not be implemented", claudeqq);
|
|
|
|
const invalidApproval = dataOf(runCli(["commander", "approval", "request", "--action", "read-token-file", "--dry-run"], 1));
|
|
assertCondition(invalidApproval.error === "validation-failed", "unsupported approval action must fail validation", invalidApproval);
|
|
|
|
const secretReasonResult = spawnSync("bun", [
|
|
"scripts/cli.ts",
|
|
"commander",
|
|
"approval",
|
|
"request",
|
|
"--action",
|
|
"code-queue-backend-restart",
|
|
"--reason",
|
|
"token=ghp_1234567890abcdef",
|
|
"--dry-run",
|
|
], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
maxBuffer: 4 * 1024 * 1024,
|
|
});
|
|
assertCondition(secretReasonResult.status === 0, "secret-like approval reason command should still return successfully", {
|
|
stdout: secretReasonResult.stdout,
|
|
stderr: secretReasonResult.stderr,
|
|
});
|
|
assertCondition(!secretReasonResult.stdout.includes("ghp_1234567890abcdef"), "secret-like approval reason must be redacted from stdout", {
|
|
stdout: secretReasonResult.stdout,
|
|
});
|
|
assertCondition(secretReasonResult.stdout.includes("<redacted>"), "redacted approval reason should disclose redaction marker", {
|
|
stdout: secretReasonResult.stdout,
|
|
});
|
|
|
|
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
|
for (const snippet of [
|
|
"本地 skeleton 阶段",
|
|
"/health",
|
|
"/api/commander/contract",
|
|
".state/commander/",
|
|
"trace summary dry-run",
|
|
"approval draft preview",
|
|
"sendImplemented=false",
|
|
]) {
|
|
assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
|
|
}
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"commander contract exposes host Codex service boundary and phase-one no-live-operation flags",
|
|
"dry-run plan covers process discovery, SSH/PTY/stdio bridge, prompt guidance, trace summary, #20/#46 and ClaudeQQ approval",
|
|
"non-dry-run plan is rejected",
|
|
"approval request is dry-run only and rejects unsupported high-risk actions",
|
|
"secret-like approval reasons are redacted from stdout",
|
|
"reference doc states backend restart, task interrupt/cancel, and token-output prohibitions",
|
|
],
|
|
}, null, 2)}\n`);
|