diff --git a/ops/agent-v2/SHADOW-RUNBOOK.md b/ops/agent-v2/SHADOW-RUNBOOK.md index 3cafdf5c..8528694e 100644 --- a/ops/agent-v2/SHADOW-RUNBOOK.md +++ b/ops/agent-v2/SHADOW-RUNBOOK.md @@ -7,7 +7,7 @@ spec 定案 2 安全轨的运维面:旧管线原地不动,v2 在 `ops/agent-v2/` ```bash # 对拍闸门(罐装世界,零网络,~1min):旧 replay vs v2 checkpoint 任务 node ops/agent-v2/shadow-diff.mjs # 退出 0=全 SAME -# 全量测试套(46 测,含 parity + cron-launchers + E2E + SIGKILL resume + 重试/跨库/exit 码面) +# 全量测试套(52 测/51 绿+1 设计 SKIP,含 parity + cron-launchers + E2E + SIGKILL resume + 重试/跨库/exit 码面 + golden 三面) cd ops/agent-v2 && node --test 'tests/*.test.mjs' ``` @@ -16,8 +16,8 @@ cd ops/agent-v2 && node --test 'tests/*.test.mjs' ## SAME 门槛 - `shadow-diff.mjs` 退出 0(可比工件闭集 + 台账行,归一化窄面见其头注)。 -- 全量测试套 46/46 绿。 -- **golden 全集新旧逐条一致(票 #232,切流硬门槛)**:当前分支覆盖 1 例(discover full faux)+罐装健康截段(shadow-diff);全集(neg/boundary/health_full_faux 等 ~10 例)补齐前不得切流。 +- 全量测试套 52 测 51 绿 + 1 设计 SKIP(smoke 凭证门)。 +- **golden 对拍(#232 已收口,切流硬门槛)**:管线面 2/2 位等价对拍(`pipeline_health_full_faux`+`pipeline_discover_full_faux`,tests/golden-health-parity/golden-discover-parity,归一化窄面同 shadow-diff)+ legacy 判断器 8 例回归钉(golden-judge-regression:判断器例驱动 legacy agent.mjs runRound 面,spec T3 明示不迁 v2——钉的是 v2 依赖世界保持 GOLDENPASS,非管线对拍;切流删旧时随 legacy 面退役)+ smoke 例凭证门(golden-smoke-gated:选层门恒钉,OLD 面双门手跑,NEW=N/A)。 - 连续一个完整月轮周期内:零 infra 信封(v2 侧)、零对拍 DIFF、SIGKILL/断电恢复演练至少一次通过(`tests/resume.test.mjs` + 人工 kill 面板)。 ## 已知差异面(声明,非缺陷) diff --git a/ops/agent-v2/shadow-diff.mjs b/ops/agent-v2/shadow-diff.mjs index 1d7c347b..18447526 100644 --- a/ops/agent-v2/shadow-diff.mjs +++ b/ops/agent-v2/shadow-diff.mjs @@ -97,7 +97,9 @@ async function runSide(cmd, args, env, label) { } // —— 归一化(窄面:时间/时长/tmp 路径;语义字段零归一)—— -function normalizeText(t, base) { +// 导出面(#232):golden 全集对拍测试复用同一归一化(勿 fork;基座=TS/dur_ms/tmp/-rN, +// usage 三计数等 JSON 数值面由调用方按票面窄面在其上叠一层)。 +export function normalizeText(t, base) { return t .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, "") .replace(/"generated_at":\s*""/g, '"generated_at":""') diff --git a/ops/agent-v2/tests/golden-health-parity.test.mjs b/ops/agent-v2/tests/golden-health-parity.test.mjs new file mode 100644 index 00000000..6ace92d2 --- /dev/null +++ b/ops/agent-v2/tests/golden-health-parity.test.mjs @@ -0,0 +1,165 @@ +// golden-health-parity.test.mjs — golden 对拍补齐(票 #232;ops/agent/golden/cases.json +// 健康轮管线例 pipeline_health_full_faux):旧引擎(ops/agent/pipeline.mjs runPipeline, +// in-proc)vs 新任务面(ops/agent-v2 run-health.mjs 全链 PROBE→CLOSE,子进程)逐工件 +// 位等价对拍。与 golden-discover-parity.test.mjs(#227 T3)合成「管线面 golden 2/2」。 +// 世界=f run-golden.mjs runCase 同构(fixture 判例树 + curl/OPS_EVAL_PY stub + 抑制规则 +// fixture,出处:run-golden.mjs writePrecedents/stubCurl/stubPy;用例数据单源=cases.json +// 运行时读,不手抄)。归一化=shadow-diff.mjs normalizeText(导出面,#232 复用勿 fork) +// + 票面窄面叠层(usage 三计数/wall_s:ENVELOPE.md §usage 计量边界合法差,cost 逐字)。 +// 口径裁决(父会话 2026-10-28,A+C):管线面=位等价对拍;判断器 8 例=legacy 面回归钉 +// (golden-judge-regression.test.mjs),不在此文件。 +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFile as execFileCb } from "node:child_process"; +import { promisify } from "node:util"; +import { GOLDEN_SWEEP_ANCHORS, anchorRow } from "../../agent/anchor-mirror.mjs"; // #194 C3-2 单源(run-golden.mjs stubPy 同引) +import { normalizeText } from "../shadow-diff.mjs"; + +const execFile = promisify(execFileCb); +const V2 = path.resolve(import.meta.dirname, ".."); +const CASE = JSON.parse(fs.readFileSync(path.resolve(V2, "..", "agent", "golden", "cases.json"), "utf8")) + .cases.find((c) => c.id === "pipeline_health_full_faux"); +const ROUND = "2026-06-01-0400-health"; // run-golden.mjs roundType 分支同字面(#208 T2:健康例原字面) +const FIXTURES = path.resolve(V2, "..", "agent", "fixtures", "pipeline"); + +// fixture 判例树(run-golden.mjs writePrecedents 逐字副本:A 命中 health+discover 检索词; +// B 在库不命中任何管线例检索词——闭集负例载体,tree 同构保金样世界零漂移) +const PREC_A = "2026-06-01-golden-aggregator.md"; +const PREC_B = "2026-06-02-golden-matrix-nolayer.md"; +const writePrecedents = (dir) => { + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, PREC_A), + `---\nid: 2026-06-01-golden-aggregator\ndate: 2026-06-01\nsubject: fixture — 聚合器候选必须实测排他率\ncontext: >\n 聚合器形状候选,上游映射推断重叠不可信,\n 排他率必须实测(冻结门线外另议)。\nhuman_ruling: >\n 实测排他率 50.0%,推翻 REJECT,立项。\nreward:\n hard_gates:\n exclusive_rate: "50.0%"\n ruling_agreement: overruled\n---\n\n正文检索词:聚合器 排他率 回声 冻结 违例。\n`); + fs.writeFileSync(path.join(dir, PREC_B), + `---\nid: 2026-06-02-golden-matrix-nolayer\ndate: 2026-06-02\nsubject: fixture — 语义矩阵 NO-LAYER 层读数约定\ncontext: >\n truth 层无取样资产时读数记 NO-LAYER,\n 摘要员如实提及不计入异常。\nhuman_ruling: >\n 认可约定。\nreward:\n hard_gates: {}\n ruling_agreement: agreed\n---\n\n正文:语义矩阵 NO-LAYER 层读数约定(零检索词命中样本)。\n`); + fs.writeFileSync(path.join(dir, "_schema.md"), "schema 说明(注入排除面)。\n"); +}; + +// stub 脚本(run-golden.mjs stubCurl/stubPy 同型:CLI 边界替身,工具实现层全真;零网络) +const stubCurl = (rules) => `#!/usr/bin/env bash +url="${'${@: -1}'}" +d=180; s=0 +case "$url" in +${rules.map((r) => ` *${r.match}*) d=${r.days ?? 180}; s=${r.status ?? 0} ;;`).join("\n")} +esac +if (( s >= 400 )); then + printf 'HTTP/1.1 %s stub\\r\\nContent-Length: 0\\r\\n\\r\\n' "$s" + exit 0 +fi +printf 'HTTP/1.1 200 OK\\r\\nLast-Modified: %s\\r\\nContent-Length: 4\\r\\n\\r\\n' \\ + "$(date -u -d "@$(( $(date +%s) - d * 86400 - 600 ))" '+%a, %d %b %Y %H:%M:%S GMT')" +exit 0 +`; +const stubPy = (matrix) => `#!/usr/bin/env bash +SWEEP_OK='${JSON.stringify({ kind: "anchor_sweep", anchors: GOLDEN_SWEEP_ANCHORS, n_total: GOLDEN_SWEEP_ANCHORS.length, n_fail: 0, failures: [], + results: GOLDEN_SWEEP_ANCHORS.map(([ip, expect]) => anchorRow({ ip, expect })) }).replace(/'/g, "'\\''")}' +case " $* " in + *--anchor-sweep*) printf '%s\\n' "$SWEEP_OK" ;; + *) printf '%s\\n' '${JSON.stringify(matrix).replace(/'/g, "'\\''")}' ;; +esac +`; + +// 世界置备(双侧同构:tmp/docs + 判例树 + stub + 抑制规则 fixture[cases.json 单源]) +function seedSide(tag) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `v2-hpar-${tag}-`)); + const docsRoot = path.join(tmp, "docs"); + const ops = path.join(docsRoot, "ops"); + writePrecedents(path.join(ops, "precedents")); + fs.mkdirSync(path.join(ops, "rules"), { recursive: true }); + fs.writeFileSync(path.join(ops, "rules", "suppressions.json"), JSON.stringify(CASE.pipeline.rules, null, 2) + "\n"); + const stubDir = path.join(tmp, "stubs"); + fs.mkdirSync(stubDir, { recursive: true }); + const writeStub = (name, text) => { const p = path.join(stubDir, name); fs.writeFileSync(p, text); fs.chmodSync(p, 0o755); return p; }; + writeStub("curl", stubCurl(CASE.stubs.curl_age_days)); + const py = writeStub("py-matrix", stubPy(CASE.stubs.matrix)); + const env = { + ...process.env, + DOCS_ROOT: docsRoot, PRECEDENTS_DIR: path.join(ops, "precedents"), LEDGER_FILE: path.join(ops, "ledger.md"), + PATH: `${stubDir}:${process.env.PATH}`, OPS_EVAL_PY: py, OPS_SWEEP_DRY_RUN: "1", + }; + return { tmp, docsRoot, ops, env }; +} + +// 可比工件闭集(shadow-diff COMPARABLE 同源 + 01/02/09:全链双侧均在——shadow 载具停笔 +// RETRO 才排除 09,本对拍双侧跑全链到 CLOSE,09-budget 双侧落件)。NEW 侧白名单 +// .v2-claim.json(跨库撞轮认领件,shadow-diff isUndeclaredNewFile 同声明)。 +const COMPARABLE = ["01-", "02-", "03-", "04-", "05-", "06-", "07-", "08-", "09-budget.json", "gates.json"]; + +test("golden 对拍 pipeline_health_full_faux:旧 runPipeline 全链 vs v2 run-health 全链,工件闭集位等价", async () => { + const old = seedSide("old"); + const neu = seedSide("new"); + try { + // —— 旧侧:in-proc runPipeline(run-golden.mjs pipeline 分支同调法;env 先于 import)—— + const saved = { DOCS_ROOT: process.env.DOCS_ROOT, PRECEDENTS_DIR: process.env.PRECEDENTS_DIR, LEDGER_FILE: process.env.LEDGER_FILE, OPS_EVAL_PY: process.env.OPS_EVAL_PY, OPS_SWEEP_DRY_RUN: process.env.OPS_SWEEP_DRY_RUN, PATH: process.env.PATH }; + Object.assign(process.env, old.env); + let oldEnv = null; + try { + const { runPipeline, DEFAULT_FIXTURES_DIR } = await import("../../agent/pipeline.mjs"); + const r = await runPipeline({ type: "health", roundId: ROUND, fixturesDir: DEFAULT_FIXTURES_DIR, + stdout: { write: () => true }, stderr: { write: () => true } }); + oldEnv = r?.envelope ?? null; + } finally { + for (const k of Object.keys(saved)) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + } + assert.ok(oldEnv?.ok, `旧引擎例应跑出 ok 信封:${JSON.stringify(oldEnv)?.slice(0, 300)}`); + + // —— 新侧:子进程 run-health.mjs 全链(PROBE→CLOSE 缺省;faux fixtures 缺省)—— + const { stdout } = await execFile(process.execPath, [path.join(V2, "run-health.mjs"), ROUND, + "--fixtures", FIXTURES, "--db", path.join(neu.tmp, "v2.sqlite")], { env: neu.env, maxBuffer: 32 * 1024 * 1024 }); + const newR = JSON.parse(stdout.trim().split("\n").filter((l) => l.startsWith("{")).pop()); + assert.equal(newR.ok, true, `v2 例应终态 ok(失败面可读):${JSON.stringify(newR.error ?? newR).slice(0, 300)}`); + + // —— 逐工件位等价(归一化窄面:TS/dur_ms/tmp/-rN=shadow-diff 单源;usage 三计数/ + // wall_s=票面合法差叠层;cost 逐字)—— + const base = { oldTmp: old.tmp, newTmp: neu.tmp, roundId: ROUND }; + const norm = (t) => normalizeText(t, base) + .replace(/"(input|output|totalTokens)":\s*\d+/g, '"$1": N') + .replace(/"wall_s":\s*[\d.]+/g, '"wall_s": 0') + .replace(/\d+ 字节/g, "N 字节") // G_CLOSE reason 信封字节数=usage 数位下游(golden-discover-parity 同规则;shadow-diff 停笔 RETRO 不遇此面) + .replace(/(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{1,2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT/g, ""); // stub curl Last-Modified 毫秒漂移(金样自身 600s 余量同设计;语义面=age_days/分桶,逐字比不变) + const dirA = path.join(old.ops, "rounds", ROUND); + const dirB = path.join(neu.ops, "rounds", ROUND); + const filesOf = (d) => fs.existsSync(d) ? fs.readdirSync(d).filter((f) => !fs.statSync(path.join(d, f)).isDirectory()) : []; + const oldFiles = filesOf(dirA).filter((f) => f !== "state.json"); // OLD=磁盘状态机;NEW 无(state 面=checkpoint,shadow-diff 同排除) + const newFiles = filesOf(dirB).filter((f) => f !== "state.json" && f !== ".v2-claim.json"); // NEW 白名单(认领件,shadow-diff 同声明) + const rows = []; + for (const f of [...new Set([...oldFiles, ...newFiles])].sort()) { + const inComparable = COMPARABLE.some((p) => f.startsWith(p)); + const o = oldFiles.includes(f), n = newFiles.includes(f); + if (!inComparable) { rows.push(`UNDECLARED ${f}(闭集外${o && !n ? ",旧侧" : ""}——结构性差异面须显式声明后入集,见 shadow-diff isUndeclaredNewFile 同哨)`); continue; } // 残余哨(P1:SKIP 仅记日志不断言→闭集外新面无哨;与 shadow-diff.mjs:158 同判据) + if (o && !n) { rows.push(`ONLY-OLD ${f}`); } + else if (!o && n) { rows.push(`ONLY-NEW ${f}`); } + else { + const a = norm(fs.readFileSync(path.join(dirA, f), "utf8")); + const b = norm(fs.readFileSync(path.join(dirB, f), "utf8")); + rows.push(`${a === b ? "SAME" : "DIFF"} ${f}`); + if (a !== b) { + const la = a.split("\n"), lb = b.split("\n"); + const i = la.findIndex((l, k) => l !== lb[k]); + rows.push(` L${i + 1}: OLD=${(la[i] ?? "").slice(0, 160)} | NEW=${(lb[i] ?? "").slice(0, 160)}`); + } + } + } + const bad = rows.filter((r) => /^(DIFF|ONLY-|UNDECLARED)/.test(r)); + // 信封相位序列/状态(旧 runPipeline 信封 vs v2;resumed/usage 已知差异面不入) + assert.deepEqual(newR.pipeline.stages.map((s) => [s.name, s.status]), oldEnv.pipeline.stages.map((s) => [s.name, s.status]), "相位序列/状态不等价"); + // 台账行(RETRO 写面;usage 计数归一,cost 逐字——shadow-diff normLedger 同法) + const normLedger = (t) => normalizeText(t, base).replace(/in=\d+ out=\d+ total=\d+/g, "in= out= total="); + const ledA = normLedger(fs.readFileSync(path.join(old.ops, "ledger.md"), "utf8")).trim(); + const ledB = normLedger(fs.readFileSync(path.join(neu.ops, "ledger.md"), "utf8")).trim(); + assert.equal(ledB, ledA, "台账新旧不等价(归一化后)"); + // 双侧可比集非空(空集=闸门失败,shadow-diff P1-1 同判据) + assert.ok(rows.some((r) => r.startsWith("SAME")), `可比工件集为空或全异:\n${rows.join("\n")}`); + assert.deepEqual(bad, [], `golden pipeline_health_full_faux 位等价破口:\n${rows.join("\n")}`); + console.log(`[golden-parity] pipeline_health_full_faux 逐件清单:\n ${rows.join("\n ")}`); + } finally { + fs.rmSync(old.tmp, { recursive: true, force: true }); + fs.rmSync(neu.tmp, { recursive: true, force: true }); + } +}); diff --git a/ops/agent-v2/tests/golden-judge-regression.test.mjs b/ops/agent-v2/tests/golden-judge-regression.test.mjs new file mode 100644 index 00000000..1e3d0674 --- /dev/null +++ b/ops/agent-v2/tests/golden-judge-regression.test.mjs @@ -0,0 +1,44 @@ +// golden-judge-regression.test.mjs — legacy 判断器面回归钉(票 #232;口径裁决 A+C,父会话 +// 2026-10-28)。cases.json 8 个判断器例(health_summary_format/discover_predicate_hit/ +// discover_midround_search_citation/neg1-3/boundary×2)驱动的是 legacy agent.mjs runRound +// 回路(ToolHost+写前语义门+收口四件)——spec T3 明示该面不迁 v2(tools/discovery-tools.mjs +// 头注:fetch_url/gh_search 留 legacy 面),故本文件不是管线对拍,而是「v2 依赖世界 +// (pi-durable@1.4.0 + ops/agent 双 node_modules 共存,scaffold 单实例钉)不破坏 legacy +// 判断器面」的回归钉:在 v2 测试套内子进程起 ops/agent/golden/run-golden.mjs(ops/agent +// 零改动),faux 层全 10 例(8 判断器 + 2 管线,= runner 缺省层)须 GOLDENPASS 全绿。 +// 切流删旧时本钉随 legacy 面退役(SHADOW-RUNBOOK §SAME 门槛行同口径)。 +import test from "node:test"; +import assert from "node:assert/strict"; +import path from "node:path"; +import { execFile as execFileCb } from "node:child_process"; +import { promisify } from "node:util"; +import fs from "node:fs"; + +const execFile = promisify(execFileCb); +const V2 = path.resolve(import.meta.dirname, ".."); +const RUNNER = path.resolve(V2, "..", "agent", "golden", "run-golden.mjs"); +const CASES = JSON.parse(fs.readFileSync(path.resolve(V2, "..", "agent", "golden", "cases.json"), "utf8")).cases; +const JUDGE_IDS = CASES.filter((c) => c.tier === "faux" && c.kind !== "pipeline").map((c) => c.id); // 8 判断器例(裁决口径) + +test("legacy 判断器 8 例回归钉:判断器面断言词汇表外零漂移(run-golden.mjs --case 逐例 GOLDENPASS)", async () => { + // 逐例子进程(= runner 自身隔离形态:DOCS_ROOT/PATH env 每例独立,零网络全 faux); + // 8 例复用 runner 的世界置备/断言评估零复制——本钉只派生与判绿,不 fork 判断逻辑。 + for (const id of JUDGE_IDS) { + let stdout = ""; + try { + ({ stdout } = await execFile(process.execPath, [RUNNER, "--case", id], { timeout: 300_000, maxBuffer: 32 * 1024 * 1024 })); + } catch (e) { // runner 非零退出时 execFile 先拒——捕获后仍走 match,让 GOLDENFAIL 明细面世(P2) + stdout = e.stdout ?? String(e); + } + assert.match(stdout, new RegExp(`^GOLDENPASS ${id}$`, "m"), `判断器例 ${id} 未 GOLDENPASS:\n${stdout.slice(0, 800)}`); + } +}); + +test("管线例判断面回归钉:faux 层全 10 例(8 判断器+2 管线)= runner 缺省层 PASS=10 FAIL=0", async () => { + // 父进程模式逐例派生子进程(runner 缺省=faux 层;与上行互补:钉选层缺省恒 faux, + // 防 smoke 层误进 CI——修正③判例面)。smoke 例零凭证零网络永不入本钉。 + const { stdout: sum } = await (async () => { try { + return await execFile(process.execPath, [RUNNER], { timeout: 600_000, maxBuffer: 32 * 1024 * 1024 }); + } catch (e) { return { stdout: e.stdout ?? String(e) }; } })(); // 同上:非零退出时保 stdout 入断言消息(P2) + assert.match(sum, /^golden: PASS=10 FAIL=0$/m, `faux 层非全绿:\n${sum.slice(-1200)}`); +}); diff --git a/ops/agent-v2/tests/golden-smoke-gated.test.mjs b/ops/agent-v2/tests/golden-smoke-gated.test.mjs new file mode 100644 index 00000000..039117a0 --- /dev/null +++ b/ops/agent-v2/tests/golden-smoke-gated.test.mjs @@ -0,0 +1,55 @@ +// golden-smoke-gated.test.mjs — golden smoke 例 2 例的凭证门面(票 #232;口径裁决 A+C: +// smoke 例驱动 legacy agent.mjs runRound 真模型面,NEW 面 N/A=spec T3 不迁;有凭证时只跑 +// OLD 面)。默认零网络零凭证全绿: +// ① 无凭证面(恒跑,零 API 零网络):runner 选层门——tier=smoke 缺 --smoke 旗标即拒 +// (run-golden.mjs runCase 判例「smoke 层花 API,须 --smoke 显式手跑」);缺省层恒 faux +// (修正③),smoke 永不误进 CI。 +// ② 凭证面(creds ∧ OPS_GOLDEN_SMOKE=1 双门才跑):OLD 面 run-golden.mjs --smoke --case +// 逐例 GOLDENPASS。比 smoke-glm.test.mjs 多一道显式 env 旗标:golden smoke_health= +// 全源真探测 + 整轮真模型(≈50+ URL 真网络,非单次 ¥0.00004 调用),仅凭证在即自动烧 +// 违「默认零网络」——故双门,手跑面与 runner --smoke 语义同构(显式手跑才花 API)。 +// 凭证判定布尔零打印(密钥零入日志/报告,任务书纪律)。 +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFile as execFileCb } from "node:child_process"; +import { promisify } from "node:util"; + +const execFile = promisify(execFileCb); +const V2 = path.resolve(import.meta.dirname, ".."); +const RUNNER = path.resolve(V2, "..", "agent", "golden", "run-golden.mjs"); +const SMOKE_IDS = ["smoke_health", "smoke_discover_threatview_replay"]; + +// 凭证在否(smoke-glm.test.mjs hasZaiCredential 同型:布尔判定不打印值) +function hasZaiCredential() { + const authPath = process.env.OPS_AUTH ?? path.join(os.homedir(), ".pi", "agent", "auth.json"); + try { + const auth = JSON.parse(fs.readFileSync(authPath, "utf8")); + if (auth?.["zai-coding-cn"]?.key) return true; + } catch { /* 无 auth.json → 只看 env */ } + return Boolean(process.env.ZAI_CODING_CN_API_KEY); +} +const OPT_IN = process.env.OPS_GOLDEN_SMOKE === "1"; +const SKIP = !hasZaiCredential() + ? "无 zai 凭证(auth.json/ZAI_CODING_CN_API_KEY 均缺)——SKIP(CI 无凭证判例)" + : !OPT_IN + ? "凭证在但未显式 OPS_GOLDEN_SMOKE=1——SKIP(真 API+真网络整轮,显式手跑面)" + : false; + +test("选层门:tier=smoke 缺 --smoke 旗标即拒(缺省层恒 faux;smoke 永不误进 CI)", async () => { + for (const id of SMOKE_IDS) { + const r = await execFile(process.execPath, [RUNNER, "--case", id], { timeout: 60_000 }) // 拒面=rc 1,拒值捕为断言面 + .then((o) => ({ code: 0, stdout: o.stdout }), (e) => ({ code: e.code, stdout: e.stdout ?? "" })); + assert.equal(r.code, 1, `${id} 应 rc=1(实得 ${r.code})`); + assert.match(r.stdout, new RegExp(`^GOLDENFAIL ${id}: smoke 层花 API,须 --smoke 显式手跑$`, "m"), `选层门文案漂移:\n${r.stdout.slice(0, 400)}`); + } +}); + +test("OLD 面 smoke 2 例(真模型;NEW=N/A spec T3 不迁)", { skip: SKIP }, async () => { + for (const id of SMOKE_IDS) { + const { stdout } = await execFile(process.execPath, [RUNNER, "--case", id, "--smoke"], { timeout: 300_000, maxBuffer: 32 * 1024 * 1024 }); + assert.match(stdout, new RegExp(`^GOLDENPASS ${id}( fixture=.*)?$`, "m"), `smoke 例 ${id} 未过:\n${stdout.slice(0, 800)}`); // 修正④:smoke 保全 fixture 树,行尾带路径 + } +});