// [[INFRA-F138]] —— `scripts/audit-exports-types-contract.mjs` 的**整脚本**回归面。
// -----------------------------------------------------------------------------
// 与 `tests/audit-exports-types-contract.test.ts` 的分工（⛔ 别合并，两者测的不是一回事）：
//   · 那一份 `import { audit }` 直接调判据函数 ⇒ 覆盖**判据逻辑**，看不见 `main()` 的接线。
//   · 本份 **spawn 整个脚本**（共享 harness `tests/lib/gate-fixture-root.ts`）⇒ 覆盖
//     入口守卫 / 判据 → 输出 → 退出码的**整条接线**。被测闸一行不改。
//
// 为什么这条闸值得补（爆炸半径，⛔ 不是「挂载最深」也不是「最便宜」—— 它只 2 层）：
//   它自己不随包，但它**守的对象随包** —— `package.json` 的 `exports` types 条件与
//   `dist/*.d.ts` 的内部链路。假绿的代价落在**所有下游 consumer 的类型面**，本仓任何
//   闸都看不见（同型先例 = `audit-consumer-contract` 那一轮）。而且这条闸的假绿**已经
//   真实发生过一次**：INFRA-F114（2026-08-13）consumer 侧 TS7016，15 个 exports 条目里
//   只有 5 个带 types、**主出口 `.` 就缺**，顶层兜底 d.ts 自己断在 `./icons/index` 上。
//   闸头注释另逐字论证了 `consumer-contract`（⛔ 去 `audit:` 冒号 —— 见文末污染面纪律）的 S2
//   结构上抓不到这两面（它校的是「types 指向的东西存在」，不是「types 存在」）
//   ⇒ 本闸是这两面的**唯一**机械保障。
//
// 🔴 **本轮实测：harness 的 `realpathSync` 对本闸是承重的。** 本闸用的是**原始**入口守卫
//   （`resolve(argv[1]) === resolve(fileURLToPath(import.meta.url))`），属 F138 记的
//   「14 条非随包闸仍用原始守卫」那一档（⛔ 那不是缺陷 —— 它们不随包、只从仓库根跑）。
//   独立探针实测：fixture root 不 realpath ⇒ **11 个场景全部 `EXIT=0` + 双流零字节**。
//
// 🔴 **S3 是一个 fail-open 出口，而它开不开由环境决定**（= F138 记的「第四种形态：分叉
//   不在数据里、在环境里」，与 `audit-page-recipes` 的 S4 同型）。`dist/` 是 gitignored
//   产物 ⇒ 同一份闸、同一个 commit：
//     · 主仓（build 过）  `5 份 d.ts 做了链路解析`，**无** SKIPPED 警告
//     · worktree（无 dist）`0 份 d.ts 做了链路解析` + S3 SKIPPED 警告
//   ⇒「它每次 commit 都跑所以有覆盖」在这一层上取决于谁的机器上有 `dist`。
//   fixture 是唯一能把**红 / 绿 / SKIPPED 三态**都钉住的地方（本文件 F / G 两组）。
//
// ⚠️ **本文件的绿档是「真 EXEMPTIONS 表」的漂移钉 —— 这是设计意图，不是脆弱**：
//   CLI 走 `audit()` 的默认参数 ⇒ 用的是闸里**硬编码**的 `EXEMPTIONS`（不可从 fixture 注入）。
//   ⇒ 谁改了那张表（增 / 删 / 改 subpath），本文件会红，并由下方 C 组那条自钉判据**直接说出
//   原因**。同 `audit-token-contract` 内联 generator `HEADER` 的既有范式。
//
//   🔴 **2026-08-28 那张表缩到空了，本段随之翻转，⛔ 别照旧版读**：此前这里逐字写着
//   「每个绿档 fixture 的 `package.json` 必须自带 `./web-components` 这个**无 types 的代码
//   出口**」—— 那是表里有唯一一行 `./web-components` 时的形态。当天该出口补上了随包 d.ts +
//   `types` 条件，S4 的 shrink-only 判据据此要求删掉豁免行，表回到终态空。⇒ 现在反过来：
//   绿档 fixture 的那个出口**必须自带 types**，和别的代码出口一样。
//
// ⛔ **本面覆盖不到 S4 的任何分支**（2026-08-28 起）：表空 ⇒ `for (const ex of exemptions)`
//   一次都不进 ⇒ 豁免通道与 shrink-only 两条红档在 CLI 面**结构上不可达**。这正是那条
//   shrink-only 判据**生效了**的样子 —— 它逼着上一条豁免被删掉，然后自己没戏可唱。
//   ⇒ 下方 E 组一条、H 组两条改为 `it.skip` 并**保留场景描述**（不删），C 组第一条钉住
//   「表是空的」；表一旦再非空，C 组当场红并指回那三条要求恢复。
//   判据逻辑由纯函数那份（`tests/audit-exports-types-contract.test.ts`，可传 `exemptions`
//   参数）覆盖，那边四个 S4 场景都是活的。
//   **如实登记的边界，不是待补 TODO** —— 与前几轮的 `STRUCTURAL_L4_ONLY` / `KNOWN_COLLISIONS`
//   同型。⚠️ 旧版这里写的是另一个方向（「表里那行是对的，造不出**错的**」，指 `since` 形态错 /
//   缺 `fixDirection` 那两条畸形行分支）—— 那句现在被更强的条件吸收了：表空 ⇒ 连合规行都没有。
//
// 🔴 **一处如实登记的闸自印畸形，本轮刻意未修、报 owner**（⛔ 下方 D 组那条断言是**钉住
//   现行行为**，不是背书）：~~四条 S1 早退路径返回的对象不带 `exemptCount` ⇒ 自印印成
//   `undefined 条具名豁免`~~ → ✅ **2026-08-28 owner 拍板已修**（三条早退各补该字段；
//   ⚠️ 现取实测是**三条**不是四条，原文那个数已订正）。下方 D 那条钉已翻转方向。
//
// ⛔ 污染面纪律（[[INFRA-F138]] 逐字）：fixture 假名统一用 `vlum-*`（落地前实测全仓 0 命中）；
//   散文与断言里提到别的闸一律**去 `.mjs` 后缀、去 `audit:` 冒号**，否则量具会把那条闸
//   误报成「已覆盖」（高报）或挪档（低报）。本闸 stdout 里逐字印着另一条闸的 npm key
//   （覆盖面那句的括号里），⇒ 断言该行时**只取不含冒号的片段**。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import {
  createGateFixture,
  runGate,
  expectGateRed,
  expectGateGreen,
  cleanupGateFixtures,
  type GateRun,
} from './lib/gate-fixture-root'
import {
  EXEMPTIONS,
  REPO_ROOT,
  readPackageJson,
  isCodeEntry,
} from '../scripts/audit-exports-types-contract.mjs'

