import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { commanderApprovalProxyFailureSummary } from "../src/components/microservices/host-codex-commander/src/approval-notification"; type JsonRecord = Record; 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 runCli(args: string[], expectStatus: number, extraEnv: Record = {}): JsonRecord { const result = spawnSync(process.execPath, ["scripts/cli.ts", ...args], { cwd: process.cwd(), encoding: "utf8", env: { ...process.env, ...extraEnv, }, 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"); } function charLength(value: string): number { return Array.from(value).length; } function assertPlainApprovalMessage(message: string): void { assertCondition(charLength(message) <= 200, "approval notification draft must be <=200 chars", { message, chars: charLength(message) }); assertCondition(!/[`*_#[\]|]/u.test(message), "approval notification draft must not contain Markdown syntax", { message }); assertCondition(!message.includes("\n"), "approval notification draft must be one paragraph", { message }); } const approval = dataOf(runCli([ "commander", "approval", "request", "--action", "code-queue-task-interrupt", "--task-id", "stale-active-118", "--reason", "stale-active 恢复需要 interrupt;token=ghp_1234567890abcdef;不要使用 `local` 路径", "--dry-run", ], 0, { PATH: "/usr/bin:/bin", })); assertCondition(approval.ok === true, "approval dry-run must succeed without local powershell", approval); assertCondition(approval.mutation === false, "approval dry-run must be non-mutating", approval); assertCondition(!JSON.stringify(approval).includes("ghp_1234567890abcdef"), "approval dry-run must redact secret-like reason", approval); const claudeqq = asRecord(approval.claudeqq, "claudeqq"); assertCondition(claudeqq.mutation === false, "ClaudeQQ dry-run must not mutate", claudeqq); assertCondition(claudeqq.sendImplemented === false, "commander skeleton must not claim send implementation", claudeqq); assertCondition(claudeqq.dryRunNoClaudeQqSend === true, "dry-run must explicitly report no ClaudeQQ send", claudeqq); const draft = asRecord(claudeqq.notificationDraft, "claudeqq.notificationDraft"); assertCondition(draft.format === "plain-text", "approval draft must be plain text", draft); assertCondition(draft.markdownAllowed === false, "approval draft must forbid Markdown", draft); assertCondition(draft.containsMarkdownSyntax === false, "approval draft must report no Markdown syntax", draft); assertPlainApprovalMessage(String(draft.message)); const path = asRecord(claudeqq.notificationPath, "claudeqq.notificationPath"); assertCondition(path.error === "notification-path-unavailable", "dry-run must expose notification-path-unavailable blocker", path); assertCondition(path.servicePath === "/api/microservices/claudeqq/proxy/api/push/text", "service path must be backend-core ClaudeQQ proxy", path); assertCondition(String(path.backendCoreProxyCommand).includes("bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST"), "backend-core proxy command must be returned", path); assertCondition(!String(path.backendCoreProxyCommand).includes("powershell"), "backend-core proxy command must not use local powershell", path); assertCondition(!String(path.backendCoreProxyCommand).includes(".agents/skills/claudeqq"), "backend-core proxy command must not use local skill path", path); assertCondition(path.timeoutMs === 15000, "proxy path must disclose timeout", path); const failure = commanderApprovalProxyFailureSummary({ ok: false, status: 503, error: "microservice proxy task failed", stderrTail: "token=ghp_1234567890abcdef powershell.exe failed", }); assertCondition(failure.ok === false, "proxy failure summary must be failing", failure); assertCondition(failure.error === "notification-proxy-failed", "proxy failure summary must be structured", failure); assertCondition(failure.degradedReason === "microservice-proxy-failed", "proxy failure degraded reason must be microservice-proxy-failed", failure); assertCondition(asRecord(failure.rollback, "rollback").issueUpdateRolledBack === false, "proxy failure must not imply issue rollback", failure); assertCondition(!JSON.stringify(failure).includes("ghp_1234567890abcdef"), "proxy failure summary must redact secrets", failure); const tmp = mkdtempSync(join(tmpdir(), "unidesk-gh-brief-notify-")); try { const bodyPath = join(tmp, "brief.md"); writeFileSync(bodyPath, [ "# 指挥简报", "", "## 常驻观察与长期建议", "", "- 保持监督。", "", "## 更新 2026-05-23 10:00 北京时间", "", "- 新增高风险审批路径 dry-run 预览。", "", ].join("\n"), "utf8"); const ghDryRun = dataOf(runCli([ "gh", "issue", "edit", "24", "--body-file", bodyPath, "--notify-claudeqq-brief-diff", "--dry-run", ], 0, { UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL: "http://127.0.0.1:9082", })); const notification = asRecord(ghDryRun.commanderBriefNotification, "commanderBriefNotification"); const ghClaudeqq = asRecord(notification.claudeqq, "commanderBriefNotification.claudeqq"); assertCondition(ghClaudeqq.wouldSend === false, "dry-run helper must not allow non-proxy ClaudeQQ path", ghClaudeqq); assertCondition(ghClaudeqq.blockedReason === "notification-path-unavailable", "dry-run helper must structure non-proxy blocker", ghClaudeqq); assertCondition(String(ghClaudeqq.recommendedCommand).includes("microservice proxy claudeqq"), "dry-run helper must recommend repo-owned proxy command", ghClaudeqq); } finally { rmSync(tmp, { recursive: true, force: true }); } const hostDoc = readFileSync("docs/reference/host-codex-commander.md", "utf8"); const supervisionDoc = readFileSync("docs/reference/code-queue-supervision.md", "utf8"); for (const snippet of [ "notification-path-unavailable", "microservice proxy claudeqq /api/push/text", "200 字以内中文纯文本", ]) { assertCondition(hostDoc.includes(snippet), `host commander doc missing snippet: ${snippet}`); } for (const snippet of [ "不超过 200 个中文字符", "不使用 Markdown 语法", "发送失败只记录到 #24 或对应 blocker issue", ]) { assertCondition(supervisionDoc.includes(snippet), `code queue supervision doc missing snippet: ${snippet}`); } process.stdout.write(`${JSON.stringify({ ok: true, checks: [ "commander approval dry-run survives without local powershell or ClaudeQQ skill server", "dry-run generates a <=200 char Chinese plain-text ClaudeQQ draft and does not send", "dry-run exposes notification-path-unavailable plus backend-core microservice proxy command", "microservice proxy failure summary is structured, redacted, and best-effort", "legacy gh issue edit notify dry-run rejects non-proxy ClaudeQQ base URLs", "reference docs state the high-risk ClaudeQQ approval notification path", ], }, null, 2)}\n`);