// tests/f129-rootradius-silent-gap-probe.test.ts
// -----------------------------------------------------------------------------
// `audit:render-silent-checks`（L4 pre-commit 条件触发 + L5 gitea-pr-checks）的**整脚本**回归面。
//
// 为什么是整脚本而不是 import 判据函数：该闸零导出，判据全写在模块顶层 + 一个 `if (GATE)` 块
// （no `main()`），量具 `pnpm report:gate-regression-face` 此前把它记在「零判据覆盖」。
// ⛔ 闸本体一行没改（凭据 = `shasum -a 256` 注入前后比对 + `git status scripts/` 空）。
//
// 挂载真源（脚本头注释是挂载真源，此处只登记本文件测到哪一层）：
//   L4 = `.husky/pre-commit`，**条件触发** —— staged 命中 manifest / `drift-compare-core.ts`
//        / 闸本体 时才跑。
//   L5 = `.gitea/workflows/pr-checks.yml`（pull_request + push:master 两条）。
//   ⛔ 本文件**不**声明挂载层真的挂上了 —— 那是 `audit-gate-ci-parity` +
//      `audit-gate-mount-declaration` 的判据面（[[INFRA-F138]] 诚实边界 ①）。
//      （这两个名字刻意**不带** `.mjs`、也不带 `audit:` 冒号 —— 见下方 🔴 脱敏那段。）
//
// ✅ 绿档**能**自证非空过，且有**三个**互不相关的 fixture 读数（比前几轮更强）：
//   · `manifest entries            : 287`（真仓库 936）
//   · `report 里出现过 1 个字段名`（真仓库那份 report 有一整族字段）
//   · `本轮判了 1 份 report`（真仓库两条链都有 report ⇒ 2）
//   ⇒ 三个数同时对上，结构上不可能是「闸退回真仓库读」。配反向钉断言 stdout 不含 `: 936`。
//
// 🔴 **本文件登记一条边界（会咬人，先读再改）**：下方 `POPULATION` / `GAP` 是闸里
//   `SILENT_BASELINE.byComponent` 的**逐字副本**（17 个组件的人口数 + 4 个组件的真缺口数）。
//   闸对 baseline 里每个组件都判 G2/G3（实测 < 记录 或 归 0 都红），所以 fixture 的 manifest
//   **必须**把这 17 个组件的人口逐一造齐 —— 这不是「能省的重复」，是判据本身要求的分母。
//   ⇒ 改了闸的 `SILENT_BASELINE` 会让本文件的绿档用例红。**那是设计意图，不是脆弱**：
//   它同时是那张 baseline 表的漂移钉（同 `tests/audit-token-contract.test.ts` 内联 `HEADER`
//   与 `slugifySectionTitle` 的既有范式）。修法 = 照红档印出的 `实测 N > 记录的 M` 同步下方两张表，
//   ⛔ 别改闸、⛔ 别把绿档降级成只断言 exit 0。
//
// 🔴 **脱敏（[[INFRA-F138]] 的「别写真实闸名」纪律，三种形态本文件都体检过）**：
//   ① fixture 假名 —— 下方 `prefix` 用 `silent-gap-fx`，fixture 里零 `.mjs` 文件。
//   ② 散文提闸去后缀 —— 见上方挂载段那两个名字。
//   ③ 断言字符串本身 —— **本闸这一关是干净的**：它 stdout 里唯一提到的别的产物是
//      `pnpm test:render-verification`，而 `test:` 不在量具的闸清单前缀（`audit|check|lint|smoke`）
//      ⇒ 断言它不构成污染。⛔ 但这是**核过**的结论，不是默认安全 —— 换一条闸要重核。
//
// 输入面闭合（每一项都是一条断言，不是省事）：
//   · `linkDirs` 保留默认 `['scripts/lib']` —— 闸 `await import('./lib/render-inputs-fingerprint.mjs')`
//     算 G5 的指纹。那批 lib 不自己算 REPO_ROOT，软链安全（harness 判据 1）。
//   · `copyFiles` 拷两份 `.ts` —— 闸**顶层**（不是 `--gate` 分支内）就 `await import` 了
//     `drift-compare-core.ts`，它又 import 无扩展名的 `./render-excused-fields`（闸自挂的
//     resolve hook 补 `.ts`）。少拷任一份 ⇒ 顶层 ERR_MODULE_NOT_FOUND，`--gate` 根本进不去。
//     ⛔ 必须**拷**不能软链：ESM 走 realpath 会把它们的 root 解回真仓库
//     （理由全文在 `tests/lib/gate-fixture-root.ts` 的 `copyFiles` 注释）。
//   · fixture 必须有 `src/`（哪怕一个文件）—— 指纹的输入面是 `RENDER_INPUT_PATHS`
//     = manifest + `src/`，缺了它 `computeRenderInputsFingerprint` 直接 throw（fail closed）。
//
// ⚠️ 指纹**只 import 活源那份、不在测试里重算一份**（与闸同源）—— 闸头注释逐字警告过
//   「写方与验方算法有一点不同 = 判据恒红或恒绿」。测试站在**写方**位置（造 report 里的指纹），
//   闸站在验方位置，两边同一份实现才是对的。
//
// 覆盖：G0 分母 fail closed · S1 归因自证四态（含 renderTarget 装置失败单列的阴性对照）
//       · G1 两档（blocking 红 / report 只印）· G2 / G3 / G4 · G5 三态（旧格式 / algo 换代 /
//       STALE 不红且不门控人口与字段面）· G6 / G7 / G8（含 G8 先于 G7 的顺序钉）· G9 白名单外
//       fail closed · 退出码与流向 · 「无 fallback 回真仓库」凭据。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterEach } from 'vitest'
import {
  createGateFixture,
  writeFixtureFile,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
} from './lib/gate-fixture-root'
import { computeRenderInputsFingerprint } from '../scripts/lib/render-inputs-fingerprint.mjs'