const GATE = 'scripts/audit-exports-types-contract.mjs'

/**
 * 一个额外的代码出口，用来把分母顶到 4。
 *
 * ⚠️ 2026-08-28 语义变了：它**曾经**是「真 EXEMPTIONS 表里唯一那一条的 subpath」，绿档
 * 靠真豁免表放行才不红。豁免表当天缩到空（`./web-components` 补上了随包 d.ts），于是这个
 * 出口现在必须**自带 types**，和别的代码出口一样。名字保留不变：它同时还是 H 组那两条
 * S4 场景的 subpath，改名只会让 skip 掉的那几条更难读回来。
 */
const WC = './web-components'

/** fixture 自己的读数（非空过凭据 —— 三个来自**不同输入面**的数，见 A 组）。 */
const FIXTURE_CODE_ENTRIES = 4 // ← 输入面①：package.json 的 exports 条目形态
const FIXTURE_CHECKED_DTS = 3 // ← 输入面②：fixture 磁盘上 d.ts 的存在性（2026-08-28 由 2 增：WC 现在也有 types）
const FIXTURE_ROOT_TYPES = './dist/vlum-root.d.ts'

// 真仓库读数 —— ⛔ **不写死**，当场从活源算，否则真仓库一改这些反向钉就静默变空。
const REAL_PKG = readPackageJson(REPO_ROOT) as {
  types?: string
  exports?: Record<string, unknown>
}
const REAL_CODE_ENTRIES = Object.values(REAL_PKG.exports ?? {}).filter(isCodeEntry).length
const REAL_ROOT_TYPES = REAL_PKG.types as string

