test: add commander no-daemon smoke contract

This commit is contained in:
Codex
2026-05-21 12:51:53 +00:00
parent 67f6d0e820
commit afe50b9e8f
7 changed files with 367 additions and 6 deletions
@@ -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`);
+4
View File
@@ -25,6 +25,7 @@ const syntaxFiles = [
"scripts/src/commander.ts",
"scripts/src/remote.ts",
"scripts/host-codex-commander-contract-test.ts",
"scripts/host-codex-commander-no-daemon-smoke-contract-test.ts",
"scripts/host-codex-commander-skeleton-contract-test.ts",
"src/components/frontend/src/index.ts",
"src/components/frontend/src/app.tsx",
@@ -299,6 +300,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
fileItem("scripts/code-queue-pr-preflight-contract-test.ts"),
fileItem("scripts/code-queue-submit-routing-contract-test.ts"),
fileItem("scripts/host-codex-commander-skeleton-contract-test.ts"),
fileItem("scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"),
fileItem("scripts/provider-runner-triage-contract-test.ts"),
fileItem("scripts/src/provider-triage.ts"),
fileItem("src/components/microservices/code-queue/src/runner-error-classifier.ts"),
@@ -326,6 +328,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(commandItem("code-queue:pr-preflight-contract", ["bun", "scripts/code-queue-pr-preflight-contract-test.ts"], 30_000));
items.push(commandItem("code-queue:submit-routing-contract", ["bun", "scripts/code-queue-submit-routing-contract-test.ts"], 30_000));
items.push(commandItem("host-codex-commander:skeleton-contract", ["bun", "scripts/host-codex-commander-skeleton-contract-test.ts"], 30_000));
items.push(commandItem("host-codex-commander:no-daemon-smoke-contract", ["bun", "scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"], 30_000));
items.push(commandItem("provider:runner-triage-contract", ["bun", "scripts/provider-runner-triage-contract-test.ts"], 30_000));
items.push(commandItem("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 30_000));
items.push(commandItem("decision-center:desired-state-contract", ["bun", "scripts/decision-center-desired-state-contract-test.ts"], 30_000));
@@ -347,6 +350,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:submit-routing-contract", "Code Queue submit routing contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("host-codex-commander:skeleton-contract", "host Codex commander skeleton contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("host-codex-commander:no-daemon-smoke-contract", "host Codex commander no-daemon smoke contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("provider:runner-triage-contract", "Provider runner triage contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("decision-center:desired-state-contract", "Decision Center desired-state drift contract is opt-in with script checks", "--scripts-typecheck or --full"));
+161
View File
@@ -50,6 +50,7 @@ function commanderHelp(): Record<string, unknown> {
usage: [
"bun scripts/cli.ts commander contract",
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
"bun scripts/cli.ts commander smoke --dry-run [--session-id id]",
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
],
highRiskActions,
@@ -211,6 +212,165 @@ function commanderPlan(args: string[]): Record<string, unknown> {
};
}
function healthEndpointValidation(): Record<string, unknown> {
return {
surface: "health endpoint",
endpoint: "GET /health",
validationMethod: "invoke createCommanderRequestHandler with a temporary RuntimeConfig inside a short-lived contract test",
expectedEvidence: [
"body.ok=true",
"body.service=host-codex-commander",
"body.stateRoot points at the temp directory",
"body.currentLogFile is reported",
],
noRuntimeSideEffects: [
"do not run Bun.serve",
"do not publish or expose a port",
"do not restart any service",
],
};
}
function stateFileValidation(sessionId: string): Record<string, unknown> {
return {
surface: "state file",
storageRoot: ".state/commander/",
validationMethod: "write and read a session record only under a temporary directory owned by the contract test",
files: [
`sessions/${sessionId}.json`,
`events/${sessionId}.jsonl`,
"approvals/draft.json",
"logs/commander.jsonl",
],
expectedEvidence: [
"session state round-trips through writeCommanderSession/readCommanderSession",
"secret-like notes are redacted before persistence",
"temporary state root is deleted after the smoke contract",
],
noRuntimeSideEffects: [
"do not touch the live .state/commander directory",
"do not patch database state",
"do not discover or signal live host Codex processes",
],
};
}
function traceSummaryValidation(sessionId: string): Record<string, unknown> {
return {
surface: "trace summary dry-run",
validationMethod: "summarize mock JSONL trace input through summarizeCommanderTrace",
inputPolicy: "bounded mock Code Queue trace JSONL only; no live codex task trace fetch",
expectedEvidence: [
"taskId and sessionId are preserved",
"lastSeq is computed from mock seq fields",
"status is derived without returning raw transcript",
"secret-like text is redacted",
],
sampleSessionId: sessionId,
noRuntimeSideEffects: [
"do not call Code Queue manager",
"do not mark tasks read",
"do not interrupt or cancel tasks",
],
};
}
function approvalDraftValidation(): Record<string, unknown> {
return {
surface: "approval draft preview",
validationMethod: "build a draft preview for one high-risk action with --dry-run",
commandShape: "bun scripts/cli.ts commander approval request --action code-queue-task-interrupt --reason <text> --dry-run",
expectedEvidence: [
"requiresExplicitUserApproval=true",
"claudeqq.mutation=false",
"claudeqq.sendImplemented=false",
"reason and messageTemplate are redacted",
],
noRuntimeSideEffects: [
"do not POST to ClaudeQQ",
"do not notify any QQ user or group",
"do not record an approval as consumed",
],
};
}
function sshBridgeBoundaryValidation(): Record<string, unknown> {
return {
surface: "SSH bridge boundary",
validationMethod: "assert contract-only bridge metadata from commander plan --dry-run",
allowedAtThisStage: [
"readonly contract description",
"future reviewed maintenance command shape",
"explicit approval precondition text",
],
noRuntimeSideEffects: [
"do not open provider SSH session",
"do not open PTY bridge",
"do not open stdio bridge",
"do not inject prompt",
"do not restart services",
"do not interrupt or cancel tasks",
],
expectedEvidence: [
"bridge.mutation=false",
"startPlan.enabled=false",
"safetyBoundary.phaseOneMutationAllowed=false",
],
};
}
function commanderSmoke(args: string[]): Record<string, unknown> {
if (!hasFlag(args, "--dry-run")) {
return {
ok: false,
error: "dry-run-required",
message: requiredDryRunMessage,
command: "bun scripts/cli.ts commander smoke --dry-run",
};
}
const sessionId = optionValue(args, "--session-id") ?? "primary";
return {
ok: true,
phase: "source-contract",
mode: "dry-run",
mutation: false,
serviceId: "host-codex-commander",
noDaemonSmokeContract: {
startsDaemon: false,
startsPtyBridge: false,
startsStdioBridge: false,
opensSshBridge: false,
sendsClaudeqq: false,
restartsServices: false,
interruptsTasks: false,
cancelsTasks: false,
deploys: false,
runsFullCheckOrE2e: false,
allowedCommands: [
"bun scripts/cli.ts commander contract",
"bun scripts/cli.ts commander plan --dry-run",
"bun scripts/cli.ts commander smoke --dry-run",
"bun scripts/cli.ts commander approval request --action <action> --dry-run",
"bun scripts/host-codex-commander-no-daemon-smoke-contract-test.ts",
],
},
validationPlan: [
healthEndpointValidation(),
stateFileValidation(sessionId),
traceSummaryValidation(sessionId),
approvalDraftValidation(),
sshBridgeBoundaryValidation(),
],
manualAuthorizationBeforeLiveRuntime: [
"operator explicitly names the exact live action and target session/task/service",
"current source-contract smoke and skeleton contract tests are green",
"risk review confirms no token output, no direct database patch, and no backend restart bypass",
"ClaudeQQ approval draft is reviewed, sent by an authorized future path, and matched to an explicit approval id",
"rollback and observation steps are written before enabling any daemon or bridge",
],
};
}
function commanderApprovalRequest(args: string[]): Record<string, unknown> {
if (!hasFlag(args, "--dry-run")) {
return {
@@ -276,6 +436,7 @@ export function runCommanderCommand(args: string[]): Record<string, unknown> {
if (sub === undefined || isHelpToken(sub)) return commanderHelp();
if (sub === "contract") return commanderContract();
if (sub === "plan") return commanderPlan(args.slice(1));
if (sub === "smoke") return commanderSmoke(args.slice(1));
if (sub === "approval" && second === "request") return commanderApprovalRequest(args.slice(2));
return {
ok: false,
+4 -3
View File
@@ -44,7 +44,7 @@ export function rootHelp(): unknown {
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
{ command: "commander contract|plan --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
@@ -188,14 +188,15 @@ function providerHelp(): unknown {
function commanderHelp(): unknown {
return {
command: "commander contract|plan|approval",
command: "commander contract|plan|smoke|approval",
output: "json",
usage: [
"bun scripts/cli.ts commander contract",
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
"bun scripts/cli.ts commander smoke --dry-run [--session-id id]",
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
],
description: "Inspect the local host Codex commander skeleton contract, dry-run planner, state helpers, trace summary aggregator, and approval draft preview.",
description: "Inspect the local host Codex commander skeleton contract, dry-run planner, no-daemon smoke validation plan, state helpers, trace summary aggregator, and approval draft preview.",
boundary: [
"the current skeleton is local-only and never attaches to live bridges",
"dry-run commands never open SSH, PTY, or stdio bridges",