const GATE = 'scripts/f129-rootradius-silent-gap-probe.mjs'
const ARGS = ['--gate']
const MANIFEST = 'figma-data/render-verification-manifest.json'
const REPORT = 'figma-data/normalized/render-verification.report.json'
const REACT_REPORT = 'figma-data/normalized/react-render-verification.report.json'

/**
 * 闸里 `SILENT_BASELINE.byComponent` 的**人口**列逐字副本（见文件头 🔴 边界段）。
 * 前 4 个是 `growth: 'blocking'`，其余 13 个是 `growth: 'report'`。
 */
const POPULATION: Record<string, number> = {
  Switch: 14,
  Tooltip: 12,
  Slider: 4,
  Button: 4,
  DropDownListSelect: 82,
  FormItem: 64,
  StepItem: 18,
  Progress: 16,
  CheckBox: 12,
  Table: 12,
  Rating: 10,
  BreadcrumbItem: 8,
  Radio: 8,
  TabItem: 8,
  TopBar: 4,
  TabList: 4,
  Tab: 4,
}

/** 同一张表的**真缺口**列（其余 13 个组件的 `silentGap` 都是 0）。 */
// ⚠️ Slider 2026-08-28 由 4 → 0（[[INFRA-F106]] 对照物修正后 G7 棘轮当场要求缩）。
// 成因不是「圆角修好了」而是 rootRadius 换了被测对象，逐字理由在闸的 baseline 那行原地。
const GAP: Record<string, number> = { Switch: 14, Tooltip: 12, Slider: 0, Button: 4 }

const TOTAL_NULL = Object.values(POPULATION).reduce((a, b) => a + b, 0) // 284
const NON_NULL = 3
const TOTAL_ENTRIES = TOTAL_NULL + NON_NULL // 287 —— fixture 自己的数，真仓库是 936

type ManifestEntry = {
  manifestId: string
  codeComponent: string
  expectedFromFigma?: { rootRadius: number | null } | null
}
type Check = Record<string, unknown> & { field: string }
type ReportEntry = { manifestId: string; checks: Check[] }

const idOf = (comp: string, i: number) => `${comp.toLowerCase()}--fx-variant-${i}`