function pkg(obj: unknown): string {
  return JSON.stringify(obj, null, 2)
}

const ROOT_DTS = "export { A } from './vlum-sub';\nimport type { Ref } from 'vue';\nimport 'node:fs';\nexport type X = Ref<number>;\n"
const HEALTHY_PKG = pkg({
  name: '@vlum/fixture',
  types: FIXTURE_ROOT_TYPES,
  exports: {
    '.': { types: FIXTURE_ROOT_TYPES, import: './dist/vlum-root.js' },
    './chart': { types: './dist/vlum-chart.d.ts', import: './dist/vlum-chart.js' },
    // subpath-pattern：types 里带 `*` ⇒ 该跳过且**不**触发 SKIP 警告
    './icons/esm/*': { types: './dist/vlum-icons/*.d.ts', import: './dist/vlum-icons/*.js' },
    // 2026-08-28：豁免表缩到空之后，这个出口和别的代码出口一样要自带 types
    [WC]: { types: './dist-wc/vlum-wc.d.ts', import: './dist-wc/vlum-wc.js' },
    './style.css': './dist/vlum.css', // ← 字符串形态 = 资产，不是代码出口
  },
})

const HEALTHY_FILES: Record<string, string> = {
  'package.json': HEALTHY_PKG,
  'dist/vlum-root.d.ts': ROOT_DTS,
  'dist/vlum-sub.d.ts': 'export declare const A: number;\n',
  'dist/vlum-chart.d.ts': "export { default as C } from './vlum-canonical/Chart.vue';\n",
  'dist/vlum-canonical/Chart.vue.d.ts': 'declare const _d: unknown;\nexport default _d;\n',
  // 零相对 specifier —— 与真仓库的随包 web-components d.ts 同形（它只做 JSX 增广，不 re-export）
  'dist-wc/vlum-wc.d.ts': 'export declare function registerVlumElements(): void;\n',
}

/** 建 fixture + 跑整条闸。`overrides` 覆盖 / 新增 fixture 文件。 */
function run(prefix: string, overrides: Record<string, string> = {}, drop: string[] = []): GateRun {
  const files: Record<string, string> = { ...HEALTHY_FILES, ...overrides }
  for (const d of drop) delete files[d]
  const root = createGateFixture({
    gate: GATE,
    prefix,
    files,
    // ⛔ 显式关掉 harness 默认的 `scripts/lib` 软链 —— 本闸只 import node 内建，不碰它。
    linkDirs: [],
  })
  return runGate(root, GATE)
}

/** 取「程序自己最后那句话」—— 终态事实，见 I 组。 */
function lastLine(s: string): string {
  const lines = s.replace(/\s+$/, '').split('\n')
  return lines[lines.length - 1]
}

afterAll(cleanupGateFixtures)

