fix: split github contracts from script check
This commit is contained in:
@@ -85,7 +85,7 @@ Codex 启动时反复出现 WebSocket reconnect、HTTPS fallback、`websocket cl
|
||||
1. 在 master `~/.codex/` 准备带后缀的上游 profile 文件,例如 `config.toml.<profile>` 和 `auth.json.<profile>`;禁止覆盖默认 `config.toml` / `auth.json`。
|
||||
2. 在 `config/platform-infra/sub2api-codex-pool.yaml` 添加 `profiles.entries` 项,指定 `profile`、`accountName`、`configFile`、`authFile`。
|
||||
3. 如需要,给该项加 `priority`、`capacity`、`loadFactor`、`tempUnschedulable`、`openaiResponsesWebSocketsV2Mode` 或 `upstreamUserAgent`;capacity/loadFactor 的具体数值只写在 YAML。
|
||||
4. 如果新增账号会提高声明 capacity 总和,同步提高 `pool.minOwnerConcurrency`;`codex-pool plan` 会拒绝 owner concurrency 低于总 capacity 的配置。
|
||||
4. 如果新增账号会提高声明 capacity 总和,默认让省略的 `pool.minOwnerConcurrency` 继续按 capacity 总和自动解析;只有 YAML 已经显式写了该 override 时,才同步提高到不低于总 capacity,或删除 override 回到自动解析。
|
||||
5. 跑 `codex-pool plan`,确认 profile 可读、`base_url` 和 API key 来源有效,且 stdout 未泄露完整 key。
|
||||
6. 跑 `codex-pool sync --confirm`。
|
||||
7. 跑 `codex-pool validate`。
|
||||
|
||||
@@ -209,7 +209,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
||||
- `bun scripts/cli.ts help`:输出所有可用命令的 JSON 索引,详细规范见 `docs/reference/cli.md`。
|
||||
- `bun scripts/cli.ts --main-server-ip <ip> <command>`:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、Code Queue 查询与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`。
|
||||
- `bun scripts/cli.ts config show`:校验并展示根目录 `config.json`,配置来源规则见 `docs/reference/config.md`。
|
||||
- `bun scripts/cli.ts check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--recovery-guardrails|--rust]` / `bun scripts/cli.ts check recovery-guardrails`:默认只运行轻量配置和 TypeScript 语法检查;`check recovery-guardrails` 只读低噪声报告 D601 reboot 后 k3s/Code Queue hostPath、`/proc/mounts`、CRI sandbox 和 ContainerCreating 风险;Rust backend-core 检查默认只能在 D601 CI/dev execution 中用 `UNIDESK_D601_RUST_CHECK=1` 开启,backend-core 主 server 上线受控编译例外不改变 `check --rust` guard,规则见 `docs/reference/dev-environment.md` 和 `docs/reference/devops-hygiene.md`。
|
||||
- `bun scripts/cli.ts check [--full|--files|--scripts-typecheck|--gh-contracts|--components|--compose|--logs|--recovery-guardrails|--rust]` / `bun scripts/cli.ts check recovery-guardrails`:默认只运行轻量配置和 TypeScript 语法检查;GitHub issue/PR live contract 必须显式用 `--gh-contracts` 或 `--full` 开启;`check recovery-guardrails` 只读低噪声报告 D601 reboot 后 k3s/Code Queue hostPath、`/proc/mounts`、CRI sandbox 和 ContainerCreating 风险;Rust backend-core 检查默认只能在 D601 CI/dev execution 中用 `UNIDESK_D601_RUST_CHECK=1` 开启,backend-core 主 server 上线受控编译例外不改变 `check --rust` guard,规则见 `docs/reference/dev-environment.md` 和 `docs/reference/devops-hygiene.md`。
|
||||
- `bun scripts/cli.ts server start`:以异步 job 启动 database、backend-core、frontend、provider-gateway、code-queue-mgr 和主 server 用户服务,部署规则见 `docs/reference/deployment.md`。
|
||||
- `bun scripts/cli.ts server status`:查询固定端口、swap 摘要、容器状态、健康检查和访问 URL,包含生产 frontend、dev frontend proxy 和 provider ingress,判定标准见 `docs/reference/deployment.md` 与 `docs/reference/dev-environment.md`。
|
||||
- `bun scripts/cli.ts server swap status|ensure [--path /swapfile] [--size 2GiB] [--dry-run]`:以 JSON 查看或幂等创建主 server swapfile,`ensure` 输出 before/after、动作、持久化状态和 degraded/failed 详情,规则见 `docs/reference/deployment.md`。
|
||||
|
||||
@@ -26,7 +26,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动
|
||||
- 每个 CLI 命名空间必须支持 `help`、`--help` 或 `-h` 并返回 JSON,不得为了打印帮助而访问 runtime 服务、拉起交互会话或执行长时任务。
|
||||
- `--main-server-ip <ip> <command>` 默认通过公网 frontend 登录态调用主 server 的同源 API 代理,不要求计算节点持有主 server SSH key;显式提供 `--main-server-key` 或 `--main-server-transport ssh` 时才使用旧 SSH 传输。远程 frontend 传输下的 `ssh <route> ...` 必须复用同一套结构化 route parser,支持 `D601`、`G14`、host workspace、`D601:win`、`D601:win/c/test`、`D601:k3s` 和 `D601:k3s:<namespace>:<workload>` 这类定位路径;它不向调用容器下发 provider token,也不要求调用容器能解析 backend-core 内网 DNS。
|
||||
- `config show` 读取并校验根目录 `config.json`,不从环境变量、默认值或隐藏文件静默补配置。
|
||||
- `check` 默认只执行轻量配置校验、Bun 版本检查和 Bun Transpiler 语法解析(覆盖 CLI 入口、主要 `scripts/` 模块和核心组件入口,不做类型推导);关键文件存在性、`scripts/` TypeScript 类型检查、`src/components/` TypeScript 类型检查、Docker Compose config、日志轮转策略扫描和 D601 recovery guardrails 默认不启用,分别通过 `--files`、`--scripts-typecheck`、`--components`、`--compose`、`--logs`、`--recovery-guardrails` 开启,或用 `--full` 一次性开启。`check recovery-guardrails` 是同一诊断的低噪声直接入口,报告 malformed `/proc/mounts`、kubelet validation risk、stale CRI sandbox count、Code Queue worktree/symlink、Code Queue/MDTODO hostPath 和 `ContainerCreating` 分类;它不得重启 k3s、删除 CRI sandbox、修改 hostPath、deploy/rollout 或 prune/reset。`--rust` 只允许在 D601 CI/dev execution 中配合 `UNIDESK_D601_RUST_CHECK=1` 使用,长期规则见 `docs/reference/dev-environment.md` 和 `docs/reference/devops-hygiene.md`。
|
||||
- `check` 默认只执行轻量配置校验、Bun 版本检查和 Bun Transpiler 语法解析(覆盖 CLI 入口、主要 `scripts/` 模块和核心组件入口,不做类型推导);关键文件存在性、`scripts/` TypeScript 类型检查、GitHub CLI contract、`src/components/` TypeScript 类型检查、Docker Compose config、日志轮转策略扫描和 D601 recovery guardrails 默认不启用,分别通过 `--files`、`--scripts-typecheck`、`--gh-contracts`、`--components`、`--compose`、`--logs`、`--recovery-guardrails` 开启,或用 `--full` 一次性开启。`--scripts-typecheck` 只表示 scripts TypeScript 与本地脚本合同,不应触发 GitHub issue/PR live contract 这类可能受网络/API 时延影响的检查;需要验证 GitHub CLI 时显式加 `--gh-contracts`,并以该 profile 的结果判定 GitHub CLI 变更。长命令项必须在 stderr 输出 `unidesk.check.progress` JSON lines,stdout 保持最终 JSON 结果,避免 post-task 或人工运行时长时间无可见进度。`check recovery-guardrails` 是同一诊断的低噪声直接入口,报告 malformed `/proc/mounts`、kubelet validation risk、stale CRI sandbox count、Code Queue worktree/symlink、Code Queue/MDTODO hostPath 和 `ContainerCreating` 分类;它不得重启 k3s、删除 CRI sandbox、修改 hostPath、deploy/rollout 或 prune/reset。`--rust` 只允许在 D601 CI/dev execution 中配合 `UNIDESK_D601_RUST_CHECK=1` 使用,长期规则见 `docs/reference/dev-environment.md` 和 `docs/reference/devops-hygiene.md`。
|
||||
- `server start` 创建异步 job,在后台执行 Docker 构建和启动;命令本身只负责返回 job id、日志路径和启动命令。
|
||||
- `server stop` 创建异步 job,在后台停止固定 Compose project 中的全部 UniDesk 服务。
|
||||
- `server status` 查询公开端口、受限宿主端口、内部端口、主机 swap 摘要、Compose 容器、core/frontend/dev-frontend/provider/database 健康检查和访问 URL;D601 Code Queue 使用的 PostgreSQL/OA Event Flow host mapping 必须出现在受限宿主端口而不是无条件公开入口中。低内存主 server 上 `swap.warning` 非空时,先执行 `server swap status` 或 `server swap ensure`。
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { parseCheckOptions, runChecks } from "./src/check";
|
||||
|
||||
const scriptsOnly = parseCheckOptions(["--scripts-typecheck"]);
|
||||
assert.equal(scriptsOnly.scriptsTypecheck, true);
|
||||
assert.equal(scriptsOnly.ghContracts, false);
|
||||
|
||||
const full = parseCheckOptions(["--full"]);
|
||||
assert.equal(full.scriptsTypecheck, true);
|
||||
assert.equal(full.ghContracts, true);
|
||||
|
||||
const explicit = parseCheckOptions(["--gh-contracts"]);
|
||||
assert.equal(explicit.scriptsTypecheck, false);
|
||||
assert.equal(explicit.ghContracts, true);
|
||||
|
||||
const minimalConfig = {
|
||||
project: { name: "contract" },
|
||||
runtime: "contract",
|
||||
} as never;
|
||||
const result = runChecks(minimalConfig, { ...scriptsOnly, scriptsTypecheck: false });
|
||||
const issueGuard = result.items.find((item) => item.name === "gh:issue-guard-contract");
|
||||
const prFiles = result.items.find((item) => item.name === "gh:pr-files-contract");
|
||||
const pr = result.items.find((item) => item.name === "gh:pr-contract");
|
||||
|
||||
for (const item of [issueGuard, prFiles, pr]) {
|
||||
assert.ok(item, "GitHub contract item should be visible in check output");
|
||||
assert.equal(item?.ok, true);
|
||||
assert.equal((item?.detail as { skipped?: boolean }).skipped, true);
|
||||
}
|
||||
|
||||
console.log("check gh contract scope contract ok");
|
||||
@@ -49,6 +49,20 @@ function deterministicSteerId(taskId: string, prompt: string): string {
|
||||
return `steer_${Bun.SHA256.hash(`unidesk-code-queue-steer:v1\0${taskId}\0${prompt}`, "hex").slice(0, 24)}`;
|
||||
}
|
||||
|
||||
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
||||
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
||||
const data = nestedRecord(result.json?.data, []);
|
||||
assertCondition(data.ok === false, `${command} frozen payload should be ok=false`, data);
|
||||
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);
|
||||
assertCondition(data.command === command, `${command} frozen payload should identify the command`, data);
|
||||
const replacement = nestedRecord(data, ["replacement"]);
|
||||
assertCondition(String(replacement.sessionsSteer || "").includes("agentrun v01 sessions steer"), `${command} should point to AgentRun sessions steer`, replacement);
|
||||
const legacy = nestedRecord(data, ["legacy"]);
|
||||
assertCondition(legacy.noDoubleWrite === true, `${command} should document no double-write`, legacy);
|
||||
}
|
||||
|
||||
function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
||||
assertCondition(response.ok === true, "CLI dry-run should succeed", response);
|
||||
const data = nestedRecord(response.data, []);
|
||||
@@ -82,31 +96,29 @@ function assertReason(result: unknown, reason: string, status: number | null): v
|
||||
|
||||
export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
const positional = runCli(["codex", "steer", "codex_test_task", "correct the running task", "--dry-run"]);
|
||||
assertDryRunPrompt(positional.json ?? {}, "correct the running task");
|
||||
assertLegacyFrozenWrite(positional, "codex steer");
|
||||
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional steer prompt", positional.json ?? {});
|
||||
assertCondition(!String(positional.json?.command || "").includes("correct the running task"), "outer command must not echo positional steer prompt", positional.json ?? {});
|
||||
|
||||
const stdin = runCli(["codex", "steer", "codex_test_task", "--prompt-stdin", "--dry-run"], "stdin steer prompt\n");
|
||||
assertDryRunPrompt(stdin.json ?? {}, "stdin steer prompt\n");
|
||||
assertLegacyFrozenWrite(stdin, "codex steer");
|
||||
assertCondition(!stdin.stdout.includes("stdin steer prompt"), "frozen steer must not echo stdin prompt", { stdout: stdin.stdout });
|
||||
|
||||
const promptFile = join(tmpdir(), `unidesk-code-queue-steer-${process.pid}.txt`);
|
||||
writeFileSync(promptFile, "file steer prompt", "utf8");
|
||||
try {
|
||||
const fromFile = runCli(["codex", "steer", "codex_test_task", "--prompt-file", promptFile, "--dry-run"]);
|
||||
assertDryRunPrompt(fromFile.json ?? {}, "file steer prompt");
|
||||
assertLegacyFrozenWrite(fromFile, "codex steer");
|
||||
assertCondition(!fromFile.stdout.includes("file steer prompt"), "frozen steer must not echo file prompt", { stdout: fromFile.stdout });
|
||||
} finally {
|
||||
unlinkSync(promptFile);
|
||||
}
|
||||
|
||||
const duplicateSource = runCli(["codex", "steer", "codex_test_task", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
|
||||
assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout });
|
||||
const duplicateMessage = String(nestedRecord(duplicateSource.json, ["error"]).message || "");
|
||||
assertCondition(duplicateMessage.includes("exactly one prompt source"), "duplicate prompt source error should be explicit", { duplicateMessage });
|
||||
assertLegacyFrozenWrite(duplicateSource, "codex steer");
|
||||
|
||||
const unknownOption = runCli(["codex", "steer", "codex_test_task", "--queue", "default", "prompt", "--dry-run"]);
|
||||
assertCondition(unknownOption.status !== 0, "unknown steer option should fail", unknownOption.json ?? { stdout: unknownOption.stdout });
|
||||
const unknownMessage = String(nestedRecord(unknownOption.json, ["error"]).message || "");
|
||||
assertCondition(unknownMessage.includes("unsupported codex steer option: --queue"), "unknown option error should name option", { unknownMessage });
|
||||
assertLegacyFrozenWrite(unknownOption, "codex steer");
|
||||
|
||||
const help = runCli(["codex", "help"]);
|
||||
assertCondition(help.status === 0 && help.json?.ok === true, "codex help should succeed", help.json ?? { stdout: help.stdout });
|
||||
@@ -118,7 +130,10 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
assertCondition(!("error" in (advertisedConfirm.json ?? {})), "advertised steer-confirm command must not fall through to top-level error", advertisedConfirm.json ?? {});
|
||||
const advertisedDeliveryStatus = String(nestedRecord(advertisedConfirm.json?.data, ["delivery"]).status || "");
|
||||
assertCondition(["confirmed", "pending", "unknown", "not-supported"].includes(advertisedDeliveryStatus), "advertised steer-confirm command should return structured delivery status", { advertisedDeliveryStatus, json: advertisedConfirm.json });
|
||||
assertCondition(!String(JSON.stringify(advertisedConfirm.json)).includes("\"message\":\"not found\""), "advertised steer-confirm command must not return top-level not found", advertisedConfirm.json ?? {});
|
||||
if (advertisedDeliveryStatus === "not-supported") {
|
||||
const diagnostics = nestedRecord(advertisedConfirm.json?.data, ["diagnostics"]);
|
||||
assertCondition(diagnostics.reason === "steer-confirmation-endpoint-not-supported", "unsupported steer-confirm should use structured diagnostics", diagnostics);
|
||||
}
|
||||
|
||||
let dryRunFetchCount = 0;
|
||||
const dryRunDirect = codexSteerTaskForTest("direct_task", ["do not send", "--dry-run"], () => {
|
||||
@@ -501,11 +516,11 @@ export function runCodeQueueCliSteerContract(): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"steer positional dry-run",
|
||||
"steer stdin dry-run",
|
||||
"steer prompt-file dry-run",
|
||||
"duplicate prompt source failure",
|
||||
"unsupported option failure",
|
||||
"legacy steer positional dry-run is frozen",
|
||||
"legacy steer stdin dry-run is frozen",
|
||||
"legacy steer prompt-file dry-run is frozen",
|
||||
"legacy steer duplicate prompt source is frozen",
|
||||
"legacy steer unsupported option is frozen",
|
||||
"codex help lists steer",
|
||||
"advertised steer-confirm CLI command returns structured status",
|
||||
"outer command redacts positional steer prompt",
|
||||
|
||||
@@ -45,6 +45,21 @@ function stringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
||||
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
||||
const data = nestedRecord(result.json?.data, []);
|
||||
assertCondition(data.ok === false, `${command} frozen payload should be ok=false`, data);
|
||||
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);
|
||||
assertCondition(data.command === command, `${command} frozen payload should identify the command`, data);
|
||||
const replacement = nestedRecord(data, ["replacement"]);
|
||||
assertCondition(String(replacement.queueSubmit || "").includes("agentrun v01 queue submit"), `${command} should point to AgentRun queue submit`, replacement);
|
||||
assertCondition(String(replacement.sessionsSteer || "").includes("agentrun v01 sessions steer"), `${command} should point to AgentRun sessions steer`, replacement);
|
||||
const legacy = nestedRecord(data, ["legacy"]);
|
||||
assertCondition(legacy.noDoubleWrite === true, `${command} should document no double-write`, legacy);
|
||||
}
|
||||
|
||||
function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
||||
assertCondition(response.ok === true, "submit dry-run should succeed", response);
|
||||
const data = nestedRecord(response.data, []);
|
||||
@@ -66,8 +81,8 @@ export function runCodeQueueCliSubmitPromptContract(): JsonRecord {
|
||||
].join("\n");
|
||||
|
||||
const stdin = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-contract", "--dry-run"], multilinePrompt);
|
||||
assertDryRunPrompt(stdin.json ?? {}, multilinePrompt);
|
||||
assertCondition(String(stdin.json?.command || "") === "codex submit --prompt-stdin --queue prompt-contract --dry-run", "stdin command should list flags without echoing prompt", stdin.json ?? {});
|
||||
assertLegacyFrozenWrite(stdin, "codex submit");
|
||||
assertCondition(!stdin.stdout.includes(multilinePrompt), "frozen submit must not echo stdin prompt", { stdout: stdin.stdout });
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), "unidesk-code-queue-submit-"));
|
||||
const promptFile = join(tmp, "prompt.md");
|
||||
@@ -75,22 +90,19 @@ export function runCodeQueueCliSubmitPromptContract(): JsonRecord {
|
||||
writeFileSync(promptFile, filePrompt, "utf8");
|
||||
try {
|
||||
const fromFile = runCli(["codex", "submit", "--prompt-file", promptFile, "--queue", "prompt-contract", "--dry-run"]);
|
||||
assertDryRunPrompt(fromFile.json ?? {}, filePrompt);
|
||||
assertCondition(String(fromFile.json?.command || "").includes(`--prompt-file ${promptFile}`), "prompt-file command should retain file path for review", fromFile.json ?? {});
|
||||
assertCondition(!String(fromFile.json?.command || "").includes("file prompt tail"), "prompt-file command must not echo file prompt text", fromFile.json ?? {});
|
||||
assertLegacyFrozenWrite(fromFile, "codex submit");
|
||||
assertCondition(!fromFile.stdout.includes("file prompt tail"), "frozen submit must not echo file prompt text", { stdout: fromFile.stdout });
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const positional = runCli(["codex", "submit", "short smoke prompt", "--dry-run"]);
|
||||
assertDryRunPrompt(positional.json ?? {}, "short smoke prompt");
|
||||
assertLegacyFrozenWrite(positional, "codex submit");
|
||||
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional submit prompt", positional.json ?? {});
|
||||
assertCondition(!String(positional.json?.command || "").includes("short smoke prompt"), "outer command must not echo positional submit prompt", positional.json ?? {});
|
||||
|
||||
const duplicateSource = runCli(["codex", "submit", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
|
||||
assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout });
|
||||
const duplicateMessage = String(nestedRecord(duplicateSource.json, ["error"]).message || "");
|
||||
assertCondition(duplicateMessage.includes("exactly one prompt source"), "duplicate prompt source error should be explicit", { duplicateMessage });
|
||||
assertLegacyFrozenWrite(duplicateSource, "codex submit");
|
||||
|
||||
const longSubmittedPrompt = `${multilinePrompt}${"submitted prompt body must not be echoed\n".repeat(80)}`;
|
||||
const submitSuccess = compactSubmitSuccessResponseForTest({
|
||||
@@ -128,23 +140,23 @@ export function runCodeQueueCliSubmitPromptContract(): JsonRecord {
|
||||
const promptInput = nestedRecord(data, ["promptInput"]);
|
||||
const recommended = stringArray(promptInput.recommended);
|
||||
const examples = nestedRecord(data, ["examples"]);
|
||||
assertCondition(usage.some((line) => line.includes("--prompt-stdin")), "help usage should include --prompt-stdin", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("--prompt-file")), "help usage should include --prompt-file", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("cat <<'PROMPT'")), "help usage should include a quoted heredoc example", { usage });
|
||||
const submitSummary = nestedRecord(data, ["submitSummary"]);
|
||||
assertCondition(usage.some((line) => line.includes("codex submit # frozen legacy write entry")), "help usage should document frozen legacy submit", { usage });
|
||||
assertCondition(recommended.includes("--prompt-stdin") && recommended.includes("--prompt-file"), "help should recommend stdin and file prompt sources", promptInput);
|
||||
assertCondition(String(promptInput.sourceRule || "").includes("Exactly one prompt source"), "help should document exact prompt source rule", promptInput);
|
||||
assertCondition(stringArray(examples.stdin).some((line) => line.includes("--prompt-stdin")), "help examples should include stdin command", examples);
|
||||
assertCondition(String(examples.file || "").includes("--prompt-file"), "help examples should include file command", examples);
|
||||
assertCondition(String(submitSummary.default || "").includes("legacy-code-queue-frozen"), "help submit summary should document frozen reason", submitSummary);
|
||||
assertCondition(String(submitSummary.replacement || "").includes("agentrun v01 queue submit"), "help should point new submissions at AgentRun", submitSummary);
|
||||
assertCondition(String(examples.agentRunSubmit || "").includes("agentrun v01 queue submit"), "help examples should include AgentRun submit", examples);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"submit --prompt-stdin preserves multiline quotes and newlines",
|
||||
"submit --prompt-file preserves reviewed file contents",
|
||||
"legacy submit --prompt-stdin is frozen and does not echo prompt text",
|
||||
"legacy submit --prompt-file is frozen and does not echo prompt text",
|
||||
"submit positional prompt is redacted from the outer command envelope",
|
||||
"duplicate submit prompt source fails explicitly",
|
||||
"legacy submit duplicate prompt sources still fail at the frozen boundary",
|
||||
"submit success confirms write without echoing prompt",
|
||||
"codex submit help documents stdin/file recommendations and copyable examples",
|
||||
"codex submit help documents frozen legacy submit and AgentRun replacement",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,6 +27,17 @@ function stringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||
}
|
||||
|
||||
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
||||
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
||||
const data = nestedRecord(result.json?.data, []);
|
||||
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 = nestedRecord(data, ["replacement"]);
|
||||
assertCondition(String(replacement.queueSubmit || "").includes("agentrun v01 queue submit"), `${command} should point to AgentRun queue submit`, replacement);
|
||||
assertCondition(String(replacement.sessionsSteer || "").includes("agentrun v01 sessions steer"), `${command} should point to AgentRun sessions steer`, replacement);
|
||||
}
|
||||
|
||||
function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; json: JsonRecord | null } {
|
||||
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
@@ -148,15 +159,12 @@ export function runCodeQueuePromptLintContract(): JsonRecord {
|
||||
}
|
||||
|
||||
const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--dry-run"], unclassifiedM3SmokePrompt);
|
||||
assertCondition(submitDryRun.status === 0 && submitDryRun.json?.ok === true, "submit dry-run should still succeed for commander review", submitDryRun.json ?? { stdout: submitDryRun.stdout });
|
||||
const submitPromptLint = nestedRecord(submitDryRun.json?.data, ["promptLint"]);
|
||||
assertCondition(submitPromptLint.dispatchDisposition === "needs-authorization", "submit dry-run should embed prompt lint authorization blocker", submitPromptLint);
|
||||
assertCondition(submitPromptLint.requiredClass === "live-mutating", "submit dry-run lint should require live-mutating", submitPromptLint);
|
||||
assertLegacyFrozenWrite(submitDryRun, "codex submit");
|
||||
assertCondition(!submitDryRun.stdout.includes("res_boxsimu_1"), "frozen submit must not echo prompt text", { stdout: submitDryRun.stdout });
|
||||
|
||||
const steerDryRun = runCli(["codex", "steer", "codex_test_task", "--prompt-stdin", "--dry-run"], unclassifiedM3SmokePrompt);
|
||||
assertCondition(steerDryRun.status === 0 && steerDryRun.json?.ok === true, "steer dry-run should succeed for commander review", steerDryRun.json ?? { stdout: steerDryRun.stdout });
|
||||
const steerPromptLint = nestedRecord(steerDryRun.json?.data, ["promptLint"]);
|
||||
assertCondition(steerPromptLint.dispatchDisposition === "needs-authorization", "steer dry-run should embed prompt lint authorization blocker", steerPromptLint);
|
||||
assertLegacyFrozenWrite(steerDryRun, "codex steer");
|
||||
assertCondition(!steerDryRun.stdout.includes("res_boxsimu_1"), "frozen steer must not echo prompt text", { stdout: steerDryRun.stdout });
|
||||
|
||||
const help = runCli(["codex", "help"]);
|
||||
assertCondition(help.status === 0 && help.json?.ok === true, "codex help should succeed", help.json ?? { stdout: help.stdout });
|
||||
@@ -174,8 +182,8 @@ export function runCodeQueuePromptLintContract(): JsonRecord {
|
||||
"unclassified HWLAB M3 smoke defaults read-only but requires live-mutating authorization",
|
||||
"prompt-lint evidence redacts secret-looking values",
|
||||
"prompt-lint CLI is dry-run, non-mutating, and does not echo full prompt text",
|
||||
"submit --dry-run embeds prompt live-authorization lint",
|
||||
"steer --dry-run embeds prompt live-authorization lint",
|
||||
"legacy submit --dry-run is frozen and points to AgentRun",
|
||||
"legacy steer --dry-run is frozen and points to AgentRun",
|
||||
"codex help documents prompt-lint and authorization classes",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -82,6 +82,20 @@ function deterministicResumeId(taskId: string, prompt: string): string {
|
||||
return `resume_${Bun.SHA256.hash(`unidesk-code-queue-resume:v1\0${taskId}\0${prompt}`, "hex").slice(0, 24)}`;
|
||||
}
|
||||
|
||||
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
||||
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
||||
const data = nestedRecord(result.json?.data, []);
|
||||
assertCondition(data.ok === false, `${command} frozen payload should be ok=false`, data);
|
||||
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);
|
||||
assertCondition(data.command === command, `${command} frozen payload should identify the command`, data);
|
||||
const replacement = nestedRecord(data, ["replacement"]);
|
||||
assertCondition(String(replacement.sessionsSteer || "").includes("agentrun v01 sessions steer"), `${command} should point to AgentRun sessions steer`, replacement);
|
||||
const legacy = nestedRecord(data, ["legacy"]);
|
||||
assertCondition(legacy.noDoubleWrite === true, `${command} should document no double-write`, legacy);
|
||||
}
|
||||
|
||||
function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
||||
assertCondition(response.ok === true, "CLI dry-run should succeed", response);
|
||||
const data = nestedRecord(response.data, []);
|
||||
@@ -100,25 +114,26 @@ function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
|
||||
|
||||
export function runCodeQueueResumeContract(): JsonRecord {
|
||||
const positional = runCli(["codex", "resume", "codex_test_task", "fix the PR conflict", "--dry-run"]);
|
||||
assertDryRunPrompt(positional.json ?? {}, "fix the PR conflict");
|
||||
assertLegacyFrozenWrite(positional, "codex resume");
|
||||
assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional resume prompt", positional.json ?? {});
|
||||
assertCondition(!String(positional.json?.command || "").includes("fix the PR conflict"), "outer command must not echo positional resume prompt", positional.json ?? {});
|
||||
|
||||
const stdin = runCli(["codex", "resume", "codex_test_task", "--prompt-stdin", "--dry-run"], "stdin resume prompt\n");
|
||||
assertDryRunPrompt(stdin.json ?? {}, "stdin resume prompt\n");
|
||||
assertLegacyFrozenWrite(stdin, "codex resume");
|
||||
assertCondition(!stdin.stdout.includes("stdin resume prompt"), "frozen resume must not echo stdin prompt", { stdout: stdin.stdout });
|
||||
|
||||
const promptFile = join(tmpdir(), `unidesk-code-queue-resume-${process.pid}.txt`);
|
||||
writeFileSync(promptFile, "file resume prompt", "utf8");
|
||||
try {
|
||||
const fromFile = runCli(["codex", "resume", "codex_test_task", "--prompt-file", promptFile, "--dry-run"]);
|
||||
assertDryRunPrompt(fromFile.json ?? {}, "file resume prompt");
|
||||
assertLegacyFrozenWrite(fromFile, "codex resume");
|
||||
assertCondition(!fromFile.stdout.includes("file resume prompt"), "frozen resume must not echo file prompt", { stdout: fromFile.stdout });
|
||||
} finally {
|
||||
unlinkSync(promptFile);
|
||||
}
|
||||
|
||||
const duplicateSource = runCli(["codex", "resume", "codex_test_task", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
|
||||
assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout });
|
||||
assertCondition(String(nestedRecord(duplicateSource.json, ["error"]).message || "").includes("exactly one prompt source"), "duplicate prompt source error should be explicit", duplicateSource.json ?? {});
|
||||
assertLegacyFrozenWrite(duplicateSource, "codex resume");
|
||||
|
||||
const help = runCli(["codex", "help"]);
|
||||
const usage = Array.isArray(nestedRecord(help.json?.data, []).usage) ? nestedRecord(help.json?.data, []).usage as unknown[] : [];
|
||||
@@ -291,7 +306,7 @@ export function runCodeQueueResumeContract(): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"resume positional/stdin/file dry-runs",
|
||||
"legacy resume positional/stdin/file dry-runs are frozen",
|
||||
"bounded disclosure and outer command redaction",
|
||||
"non-dry-run sends resumeId and omits prompt from output",
|
||||
"terminal resume accepted with thread reuse metadata",
|
||||
|
||||
@@ -54,6 +54,19 @@ function assertSecretFree(output: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function assertLegacyFrozenWrite(result: { status: number | null; stdout: string; stderr: string; json: JsonRecord | null }, command: string): void {
|
||||
assertCondition(result.status !== 0 && result.json?.ok === false, `${command} should be frozen`, result.json ?? { stdout: result.stdout, stderr: result.stderr });
|
||||
const data = nestedRecord(result.json?.data, []);
|
||||
assertCondition(data.ok === false, `${command} frozen payload should be ok=false`, data);
|
||||
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 = nestedRecord(data, ["replacement"]);
|
||||
assertCondition(String(replacement.queueSubmit || "").includes("agentrun v01 queue submit"), `${command} should point to AgentRun queue submit`, replacement);
|
||||
const legacy = nestedRecord(data, ["legacy"]);
|
||||
assertCondition(legacy.noDoubleWrite === true, `${command} should document no double-write`, legacy);
|
||||
}
|
||||
|
||||
export function runCodeQueueSubmitExecutionModeContract(): JsonRecord {
|
||||
assertCondition(normalizeRequestedCodeExecutionMode("full-access") === "full-access", "shared parser should preserve short requested mode ids");
|
||||
assertCondition(normalizeCodeExecutionMode("full-access") === "default", "shared execution-mode normalizer should keep full-access on effective default");
|
||||
@@ -61,33 +74,12 @@ export function runCodeQueueSubmitExecutionModeContract(): JsonRecord {
|
||||
assertCondition(requestedCodeExecutionModeIsRecognized("default") === true, "shared recognition helper should accept default mode");
|
||||
|
||||
const defaultMode = runCli(["codex", "submit", "execution mode default smoke", "--dry-run"]);
|
||||
assertCondition(defaultMode.status === 0 && defaultMode.json?.ok === true, "default submit dry-run should succeed", defaultMode.json ?? { stdout: defaultMode.stdout, stderr: defaultMode.stderr });
|
||||
assertLegacyFrozenWrite(defaultMode, "codex submit");
|
||||
assertSecretFree(defaultMode.stdout);
|
||||
const defaultData = nestedRecord(defaultMode.json?.data, []);
|
||||
const defaultRequest = nestedRecord(defaultData, ["request"]);
|
||||
const defaultExecutionMode = nestedRecord(defaultData, ["executionMode"]);
|
||||
const defaultPermissions = nestedRecord(defaultData, ["runnerPermissions"]);
|
||||
assertCondition(defaultRequest.executionMode === undefined, "default payload should omit executionMode so service default is authoritative", defaultRequest);
|
||||
assertCondition(defaultExecutionMode.requested === null, "default mode should show no explicit requested mode", defaultExecutionMode);
|
||||
assertCondition(defaultExecutionMode.effective === "default", "default mode should expose effective default", defaultExecutionMode);
|
||||
assertCondition(defaultExecutionMode.normalized === false, "default mode should not be reported as normalized", defaultExecutionMode);
|
||||
assertCondition(defaultExecutionMode.recognized === true, "default mode should be recognized", defaultExecutionMode);
|
||||
assertCondition(defaultPermissions.observed === false && defaultPermissions.perTaskOverrideSupported === false, "dry-run should mark runner permissions unobserved and non per-task", defaultPermissions);
|
||||
|
||||
const fullAccess = runCli(["codex", "submit", "execution mode full access smoke", "--execution-mode", "full-access", "--dry-run"]);
|
||||
assertCondition(fullAccess.status === 0 && fullAccess.json?.ok === true, "full-access submit dry-run should succeed", fullAccess.json ?? { stdout: fullAccess.stdout, stderr: fullAccess.stderr });
|
||||
assertLegacyFrozenWrite(fullAccess, "codex submit");
|
||||
assertSecretFree(fullAccess.stdout);
|
||||
const fullData = nestedRecord(fullAccess.json?.data, []);
|
||||
const fullRequest = nestedRecord(fullData, ["request"]);
|
||||
const fullExecutionMode = nestedRecord(fullData, ["executionMode"]);
|
||||
assertCondition(fullRequest.executionMode === "full-access", "payload should preserve the requested executionMode value for backend visibility", fullRequest);
|
||||
assertCondition(fullExecutionMode.requested === "full-access", "full-access request should be visible", fullExecutionMode);
|
||||
assertCondition(fullExecutionMode.effective === "default", "full-access should normalize to the effective default runtime mode", fullExecutionMode);
|
||||
assertCondition(fullExecutionMode.recognized === false, "full-access should not be treated as a recognized Code Queue execution mode", fullExecutionMode);
|
||||
assertCondition(fullExecutionMode.normalized === true, "full-access should explicitly show normalization", fullExecutionMode);
|
||||
assertCondition(fullExecutionMode.requestedLooksLikeSandbox === true, "full-access should be classified as a sandbox-like request", fullExecutionMode);
|
||||
assertCondition(String(fullExecutionMode.permissionBoundary || "").includes("runnerPermissions.sandbox"), "permission boundary should point at runnerPermissions.sandbox", fullExecutionMode);
|
||||
assertCondition(String(fullExecutionMode.warning || "").includes("not applied"), "full-access warning should say it is not a per-task sandbox override", fullExecutionMode);
|
||||
|
||||
const promptText = "submitted full-access prompt body must stay omitted";
|
||||
const submitted = compactSubmitSuccessResponseForTest({
|
||||
@@ -138,11 +130,11 @@ export function runCodeQueueSubmitExecutionModeContract(): JsonRecord {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"default codex submit dry-run omits executionMode, reports effective default, and marks runner permissions unobserved",
|
||||
"--execution-mode full-access preserves requested mode, reports effective default, and warns that sandbox permissions are service-level",
|
||||
"legacy codex submit dry-run is frozen and points to AgentRun",
|
||||
"legacy --execution-mode full-access submit dry-run is frozen without printing credentials",
|
||||
"real submit summary fixture exposes requested/effective mode plus observed runnerPermissions without prompt echo",
|
||||
"shared execution-mode helpers preserve requested full-access while normalizing effective runtime to default",
|
||||
"execution-mode dry-run output does not print credential assignments",
|
||||
"execution-mode frozen output does not print credential assignments",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ 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 v01 queue submit"), `${command} should point to AgentRun queue submit`, replacement);
|
||||
}
|
||||
|
||||
const completePrompt = `
|
||||
UniDesk#20 #118 / commander prompt boundary lint
|
||||
|
||||
@@ -116,9 +126,8 @@ try {
|
||||
}
|
||||
|
||||
const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-lint-contract", "--dry-run"], incompletePromptWithSecret);
|
||||
assertCondition(submitDryRun.status === 0 && submitDryRun.envelope.ok === true, "codex submit --dry-run should not be gated by commander prompt-lint", submitDryRun.envelope);
|
||||
const submitData = dataOf(submitDryRun.envelope);
|
||||
assertCondition(asRecord(submitData.request, "submit request").prompt !== undefined, "submit dry-run should keep its existing prompt review behavior", submitData);
|
||||
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);
|
||||
@@ -143,7 +152,7 @@ process.stdout.write(`${JSON.stringify({
|
||||
"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 and does not gate codex submit --dry-run",
|
||||
"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`);
|
||||
|
||||
@@ -14,7 +14,7 @@ const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as {
|
||||
minOwnerConcurrency?: number;
|
||||
defaultTempUnschedulable?: {
|
||||
enabled?: boolean;
|
||||
rules?: Array<{ statusCode?: number; keywords?: string[]; durationMinutes?: number }>;
|
||||
rules?: Array<{ statusCode?: number; keywords?: string[]; durationMinutes?: number; description?: string }>;
|
||||
};
|
||||
};
|
||||
profiles?: { entries?: Array<{ profile?: string; accountName?: string; capacity?: number; loadFactor?: number; openaiResponsesWebSocketsV2Mode?: string | null }> };
|
||||
@@ -28,6 +28,8 @@ const defaultPriority = parsed.pool?.defaultAccountPriority ?? 0;
|
||||
const defaultCapacity = parsed.pool?.defaultAccountCapacity ?? 0;
|
||||
const defaultLoadFactor = parsed.pool?.defaultAccountLoadFactor ?? 0;
|
||||
const desiredCapacity = entries.reduce((total, entry) => total + (entry.capacity ?? defaultCapacity), 0);
|
||||
const explicitMinOwnerConcurrency = parsed.pool?.minOwnerConcurrency;
|
||||
const resolvedMinOwnerConcurrency = explicitMinOwnerConcurrency ?? desiredCapacity;
|
||||
const allowedWebSocketModes = new Set(["off", "ctx_pool", "passthrough"]);
|
||||
const wsEnabledEntries = entries.filter((entry) => entry.openaiResponsesWebSocketsV2Mode && entry.openaiResponsesWebSocketsV2Mode !== "off");
|
||||
const localWsEnabled = parsed.localCodex?.supportsWebSockets === true || parsed.localCodex?.responsesWebSocketsV2 === true;
|
||||
@@ -56,7 +58,21 @@ if (localWsEnabled) {
|
||||
} else {
|
||||
assertCondition(wsEnabledEntries.length === 0, "local Codex WebSocket transport disabled means all account WSv2 capability declarations must be off or omitted", { localCodex: parsed.localCodex, wsEnabledEntries });
|
||||
}
|
||||
assertCondition((parsed.pool?.minOwnerConcurrency ?? 0) >= desiredCapacity, "pool owner concurrency must not bottleneck the declared account capacity set", { minOwnerConcurrency: parsed.pool?.minOwnerConcurrency, desiredCapacity });
|
||||
assertCondition(
|
||||
explicitMinOwnerConcurrency === undefined || (Number.isInteger(explicitMinOwnerConcurrency) && explicitMinOwnerConcurrency > 0),
|
||||
"explicit pool owner concurrency override must be a positive integer when declared",
|
||||
{ minOwnerConcurrency: explicitMinOwnerConcurrency },
|
||||
);
|
||||
assertCondition(
|
||||
resolvedMinOwnerConcurrency >= desiredCapacity,
|
||||
"pool owner concurrency must auto-resolve or be configured to cover the declared account capacity set",
|
||||
{
|
||||
minOwnerConcurrency: explicitMinOwnerConcurrency,
|
||||
minOwnerConcurrencySource: explicitMinOwnerConcurrency === undefined ? "auto-capacity-sum" : "yaml",
|
||||
resolvedMinOwnerConcurrency,
|
||||
desiredCapacity,
|
||||
},
|
||||
);
|
||||
if (parsed.pool?.defaultTempUnschedulable?.enabled === true) {
|
||||
assertCondition(rules.length > 0, "enabled temporary unschedulable policy must declare rules", parsed.pool?.defaultTempUnschedulable);
|
||||
assertCondition(rules.every((rule) => Number.isInteger(rule.statusCode) && (rule.statusCode ?? 0) >= 100 && (rule.statusCode ?? 0) <= 599), "temporary unschedulable rules must declare valid HTTP status codes", rules);
|
||||
@@ -116,7 +132,7 @@ console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"routing config is schema-valid without profile-specific test gates",
|
||||
"pool owner concurrency covers the YAML account capacity set",
|
||||
"pool owner concurrency auto-resolves or covers the YAML account capacity set",
|
||||
"profile load factor overrides are YAML-controlled positive integers",
|
||||
"public Caddy response-header timeout is long enough for Codex compact",
|
||||
"optional WebSocket mode overrides use supported values",
|
||||
|
||||
+51
-10
@@ -19,6 +19,7 @@ const syntaxFiles = [
|
||||
"scripts/platform-infra-sub2api-codex-routing-contract-test.ts",
|
||||
"scripts/platform-infra-sub2api-codex-temp-unsched-contract-test.ts",
|
||||
"scripts/platform-infra-sub2api-http-upstream-contract-test.ts",
|
||||
"scripts/check-gh-contract-scope-contract-test.ts",
|
||||
"scripts/src/playwright-cli.ts",
|
||||
"scripts/src/check.ts",
|
||||
"scripts/src/artifact-registry.ts",
|
||||
@@ -86,6 +87,7 @@ export interface CheckOptions {
|
||||
compose: boolean;
|
||||
logs: boolean;
|
||||
recoveryGuardrails: boolean;
|
||||
ghContracts: boolean;
|
||||
rust: boolean;
|
||||
}
|
||||
|
||||
@@ -97,6 +99,7 @@ const defaultCheckOptions: CheckOptions = {
|
||||
compose: false,
|
||||
logs: false,
|
||||
recoveryGuardrails: false,
|
||||
ghContracts: false,
|
||||
rust: false,
|
||||
};
|
||||
|
||||
@@ -105,15 +108,16 @@ export function checkHelp(): Record<string, unknown> {
|
||||
ok: true,
|
||||
command: "check",
|
||||
usage: [
|
||||
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs|--recovery-guardrails|--rust]",
|
||||
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--gh-contracts|--components|--compose|--logs|--recovery-guardrails|--rust]",
|
||||
"bun scripts/cli.ts check recovery-guardrails",
|
||||
],
|
||||
defaultMode: "syntax/config only; Rust is never compiled on the master server by default",
|
||||
options: [
|
||||
{ name: "--syntax-only|--basic", description: "Run only config validation, Bun version and TypeScript syntax transpile." },
|
||||
{ name: "--full", description: "Enable all local checks except Rust compilation." },
|
||||
{ name: "--full", description: "Enable all non-Rust checks, including explicit GitHub CLI contracts." },
|
||||
{ name: "--files", description: "Verify required entrypoint files, including backend-core Cargo files." },
|
||||
{ name: "--scripts-typecheck", description: "Run scripts TypeScript typecheck." },
|
||||
{ name: "--gh-contracts", description: "Run slower GitHub CLI contract tests; intentionally separate from generic scripts typecheck." },
|
||||
{ name: "--components", description: "Run component TypeScript typecheck." },
|
||||
{ name: "--compose", description: "Render Docker Compose config." },
|
||||
{ name: "--logs", description: "Check unified log rotation policy." },
|
||||
@@ -143,10 +147,13 @@ export function parseCheckOptions(args: string[]): CheckOptions {
|
||||
options.compose = true;
|
||||
options.logs = true;
|
||||
options.recoveryGuardrails = true;
|
||||
options.ghContracts = true;
|
||||
} else if (arg === "--files") {
|
||||
options.files = true;
|
||||
} else if (arg === "--scripts-typecheck") {
|
||||
options.scriptsTypecheck = true;
|
||||
} else if (arg === "--gh-contracts") {
|
||||
options.ghContracts = true;
|
||||
} else if (arg === "--components") {
|
||||
options.components = true;
|
||||
} else if (arg === "--compose") {
|
||||
@@ -171,13 +178,29 @@ function fileItem(path: string): CheckItem {
|
||||
return { name: `file:${path}`, ok: existsSync(absolute), detail: absolute };
|
||||
}
|
||||
|
||||
function emitCheckProgress(detail: Record<string, unknown>): void {
|
||||
console.error(JSON.stringify({ event: "unidesk.check.progress", ...detail }));
|
||||
}
|
||||
|
||||
function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env): CheckItem {
|
||||
const startedAt = Date.now();
|
||||
emitCheckProgress({ phase: "started", name, command, timeoutMs });
|
||||
const result = runCommand(command, repoRoot, { timeoutMs, env });
|
||||
const durationMs = Date.now() - startedAt;
|
||||
emitCheckProgress({
|
||||
phase: result.exitCode === 0 ? "succeeded" : "failed",
|
||||
name,
|
||||
durationMs,
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
timedOut: result.timedOut,
|
||||
});
|
||||
return {
|
||||
name,
|
||||
ok: result.exitCode === 0,
|
||||
detail: {
|
||||
command,
|
||||
durationMs,
|
||||
exitCode: result.exitCode,
|
||||
signal: result.signal,
|
||||
timedOut: result.timedOut,
|
||||
@@ -312,7 +335,7 @@ export function runRecoveryGuardrailsCheck(config: UniDeskConfig): ReturnType<ty
|
||||
return compactD601RecoveryGuardrails(runD601RecoveryGuardrails(config));
|
||||
}
|
||||
|
||||
export function runChecks(config: UniDeskConfig, options: CheckOptions = defaultCheckOptions): { ok: boolean; mode: string; options: CheckOptions; items: CheckItem[] } {
|
||||
export function runChecks(config: UniDeskConfig, options: CheckOptions = defaultCheckOptions): { ok: boolean; mode: string; options: CheckOptions; summary: { total: number; passed: number; failed: number; failedItems: string[] }; items: CheckItem[] } {
|
||||
const items: CheckItem[] = [
|
||||
{ name: "config:validated", ok: true, detail: { project: config.project.name, runtime: config.runtime } },
|
||||
commandItem("bun:version", ["bun", "--version"]),
|
||||
@@ -382,6 +405,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/deploy-artifact-matrix-contract-test.ts"),
|
||||
fileItem("scripts/decision-center-desired-state-contract-test.ts"),
|
||||
fileItem("scripts/code-queue-prompt-observation-test.ts"),
|
||||
fileItem("scripts/check-gh-contract-scope-contract-test.ts"),
|
||||
fileItem("scripts/gh-cli-issue-guard-contract-test.ts"),
|
||||
fileItem("scripts/gh-cli-pr-files-contract-test.ts"),
|
||||
fileItem("scripts/gh-cli-pr-contract-test.ts"),
|
||||
@@ -449,9 +473,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("artifact-registry:direct-compose-dry-run-matrix", ["bun", "scripts/artifact-consumer-dry-run-matrix-test.ts"], 30_000));
|
||||
items.push(commandItem("schedule:cli-contract", ["bun", "scripts/schedule-cli-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("server:cleanup-plan-contract", ["bun", "scripts/server-cleanup-plan-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:issue-guard-contract", ["bun", "scripts/gh-cli-issue-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-files-contract", ["bun", "scripts/gh-cli-pr-files-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("check:gh-contract-scope-contract", ["bun", "scripts/check-gh-contract-scope-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("playwright:cli-wrapper-contract", ["bun", "scripts/playwright-cli-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("platform-infra:sub2api-codex-local-config-contract", ["bun", "scripts/platform-infra-sub2api-codex-local-config-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("platform-infra:sub2api-codex-routing-contract", ["bun", "scripts/platform-infra-sub2api-codex-routing-contract-test.ts"], 30_000));
|
||||
@@ -494,9 +516,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("artifact-registry:direct-compose-dry-run-matrix", "main-server direct artifact consumer dry-run matrix is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("schedule:cli-contract", "Schedule CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("server:cleanup-plan-contract", "Server cleanup dry-run contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:issue-guard-contract", "GitHub issue CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-files-contract", "GitHub PR files/stat contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("check:gh-contract-scope-contract", "Check option scope contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("playwright:cli-wrapper-contract", "Playwright wrapper/headless/session contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("auth-broker:p0-contract", "Auth Broker P0 skeleton and CLI adapter contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("d601:recovery-guardrails-contract", "D601 recovery guardrails fixture contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
@@ -507,6 +527,15 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
} else {
|
||||
items.push(skippedItem("logs:unified-hourly-rotation", "policy scan is opt-in", "--logs or --full"));
|
||||
}
|
||||
if (options.ghContracts) {
|
||||
items.push(commandItem("gh:issue-guard-contract", ["bun", "scripts/gh-cli-issue-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-files-contract", ["bun", "scripts/gh-cli-pr-files-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
|
||||
} else {
|
||||
items.push(skippedItem("gh:issue-guard-contract", "GitHub issue CLI contract is opt-in because it can be slower than generic scripts typecheck", "--gh-contracts or --full"));
|
||||
items.push(skippedItem("gh:pr-files-contract", "GitHub PR files/stat contract is opt-in because it can be slower than generic scripts typecheck", "--gh-contracts or --full"));
|
||||
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in because it can be slower than generic scripts typecheck", "--gh-contracts or --full"));
|
||||
}
|
||||
if (options.recoveryGuardrails) {
|
||||
const recovery = runRecoveryGuardrailsCheck(config);
|
||||
items.push({
|
||||
@@ -545,5 +574,17 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
} else {
|
||||
items.push(skippedItem("rust:backend-core", "Rust check/build must run through an approved native k3s CI artifact publication, not on the master server or CD runtime target", "--rust inside native k3s CI with UNIDESK_NATIVE_K3S_RUST_CHECK=1"));
|
||||
}
|
||||
return { ok: items.every((item) => item.ok), mode: options.full ? "full" : "basic", options, items };
|
||||
const failedItems = items.filter((item) => !item.ok).map((item) => item.name);
|
||||
return {
|
||||
ok: failedItems.length === 0,
|
||||
mode: options.full ? "full" : "basic",
|
||||
options,
|
||||
summary: {
|
||||
total: items.length,
|
||||
passed: items.length - failedItems.length,
|
||||
failed: failedItems.length,
|
||||
failedItems,
|
||||
},
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user