/** 按 `POPULATION` 造齐 284 条 `rootRadius === null` 的 entry + 3 条有值的对照。 */
function baseManifest(pop: Record<string, number> = POPULATION): ManifestEntry[] {
  const out: ManifestEntry[] = []
  for (const [comp, n] of Object.entries(pop)) {
    for (let i = 0; i < n; i++) {
      out.push({ manifestId: idOf(comp, i), codeComponent: comp, expectedFromFigma: { rootRadius: null } })
    }
  }
  // 有值的对照 —— `codeComponent` 刻意用 baseline 外的名字，证明 G4 只看 null 那批
  for (let i = 0; i < NON_NULL; i++) {
    out.push({ manifestId: `fxbadge--fx-variant-${i}`, codeComponent: 'FxBadge', expectedFromFigma: { rootRadius: 4 } })
  }
  return out
}

const unverifiableRow = (actual: number): Check => ({
  field: 'rootRadius',
  actual,
  expected: null,
  pass: true,
  status: 'unverifiable',
  reason: 'Figma exposes no value for this field; nothing to compare. NOT a pass — see UNVERIFIABLE_STATUS.',
})

const measuredRow = (): Check => ({ field: 'rootRadius', actual: 4, expected: 4, pass: true, status: 'pass' })

/**
 * 由 manifest 派生一份「全对上」的 report entries。
 * `GAP` 里的组件每条 `actual = 4`（⇒ 真缺口 = 人口）；其余 `actual = 0`（⇒ 真缺口 0）。
 */
function baseReportEntries(manifest: ManifestEntry[], gap: Record<string, number> = GAP): ReportEntry[] {
  return manifest.map((e) => ({
    manifestId: e.manifestId,
    checks: [
      e.expectedFromFigma?.rootRadius === null ? unverifiableRow(gap[e.codeComponent] ? 4 : 0) : measuredRow(),
    ],
  }))
}

type BuildOpts = {
  manifest?: ManifestEntry[] | string
  entries?: ReportEntry[]
  /** 直接接管整份 report 对象（测 G5 的指纹形态时用）。 */
  report?: (fingerprint: { algo: string; value: string; files: number }) => unknown
  /** 同内容的第二份 report（React 链），不传就不建。 */
  withReactReport?: boolean
  /** 不写 report 文件。 */
  dropReport?: boolean
}

/**
 * 建 fixture。**顺序有意义**：manifest 与 `src/` 先落盘 → 算指纹 → 再写 report。
 * 指纹的输入面含 manifest 内容，先写 report 会钉住一个还没定型的值。
 */
function build(opts: BuildOpts = {}): string {
  const manifest = opts.manifest ?? baseManifest()
  const root = createGateFixture({
    gate: GATE,
    prefix: 'silent-gap-fx',
    files: {
      [MANIFEST]: typeof manifest === 'string' ? manifest : `${JSON.stringify(manifest, null, 1)}\n`,
      // 指纹输入面的第二项 —— 内容无所谓，存在性才是判据（缺了 ⇒ fail closed throw）
      'src/fx-marker.css': ':root { --fx-marker: 1px; }\n',
    },
    copyFiles: [
      'tests/visual-verify/lib/drift-compare-core.ts',
      'tests/visual-verify/lib/render-excused-fields.ts',
    ],
  })

  if (opts.dropReport) return root

  const fingerprint = computeRenderInputsFingerprint(root)
  const entries = opts.entries ?? baseReportEntries(Array.isArray(manifest) ? manifest : baseManifest())
  const report = opts.report
    ? opts.report(fingerprint)
    : { checkedAt: '2026-08-26T00:00:00.000Z', inputsFingerprint: fingerprint, entries }
  const body = `${JSON.stringify(report, null, 1)}\n`
  writeFixtureFile(root, REPORT, body)
  if (opts.withReactReport) writeFixtureFile(root, REACT_REPORT, body)
  return root
}

const run = (root: string) => runGate(root, GATE, ARGS)

const PASS = '✅ audit:render-silent-checks PASS'
const FAIL = '❌ audit:render-silent-checks FAIL'