// =============================================================================
describe('A 绿档 —— 非空过凭据取自三个不同输入面', () => {
  it('健康 fixture 绿，且自印的三个数都是 fixture 自己算出来的', () => {
    const r = run('vlum-healthy')
    expectGateGreen(r, {
      contains: [
        // 输入面①：exports 的条目形态（`.` / ./chart / ./icons/esm/* / ./web-components = 4；
        //          字符串形态的 ./style.css 不算）
        `${FIXTURE_CODE_ENTRIES} 个代码出口受检`,
        // 输入面②：磁盘上真实存在、且 types 里不含 `*` 的 d.ts 份数
        `${FIXTURE_CHECKED_DTS} 份 d.ts 做了链路解析`,
        // 输入面③：d.ts **文件内容**里解析出的相对 specifier 条数
        './dist/vlum-root.d.ts — 1 个相对 export-from',
        './dist/vlum-chart.d.ts — 1 个相对 export-from',
      ],
    })
  })

  it('绿档是真绿：stderr 零字节、stdout 无 FAILED 标记（常驻负向对照）', () => {
    const r = run('vlum-healthy-neg')
    expect(r.status).toBe(0)
    expect(r.stderr).toBe('')
    expect(r.stdout).not.toContain('FAILED')
    expect(r.stdout).not.toContain('✗')
    // must-not-hit 的正向锚点：闸确实跑到了最后一句
    expect(r.stdout).toContain('✓ exports types contract OK')
  })

  it('subpath-pattern（types 里带 `*`）不算「取不到产物」⇒ 绿档不许出现 SKIPPED 警告', () => {
    const r = run('vlum-healthy-pattern')
    expect(r.status).toBe(0)
    expect(r.stdout).not.toContain('S3 部分 SKIPPED')
    // 正向锚点：那条 pattern 条目确实进了分母（4 而不是 3）
    expect(r.stdout).toContain(`${FIXTURE_CODE_ENTRIES} 个代码出口受检`)
  })

  it('覆盖面自陈照常印出（⛔ 断言刻意截短，不含另一条闸的 npm key —— 见文件头污染面纪律）', () => {
    const r = run('vlum-healthy-scope')
    expect(r.stdout).toContain('不做类型检查本身')
  })
})

// =============================================================================
describe('B 反向钉 —— 绿档没有退回真仓库读', () => {
  it('反向钉本身是有意义的（fail-closed 自证：fixture 的数 ≠ 真仓库的数）', () => {
    // ⛔ 若哪天真仓库的代码出口数恰好变成 4、或 types 恰好指向同一个路径，
    //    下面两条反向钉会静默失去意义 ⇒ 这条先把前提钉住。
    expect(REAL_CODE_ENTRIES).not.toBe(FIXTURE_CODE_ENTRIES)
    expect(REAL_ROOT_TYPES).not.toBe(FIXTURE_ROOT_TYPES)
    expect(REAL_CODE_ENTRIES).toBeGreaterThan(0)
    expect(REAL_ROOT_TYPES.length).toBeGreaterThan(0)
  })

  it('stdout 不含真仓库的代码出口数，也不含真仓库的顶层 types 路径', () => {
    const r = run('vlum-reverse')
    expect(r.stdout).not.toContain(`${REAL_CODE_ENTRIES} 个代码出口受检`)
    expect(r.stdout).not.toContain(REAL_ROOT_TYPES)
    // 正向锚点（must-not-hit 不许空过）：fixture 自己那两个值确实印出来了
    expect(r.stdout).toContain(`${FIXTURE_CODE_ENTRIES} 个代码出口受检`)
    expect(r.stdout).toContain(FIXTURE_ROOT_TYPES)
  })
})

// =============================================================================
describe('C 自钉判据 —— 绿档 fixture 与真 EXEMPTIONS 表的关系', () => {
  it('真表是空的（终态）—— 谁往表里加行，本条与下方 skip 掉的三条一起说明该怎么改', () => {
    // 沿革：2026-08-13 到 2026-08-28 之间这张表有唯一一行 `./web-components`，绿档 fixture
    // 靠它放行。2026-08-28 该出口补上随包 d.ts + `types` 条件，S4 的 shrink-only 要求删行，
    // 表回到终态空。闸自己的头注释逐字：「表空着是终态，不是待办」。
    //
    // 🔴 **这条现在承担双重职责，别删**：
    //   ① 表非空 ⇒ 本条红，直接说出「绿档为什么突然红了」（原漂移钉的职责，方向反过来）；
    //   ② 表非空 ⇒ 下方 E/H 那三条 `it.skip` 的前提消失，应当同时恢复它们
    //      （它们测的是豁免通道与 S4 分支，只有表非空时在 CLI 面才可达）。
    expect(EXEMPTIONS).toHaveLength(0)
  })

  it('自印的豁免条数与真表长度一致（绿档路径上）', () => {
    // 表空时这行印的是 `0 条具名豁免` —— 仍是活判据：它证明自印读的是真表，
    // 而不是把某个数写死在格式串里。
    const r = run('vlum-exempt-count')
    expect(r.stdout).toContain(`${EXEMPTIONS.length} 条具名豁免`)
    expect(r.stdout).toContain('0 条具名豁免')
  })
})

