test: add commander no-daemon smoke contract
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createCommanderRequestHandler, type RuntimeConfig } from "../src/components/microservices/host-codex-commander/src/index";
|
||||
import { commanderHealth, summarizeCommanderTrace } from "../src/components/microservices/host-codex-commander/src/state";
|
||||
|
||||
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 asRecordArray(value: unknown, label: string): JsonRecord[] {
|
||||
assertCondition(Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null && !Array.isArray(item)), `${label} must be object array`, 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[], 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),
|
||||
});
|
||||
assertCondition(result.stdout.trim().length > 0, `command produced no stdout: ${args.join(" ")}`);
|
||||
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
|
||||
}
|
||||
|
||||
function dataOf(envelope: JsonRecord): JsonRecord {
|
||||
return asRecord(envelope.data, "data");
|
||||
}
|
||||
|
||||
async function readJson(response: Response): Promise<JsonRecord> {
|
||||
return asRecord(await response.json() as unknown, "response body");
|
||||
}
|
||||
|
||||
const sessionId = `no-daemon-smoke-contract-${process.pid}`;
|
||||
const liveSessionPath = join(process.cwd(), ".state", "commander", "sessions", `${sessionId}.json`);
|
||||
assertCondition(!existsSync(liveSessionPath), "precondition failed: smoke session path should not already exist", liveSessionPath);
|
||||
|
||||
const smoke = dataOf(runCli(["commander", "smoke", "--dry-run", "--session-id", sessionId], 0));
|
||||
assertCondition(smoke.ok === true, "smoke must succeed", smoke);
|
||||
assertCondition(smoke.phase === "source-contract", "smoke must remain source-contract phase", smoke);
|
||||
assertCondition(smoke.mode === "dry-run", "smoke must report dry-run mode", smoke);
|
||||
assertCondition(smoke.mutation === false, "smoke must be non-mutating", smoke);
|
||||
|
||||
const noDaemon = asRecord(smoke.noDaemonSmokeContract, "noDaemonSmokeContract");
|
||||
for (const flag of [
|
||||
"startsDaemon",
|
||||
"startsPtyBridge",
|
||||
"startsStdioBridge",
|
||||
"opensSshBridge",
|
||||
"sendsClaudeqq",
|
||||
"restartsServices",
|
||||
"interruptsTasks",
|
||||
"cancelsTasks",
|
||||
"deploys",
|
||||
"runsFullCheckOrE2e",
|
||||
]) {
|
||||
assertCondition(noDaemon[flag] === false, `${flag} must be false`, noDaemon);
|
||||
}
|
||||
assertCondition(asStringArray(noDaemon.allowedCommands, "allowedCommands").includes("bun scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"), "smoke should name this lightweight contract", noDaemon);
|
||||
|
||||
const validationPlan = asRecordArray(smoke.validationPlan, "validationPlan");
|
||||
const surfaces = validationPlan.map((item) => item.surface);
|
||||
for (const expected of [
|
||||
"health endpoint",
|
||||
"state file",
|
||||
"trace summary dry-run",
|
||||
"approval draft preview",
|
||||
"SSH bridge boundary",
|
||||
]) {
|
||||
assertCondition(surfaces.includes(expected), `missing validation surface ${expected}`, surfaces);
|
||||
}
|
||||
for (const item of validationPlan) {
|
||||
assertCondition(asStringArray(item.expectedEvidence, "expectedEvidence").length > 0, "each validation item must define evidence", item);
|
||||
assertCondition(asStringArray(item.noRuntimeSideEffects, "noRuntimeSideEffects").length > 0, "each validation item must define no-side-effect boundary", item);
|
||||
}
|
||||
|
||||
const smokeWithoutDryRun = dataOf(runCli(["commander", "smoke", "--session-id", sessionId], 1));
|
||||
assertCondition(smokeWithoutDryRun.error === "dry-run-required", "smoke must require --dry-run", smokeWithoutDryRun);
|
||||
assertCondition(!existsSync(liveSessionPath), "smoke CLI must not write live commander state", liveSessionPath);
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-smoke-"));
|
||||
try {
|
||||
const runtime: RuntimeConfig = {
|
||||
rootDir: tmp,
|
||||
host: "127.0.0.1",
|
||||
port: 4261,
|
||||
logFile: join(tmp, "logs", "commander.jsonl"),
|
||||
serviceId: "host-codex-commander",
|
||||
stateRoot: tmp,
|
||||
sessionId,
|
||||
};
|
||||
const health = commanderHealth(runtime, "2026-05-21T00:00:00.000Z");
|
||||
assertCondition(health.ok === true && health.service === "host-codex-commander", "health helper must expose service metadata", health);
|
||||
assertCondition(health.stateRoot === tmp, "health helper must use temp state root", health);
|
||||
|
||||
const handler = createCommanderRequestHandler(runtime);
|
||||
const healthBody = await readJson(await handler(new Request("http://localhost/health")));
|
||||
assertCondition(healthBody.ok === true, "short-lived handler health route must succeed without Bun.serve", healthBody);
|
||||
|
||||
const trace = summarizeCommanderTrace({
|
||||
taskId: "task-smoke",
|
||||
sessionId,
|
||||
traceJsonl: [
|
||||
JSON.stringify({ seq: 1, kind: "message", status: "running", summary: "checking token=ghp_1234567890abcdef" }),
|
||||
JSON.stringify({ seq: 2, kind: "event", status: "attention_required", text: "needs approval" }),
|
||||
].join("\n"),
|
||||
taskSummary: "summary password=secret",
|
||||
});
|
||||
assertCondition(trace.taskId === "task-smoke", "trace summary must preserve task id", trace);
|
||||
assertCondition(trace.sessionId === sessionId, "trace summary must preserve session id", trace);
|
||||
assertCondition(trace.lastSeq === 2, "trace summary must compute last seq", trace);
|
||||
assertCondition(trace.status === "attention_required", "trace summary must derive attention_required status", trace);
|
||||
assertCondition(trace.redactionsApplied >= 2, "trace summary must redact mock secrets", trace);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const approval = dataOf(runCli([
|
||||
"commander",
|
||||
"approval",
|
||||
"request",
|
||||
"--action",
|
||||
"code-queue-task-cancel",
|
||||
"--reason",
|
||||
"token=ghp_1234567890abcdef",
|
||||
"--dry-run",
|
||||
], 0));
|
||||
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
|
||||
assertCondition(claudeqq.mutation === false, "approval preview must not mutate ClaudeQQ", claudeqq);
|
||||
assertCondition(claudeqq.sendImplemented === false, "approval preview must not implement sending", claudeqq);
|
||||
assertCondition(!JSON.stringify(approval).includes("ghp_1234567890abcdef"), "approval preview must redact secret-like reason", approval);
|
||||
|
||||
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
||||
for (const snippet of [
|
||||
"commander smoke --dry-run",
|
||||
"无 daemon smoke contract",
|
||||
"health endpoint",
|
||||
"SSH bridge boundary",
|
||||
]) {
|
||||
assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"commander smoke --dry-run is non-mutating and dry-run required",
|
||||
"no-daemon smoke contract forbids daemon, SSH/PTY/stdio bridge, ClaudeQQ send, restart, interrupt, cancel, deploy, and full e2e",
|
||||
"health endpoint and trace summary are validated through short-lived source-level helpers",
|
||||
"approval draft preview remains sendImplemented=false and redacted",
|
||||
"reference doc describes the dev validation surfaces and no-daemon boundary",
|
||||
],
|
||||
}, null, 2)}\n`);
|
||||
Reference in New Issue
Block a user