afterEach(cleanupGateFixtures)

describe('audit:render-silent-checks — 绿档（三个互不相关的 fixture 读数 = 非空过凭据）', () => {
  it('全对上 ⇒ exit 0，且自印的三个数都是 fixture 算出来的（287 / 1 字段 / 1 份 report）', () => {
    const r = run(build())
    expectGateGreen(r, {
      contains: [
        PASS,
        `manifest entries            : ${TOTAL_ENTRIES}`,
        `  rootRadius expected = null: ${TOTAL_NULL}`,
        `  rootRadius expected 有值  : ${NON_NULL}`,
        'report 里出现过 1 个字段名',
        '本轮判了 1 份 report',
      ],
    })
    // 反向钉：真仓库的读数绝不能出现 —— 出现就说明闸退回真仓库读了
    expect(r.stdout).not.toContain('manifest entries            : 936')
    expect(r.stdout).not.toContain(': 652')
  })

  it('绿档印 S1 归因自证 + 涉及组件数（17 = fixture 的 baseline 组件数）', () => {
    const r = run(build())
    expectGateGreen(r, {
      contains: [
        'S1 归因自证 ✅（0 反例）',
        'expected===null ⇔ 行在且 status==="unverifiable"',
        `涉及组件: ${Object.keys(POPULATION).length}`,
        `rootRadius 行 present=${TOTAL_ENTRIES} absent=0 · 其中 unverifiable=${TOTAL_NULL}`,
      ],
    })
  })

  it('只有一份 report ⇒ 另一份印「不是本闸的分母」并跳过，PASS 行如实说「1 份」', () => {
    const r = run(build())
    expectGateGreen(r, {
      contains: ['不存在或读不了，跳过 —— 它不是本闸的分母', '**真缺口**在 1 份 report 上也没有变'],
    })
  })

  it('两份 report 都在 ⇒ 两半都判 2 份（证明第二条链不是被静默忽略）', () => {
    const r = run(build({ withReactReport: true }))
    expectGateGreen(r, {
      contains: ['**真缺口**在 2 份 report 上也没有变', '本轮判了 2 份 report'],
    })
    expect(r.stdout).not.toContain('不是本闸的分母')
  })

  it('绿档逐组件印「现值/baseline」两个数 —— blocking 档中 silentGap>0 的组件都点名', () => {
    const r = run(build())
    // ⚠️ 2026-08-28 起只剩三个被点名：闸的自印**只列 `silentGap > 0` 的组件**，
    // 而 Slider 已缩到 0（[[INFRA-F106]] 对照物修正的后果）⇒ 它从这行消失是预期的，
    // 不是漏印。⛔ 别据此以为 blocking 档只有三个组件 —— Slider 仍在 baseline 里，
    // 由 G6 盯着「0 → n」的增长。
    expectGateGreen(r, { contains: ['真缺口 Switch=14/14 Tooltip=12/12 Button=4/4'] })
  })
})

describe('audit:render-silent-checks — G0 分母 fail closed', () => {
  it('manifest 是空数组 ⇒ G0 红，拒绝空过', () => {
    const r = run(build({ manifest: [], entries: [] }))
    expectGateRed(r, { checks: ['G0 FAIL', '分母为 0，闸拒绝空过（fail closed）'] })
    expect(r.stdout).not.toContain(PASS)
  })

  it('manifest 不是数组（对象）⇒ 同样 G0 红', () => {
    const r = run(build({ manifest: '{"entries": []}\n', entries: [] }))
    expectGateRed(r, { checks: ['G0 FAIL', '解析不出任何 entry'] })
  })

  it('manifest 缺失 ⇒ 非零退出，且 ENOENT 路径指向 **fixture**（无 fallback 回真仓库的凭据）', () => {
    const root = createGateFixture({
      gate: GATE,
      prefix: 'silent-gap-fx',
      files: { 'src/fx-marker.css': ':root{}\n' },
      copyFiles: [
        'tests/visual-verify/lib/drift-compare-core.ts',
        'tests/visual-verify/lib/render-excused-fields.ts',
      ],
    })
    const r = run(root)
    expect(r.status).not.toBe(0)
    expect(r.stderr).toContain('ENOENT')
    expect(r.stderr).toContain(MANIFEST)
    expect(r.stderr).toContain(root)
    expect(r.stdout).not.toContain(PASS)
  })

  it('report 缺失 ⇒ 非零退出（`report` 是顶层就读的，不在 try 里）', () => {
    const r = run(build({ dropReport: true }))
    expect(r.status).not.toBe(0)
    expect(r.stdout).not.toContain(PASS)
  })
})