// =============================================================================
describe('D S1 分母 fail closed —— 四条路径都必须红，⛔ 不许「扫到 0 个所以通过」', () => {
  it('package.json 没有 exports → 红', () => {
    const r = run('vlum-s1-none', { 'package.json': pkg({ name: '@vlum/fixture' }) })
    expectGateRed(r, { marker: '✗ exports types contract FAILED', checks: ['S1 分母 fail closed'] })
  })

  it('exports 是空对象 → 红', () => {
    const r = run('vlum-s1-empty', { 'package.json': pkg({ name: '@vlum/fixture', exports: {} }) })
    expectGateRed(r, { checks: ['S1 分母 fail closed'] })
  })

  it('exports 只有字符串资产条目、一个代码出口都没有 → 红', () => {
    const r = run('vlum-s1-assets', {
      'package.json': pkg({ name: '@vlum/fixture', exports: { './style.css': './dist/vlum.css' } }),
    })
    expectGateRed(r, { checks: ['一个「代码出口」', '都没有'] })
  })

  it('连 package.json 都没有 → 红且点名读不到（不抛、不静默通过）', () => {
    const r = run('vlum-s1-nopkg', {}, ['package.json'])
    expectGateRed(r, { checks: ['S1 读不到 package.json', 'ENOENT'] })
    // 无 fallback 回真仓库的凭据：ENOENT 的路径必须指向 fixture，⛔ 不是本仓
    expect(r.stderr).toContain('vlum-s1-nopkg')
    expect(r.stderr).not.toContain(REPO_ROOT as string)
  })

  it('S1 早退时自印仍印出真实豁免条数（2026-08-28 修，此前印 `undefined 条具名豁免`）', () => {
    // 沿革：S1 的早退路径返回的对象**不带 `exemptCount`** ⇒ 自印那行印成
    // `undefined 条具名豁免`。第五十九轮查实后先用一条钉住畸形现状并**逐字标注不是背书**，
    // 把改法交给 owner（当时按 [[INFRA-F138]]「闸本体一行没改」的纪律不动闸）；
    // owner 2026-08-28 拍板修，三条早退各补 `exemptCount: exemptions.length`，本条随之翻转。
    //
    // ⚠️ **顺带订正一个数**：此前这里与 CHANGELOG 都写「**四条** S1 早退」，
    // 2026-08-28 现取实测是 **三条**（`return {` 站点共 4 个，其中 `:237` 是正常出口）。
    // ⛔ 别再照抄「四条」。
    const r = run('vlum-s1-selfprint', { 'package.json': pkg({ name: '@vlum/fixture' }) })
    expect(r.stdout).not.toContain('undefined 条具名豁免')
    // 判据取**终态事实**：印出来的必须是个真数字，而不是「只要不是 undefined 就算过」。
    expect(r.stdout).toMatch(/· \d+ 条具名豁免 ·/)
    // 正向锚点：闸确实跑到了自印那行（而不是压根没跑）
    expect(r.stdout).toContain('0 个代码出口受检')
  })
})