describe('audit:render-silent-checks — S1 归因自证（新不变式，四态）', () => {
  it('expected===null 却拿到 `pass` 行 ⇒ S1 红，点名 want=unverifiable got=measured', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks = [measuredRow()]
    const r = run(build({ manifest, entries }))
    expectGateRed(r, {
      checks: ['S1 FAIL', 'want=unverifiable got=measured', idOf('Switch', 0)],
    })
    expect(r.stdout).not.toContain(PASS)
  })

  it('expected 有值却拿到 `unverifiable` 行 ⇒ S1 红，方向相反（want=measured got=unverifiable）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[entries.length - 1].checks = [unverifiableRow(0)]
    const r = run(build({ manifest, entries }))
    expectGateRed(r, { checks: ['S1 FAIL', 'want=measured got=unverifiable'] })
  })

  it('rootRadius 行整条缺失 ⇒ got=absent ⇒ 红（旧不变式看不见状态，新版连行都钉）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks = []
    const r = run(build({ manifest, entries }))
    expectGateRed(r, { checks: ['S1 FAIL', 'got=absent'] })
  })

  it('未知 status 值 ⇒ 归到 `unknown-status:` 而不是被当 measured 放行', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks = [{ ...unverifiableRow(0), status: 'skipped-by-fx' }]
    const r = run(build({ manifest, entries }))
    expectGateRed(r, { checks: ['S1 FAIL', 'got=unknown-status:skipped-by-fx'] })
  })

  it('entry 缺 `expectedFromFigma` ⇒ S1 红，理由是「判据的分母算不出来」', () => {
    const manifest = baseManifest()
    delete manifest[0].expectedFromFigma
    const entries = baseReportEntries(manifest)
    entries[0].checks = []
    const r = run(build({ manifest, entries }))
    expectGateRed(r, { checks: ['S1 FAIL', '没有 expectedFromFigma', '判据的分母算不出来'] })
  })

  it('装置失败（有 `renderTarget` 行）⇒ **单列不判反例**，闸仍绿 —— 阴性对照证明上面几条不是空过', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    // 挑一个 `silentGap: 0` 的 report 档组件：若不单列，它没有 rootRadius 行 ⇒ 会被上面
    // 那条 `got=absent` 判红。⛔ 别挑 blocking 那 4 个 —— 它们 `silentGap > 0`，
    // 少一行会先撞 G8（覆盖缩水），测到的就不是 S1 这一档了。
    entries.find((e) => e.manifestId === idOf('FormItem', 0))!.checks = [
      { field: 'renderTarget', pass: false, status: 'fail' },
    ]
    const r = run(build({ manifest, entries }))
    expectGateGreen(r, { contains: ['装置失败(renderTarget)=1 单列不计'] })
  })

  it('manifest 有、report 里没有的 id ⇒ 单列 `不在该 report 里`，不判反例', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest).slice(1)
    const r = run(build({ manifest, entries }))
    // 少的那条属 Switch（有 silentGap）⇒ G8 会红，但 S1 不该把它算反例
    expect(`${r.stdout}${r.stderr}`).toContain('1 个 manifest id 不在该 report 里')
    expect(`${r.stdout}${r.stderr}`).not.toContain('S1 FAIL')
  })
})