// =============================================================================
describe('E S2 types 存在性 —— F114 ① 的形态（缺 types ⇒ consumer 拿 TS7016）', () => {
  it('代码出口缺 types → 红、点名该 subpath、并说出 consumer 会拿什么错', () => {
    const r = run('vlum-s2', {
      'package.json': pkg({
        name: '@vlum/fixture',
        exports: {
          '.': { import: './dist/vlum-root.js' },
          [WC]: { import: './dist-wc/vlum-wc.js' },
        },
      }),
    })
    expectGateRed(r, { checks: ['S2 exports["."]', '没有 types 条件', 'TS7016'] })
  })

  // ⛔ 2026-08-28 起结构上不可达（表空 ⇒ 没有任何 subpath 走得到豁免通道），保留描述不删：
  //    表一旦再非空，C 组第一条会红并指回这里要求恢复。判据逻辑由纯函数那份
  //    （`tests/audit-exports-types-contract.test.ts`，可传 `exemptions` 参数）覆盖。
  it.skip('被豁免的那个出口缺 types → 不红（豁免通道确实通）—— 表空后 CLI 面不可达', () => {
    const r = run('vlum-s2-exempt')
    expect(r.status).toBe(0)
    // 正向锚点：那个出口确实在分母里（不是被整条忽略了）
    expect(r.stdout).toContain(`${FIXTURE_CODE_ENTRIES} 个代码出口受检`)
  })
})

// =============================================================================
describe('F S3 链路可解析 —— F114 ② 的形态（文件在、但它自己断链）', () => {
  it('d.ts 断在不存在的相对路径 → 红并点名那个 specifier', () => {
    const r = run('vlum-s3-broken', {
      'dist/vlum-root.d.ts': "export { A } from './vlum-nowhere';\n",
    })
    expectGateRed(r, {
      checks: ['S3 ./dist/vlum-root.d.ts', "from './vlum-nowhere'", '解析不到任何真实文件', 'TS2307'],
    })
  })

  it('断在 icons/index 上时**额外**给出根因指路（条件分支）', () => {
    const r = run('vlum-s3-icons', {
      'dist/vlum-root.d.ts': "export { A } from './icons/index';\n",
    })
    expectGateRed(r, { checks: ['解析不到任何真实文件', 'INFRA-F114 ②', 'icon-artifacts'] })
  })

  it('🔴 阴性对照：断在**非** icons 路径上时不许给那条根因指路', () => {
    // 这条分支在**真仓库里永远进不去**（真仓库是绿的，S3 一条 failure 都不产生）
    // ⇒ 属 [[INFRA-F138]] 记的「判据全在，而真仓库数据只覆盖一条路径」那一档。
    const r = run('vlum-s3-negative', {
      'dist/vlum-root.d.ts': "export { A } from './vlum-nowhere';\n",
    })
    expect(`${r.stderr}\n${r.stdout}`).not.toContain('icon-artifacts')
    // must-not-hit 的正向锚点：这一次判据确实开火了（不是压根没跑）
    expect(r.stderr).toContain("from './vlum-nowhere'")
  })

  it('裸包名 / node: 内建不参与解析（否则健康 fixture 会假红）', () => {
    // ROOT_DTS 里就有 `from 'vue'` 与 `import 'node:fs'`；它绿本身即凭据。
    const r = run('vlum-s3-bare')
    expect(r.status).toBe(0)
    expect(r.stdout).toContain('./dist/vlum-root.d.ts — 1 个相对 export-from')
  })
})