describe('audit:render-silent-checks — 人口棘轮 G1-G4（读 manifest）', () => {
  it('G1 blocking 档组件人口 +1 ⇒ 红，并指路「跑一次完整 probe 分类新增那几条」', () => {
    const r = run(build({ manifest: baseManifest({ ...POPULATION, Switch: 15 }) }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G1 Switch：实测 15 > 记录的 14', '新增了 1 个静默 rootRadius check', '跑一次完整 probe'],
    })
  })

  it('G1 report 档组件人口 +1 ⇒ **只印不拦**（exit 0），且自印「不放宽真缺口」', () => {
    const manifest = baseManifest({ ...POPULATION, FormItem: 65 })
    const r = run(build({ manifest }))
    expectGateGreen(r, {
      contains: ['G1(report) FormItem：实测 65 > 记录的 64', 'report 档只放宽人口，不放宽真缺口'],
    })
  })

  it('G2 人口缩小 ⇒ 红，要求把 count 改小（shrink-only，由闸自己宣布缩到哪了）', () => {
    const manifest = baseManifest({ ...POPULATION, Tooltip: 11 })
    const r = run(build({ manifest }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G2 Tooltip：实测 11 < 记录的 12', '请把 count 改成 11', 'shrink-only'],
    })
  })

  it('G3 组件在 manifest 里消失 ⇒ 红，要求删行（表空 = 终态，不是待办）', () => {
    const { TopBar, ...rest } = POPULATION
    void TopBar
    const r = run(build({ manifest: baseManifest(rest) }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G3 TopBar：实测 0', '请删掉这一行（表空 = 终态）'],
    })
  })

  it('G4 baseline 外的新组件带着静默 check 进来 ⇒ 红', () => {
    const r = run(build({ manifest: baseManifest({ ...POPULATION, FxNewComp: 2 }) }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G4 FxNewComp：baseline 里没有这个组件', '却有 2 个静默 rootRadius check'],
    })
  })

  it('G1/G2 判的是**每个组件**不是 total —— 一增一减净变化 0 时照样红（并证明两半各判各的）', () => {
    const manifest = baseManifest({ ...POPULATION, Switch: 15, Tooltip: 11 })
    const r = run(build({ manifest }))
    // 4 条而不是 2 条：人口（G1/G2，读 manifest）与真缺口（G6/G7，读 report）是**两个数、
    // 两个来源**，动一个组件的变体数会同时改变两者 ⇒ 各报各的。这正是闸头注释
    // 「⛔ 别把 silentGap 从 count 推出来、也别反推」在测试面上的样子。
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G1 Switch', 'G2 Tooltip', 'G6', 'G7', 'FAIL（4 条）'],
    })
    // total 仍是 284 —— 只判 total 的话这一对会静静溜过去
    expect(r.stdout).toContain(`静默 rootRadius check 现值 ${TOTAL_NULL}`)
  })
})

describe('audit:render-silent-checks — G5 report 新鲜度（三态）', () => {
  it('态一：缺 `inputsFingerprint` ⇒ 红（旧格式 report），⛔ 不退化成「0 条真缺口」', () => {
    const manifest = baseManifest()
    const r = run(build({
      manifest,
      report: () => ({ checkedAt: 'x', entries: baseReportEntries(manifest) }),
    }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G5', '缺 inputsFingerprint 或形状不对', '别当成「0 条真缺口」'],
    })
  })

  it('态一变体：`inputsFingerprint` 形状不对（value 不是字符串）⇒ 同样红', () => {
    const manifest = baseManifest()
    const r = run(build({
      manifest,
      report: (fp) => ({
        inputsFingerprint: { algo: fp.algo, value: 123 },
        entries: baseReportEntries(manifest),
      }),
    }))
    expectGateRed(r, { marker: FAIL, checks: ['G5', '缺 inputsFingerprint 或形状不对'] })
  })

  it('态一变体：`algo` 换代 ⇒ 红并点名两侧算法（不是误报成「代码变了」）', () => {
    const manifest = baseManifest()
    const r = run(build({
      manifest,
      report: (fp) => ({
        inputsFingerprint: { ...fp, algo: 'sha256-render-inputs-fx-v0' },
        entries: baseReportEntries(manifest),
      }),
    }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G5', '指纹算法版本不同', 'sha256-render-inputs-fx-v0'],
    })
  })

  it('态二 STALE：指纹值不同 ⇒ **不红**，自印 STALE 并说明「哪条判据没跑」', () => {
    const manifest = baseManifest()
    const r = run(build({
      manifest,
      report: (fp) => ({
        inputsFingerprint: { ...fp, value: 'f'.repeat(64) },
        entries: baseReportEntries(manifest),
      }),
    }))
    expectGateGreen(r, {
      contains: [
        '⚠️⚠️ STALE',
        '这份 report **没见过**现在这份 src/manifest',
        'G6/G7/G8（真缺口那半）本轮对这份 report 不判',
        '⚠️ **真缺口那半本轮未判**',
      ],
    })
  })

  it('STALE 时**人口那半照常判** —— 同一份 fixture 里人口涨了仍然红（G5 不是万能挡箭牌）', () => {
    const manifest = baseManifest({ ...POPULATION, Slider: 5 })
    const r = run(build({
      manifest,
      report: (fp) => ({
        inputsFingerprint: { ...fp, value: 'f'.repeat(64) },
        entries: baseReportEntries(manifest),
      }),
    }))
    expectGateRed(r, { marker: FAIL, checks: ['G1 Slider：实测 5 > 记录的 4'] })
    expect(r.stdout).toContain('⚠️⚠️ STALE')
  })

  it('STALE 时**字段面 G9 也照常判**（刻意的不对称，⛔ 别「统一」掉）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks.push({ field: 'rootGap', actual: 8, expected: null, pass: true, status: 'unverifiable' })
    const r = run(build({
      manifest,
      entries,
      report: (fp) => ({ inputsFingerprint: { ...fp, value: 'f'.repeat(64) }, entries }),
    }))
    expectGateRed(r, { marker: FAIL, checks: ['G9', '`rootGap`'] })
    expect(r.stdout).toContain('⚠️⚠️ STALE')
  })
})

describe('audit:render-silent-checks — 真缺口棘轮 G6-G8（读 report）', () => {
  it('G6 真缺口增长 ⇒ 红，且自印「无分档」（report 档组件也一样拦）', () => {
    const manifest = baseManifest()
    // FormItem 是 report 档、baseline silentGap=0；让它长出 1 条真缺口
    const entries = baseReportEntries(manifest)
    const target = entries.find((e) => e.manifestId === idOf('FormItem', 0))!
    target.checks = [unverifiableRow(6)]
    const r = run(build({ manifest, entries }))
    expectGateRed(r, {
      marker: FAIL,
      checks: [
        'G6',
        'FormItem：真缺口实测 1 > 记录的 0',
        '该行 growth=report 只放宽人口，不放宽这个数',
      ],
    })
  })

  it('G7 真缺口缩小 ⇒ 红，要求把 `silentGap` 改小（shrink-only）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    // 2026-08-28 从 Slider 换成 Switch：Slider 的 silentGap 已缩到 0，拿它造「变好」
    // 会落进 G6（0 → n 是增长）而不是 G7。Switch(14) 是仍有余量且未被 G8/多份 report
    // 用例占用的那个。
    entries.find((e) => e.manifestId === idOf('Switch', 0))!.checks = [unverifiableRow(0)]
    const r = run(build({ manifest, entries }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G7', 'Switch：真缺口实测 13 < 记录的 14', '请把 `silentGap` 改成 13'],
    })
  })

  it('G8 覆盖缩水（entry 从 report 里消失）⇒ 红，且报的是 **G8 不是 G7**（顺序钉：G7 的逃逸口）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest).filter((e) => e.manifestId !== idOf('Button', 0))
    const r = run(build({ manifest, entries }))
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G8', 'Button：1 条静默 check 在 manifest 里有、在这份 report 里**没有**', 'covered=3/4'],
    })
    // 关键：gap 被压到 3 也不许报成「变好了」
    expect(`${r.stdout}${r.stderr}`).not.toContain('G7 (render-verification.report.json) Button')
  })

  it('G6-G8 对**每一份** report 各判一次、共用同一份 baseline —— 只坏第二份也红', () => {
    const manifest = baseManifest()
    const good = baseReportEntries(manifest)
    const bad = baseReportEntries(manifest)
    bad.find((e) => e.manifestId === idOf('Tooltip', 0))!.checks = [unverifiableRow(0)]
    const root = build({ manifest, entries: good })
    const fp = computeRenderInputsFingerprint(root)
    writeFixtureFile(root, REACT_REPORT, `${JSON.stringify({ inputsFingerprint: fp, entries: bad }, null, 1)}\n`)
    const r = run(root)
    expectGateRed(r, {
      marker: FAIL,
      checks: ['G7 (react-render-verification.report.json) Tooltip', '真缺口实测 11 < 记录的 12'],
    })
  })
})