// =============================================================================
describe('G S3 的 fail-open 出口 —— 三态里的 SKIPPED 那一态', () => {
  it('types 目标不在磁盘上 → 绿，但必须大声说明「不保证 consumer 拿得到类型」', () => {
    const r = run('vlum-s3-skip', {
      'package.json': pkg({
        name: '@vlum/fixture',
        exports: {
          '.': { types: './dist/vlum-absent.d.ts', import: './dist/vlum-root.js' },
          [WC]: { types: './dist-wc/vlum-absent.d.ts', import: './dist-wc/vlum-wc.js' },
        },
      }),
    })
    expectGateGreen(r, {
      contains: [
        'S3 部分 SKIPPED',
        '不保证 consumer 拿得到类型',
        '0 份 d.ts 做了链路解析',
      ],
    })
  })

  it('SKIPPED 与 FAIL 是两回事：SKIPPED 档不许出现 FAILED 标记', () => {
    const r = run('vlum-s3-skip-neg', {
      'package.json': pkg({
        name: '@vlum/fixture',
        exports: {
          '.': { types: './dist/vlum-absent.d.ts', import: './dist/vlum-root.js' },
          [WC]: { types: './dist-wc/vlum-absent.d.ts', import: './dist-wc/vlum-wc.js' },
        },
      }),
    })
    expect(r.stderr).toBe('')
    expect(r.stdout).not.toContain('FAILED')
    // 正向锚点：这一档确实走到了 SKIPPED 分支（而不是根本没跑）
    expect(r.stdout).toContain('S3 部分 SKIPPED')
  })
})

// =============================================================================
describe('H S4 豁免 shrink-only —— 修完必须删行，不许留永久洞', () => {
  // ⛔ 本组两条 2026-08-28 起在 CLI 面结构上不可达 —— 表空 ⇒ S4 的 for 循环一次都不进。
  //    这正是这条 shrink-only 判据**成功了**的样子：它逼着上一条豁免被删掉，然后自己没戏可唱。
  //    ⇒ 不删，保留场景描述；表再非空时 C 组第一条会红并指回这里。
  //    判据逻辑仍由纯函数那份（传 `exemptions` 参数）覆盖，那边这两个场景都是活的。
  it.skip('被豁免的出口现在有 types 了 → 红并要求删掉那一行（表空后 CLI 面不可达）', () => {
    const r = run('vlum-s4-stale', {
      'package.json': pkg({
        name: '@vlum/fixture',
        exports: {
          '.': { types: FIXTURE_ROOT_TYPES, import: './dist/vlum-root.js' },
          [WC]: { types: FIXTURE_ROOT_TYPES, import: './dist-wc/vlum-wc.js' },
        },
      }),
    })
    expectGateRed(r, { checks: [`S4 豁免行 "${WC}"`, 'shrink-only', '请删掉这一行'] })
  })

  it.skip('豁免指向的出口已不存在 → 红并要求删掉那一行（表空后 CLI 面不可达）', () => {
    const r = run('vlum-s4-gone', {
      'package.json': pkg({
        name: '@vlum/fixture',
        exports: { '.': { types: FIXTURE_ROOT_TYPES, import: './dist/vlum-root.js' } },
      }),
    })
    expectGateRed(r, { checks: [`S4 豁免行 "${WC}"`, '已不是代码出口或已不存在'] })
  })
})

// =============================================================================
describe('I 终态事实 —— 防「碰巧崩在同一个退出码上」的假红', () => {
  // 判据 = **闸自己的最后一句话必须是输出的最后一行**。
  // 崩溃同样是非零退出码，只断言 status 的红是假红（harness 头注释判据 2 逐字预言过）。
  it('绿档：stdout 末行恰好是闸的收尾句', () => {
    const r = run('vlum-terminal-green')
    expect(lastLine(r.stdout)).toBe('✓ exports types contract OK')
  })

  it('红档：stderr 末行必须是闸自己印的 failure 条目，不是崩溃栈', () => {
    const r = run('vlum-terminal-red', { 'package.json': pkg({ name: '@vlum/fixture' }) })
    expect(r.status).toBe(1)
    expect(lastLine(r.stderr)).toMatch(/^ {2}- S1 /)
    // 崩溃栈尾会是 `Node.js vXX.Y.Z` 这类；这条把它排除掉
    expect(lastLine(r.stderr)).not.toMatch(/^Node\.js v/)
  })

  it('红档：stdout 那半照常印完（判据红 ≠ 程序半途死掉）', () => {
    const r = run('vlum-terminal-red-out', { 'package.json': pkg({ name: '@vlum/fixture' }) })
    expect(lastLine(r.stdout)).toContain('不做类型检查本身')
  })
})