describe('audit:render-silent-checks — 字段面 G9（白名单 + fail closed）', () => {
  it('白名单外的字段开始发 `unverifiable` ⇒ 红，理由是「拒绝把没人看过读成没问题」', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks.push({ field: 'rootOpacity', actual: 1, expected: null, pass: true, status: 'unverifiable' })
    const r = run(build({ manifest, entries }))
    expectGateRed(r, {
      marker: FAIL,
      checks: [
        'G9',
        '字段 `rootOpacity`：baseline 的 `byField` 里没有它，却有 1 条',
        '这个字段的静默路径今天活了',
        '别只为了让闸绿就把字段名加进 `byField` 而不填测量结果',
      ],
    })
  })

  it('白名单外的字段只要**不是** `unverifiable` 就不触发（判的是静默路径，不是字段本身）', () => {
    const manifest = baseManifest()
    const entries = baseReportEntries(manifest)
    entries[0].checks.push({ field: 'rootOpacity', actual: 1, expected: 1, pass: true, status: 'pass' })
    const r = run(build({ manifest, entries }))
    expectGateGreen(r, { contains: ['report 里出现过 2 个字段名', 'rootRadius=284(由 G0-G8 判)'] })
  })

  it('`rootRadius` 在 baseline 里是 `null` ⇒ 本臂只印不判（免得同一件事红两次）', () => {
    const r = run(build())
    expectGateGreen(r, { contains: ['rootRadius=284(由 G0-G8 判)'] })
    expect(`${r.stdout}${r.stderr}`).not.toContain('G9 (render-verification.report.json) 字段 `rootRadius`')
  })
})

describe('audit:render-silent-checks — 退出码与流向', () => {
  it('红档走 **stderr**、stdout 里没有 PASS 串（CI 抓 stdout 的话会漏判）', () => {
    const r = run(build({ manifest: baseManifest({ ...POPULATION, Button: 5 }) }))
    expect(r.status).toBe(1)
    expect(r.stderr).toContain(FAIL)
    expect(r.stdout).not.toContain(PASS)
  })

  it('多条失败一次报全（不是撞到第一条就退出）—— 三个组件各出问题 ⇒ 5 条', () => {
    const manifest = baseManifest({ ...POPULATION, Switch: 15, Button: 5, FxOther: 3 })
    const r = run(build({ manifest }))
    // 5 = G1 Switch + G1 Button + G6 Switch + G6 Button + G4 FxOther。
    // `FxOther` 只有 G4 一条：它不在 baseline.byComponent 里 ⇒ G6-G8 那个循环压根不遍历它
    // （闸逐字只遍历 baseline 的 key）—— 这也是 G4 存在的理由：先逼人把它登记进表。
    expectGateRed(r, {
      marker: FAIL,
      checks: ['FAIL（5 条）', 'G1 Switch', 'G1 Button', 'G4 FxOther'],
    })
    expect(`${r.stdout}${r.stderr}`).not.toContain('FxOther：真缺口实测')
  })

  it('绿档 exit 0 且 PASS 行如实分三半陈述射程（人口 / 真缺口 / 字段面）', () => {
    const r = run(build())
    expectGateGreen(r, {
      contains: ['静默 check 的**人口**没有在无人知晓的情况下变大', '**字段面**在 1 份 report 上没有新字段开始静默（G9）'],
    })
  })
})
