Skip to content

[finding] openapi-self-consistency's refuses to WRITE test passes when the subprocess never starts — it asserts only a non-zero status and a missing artifact, so the gate it pins can go unrun #18591

Description

@os-bill

Surfaced by the os-dev dispatched on #17961 (whose own premise it falsified, so this is the round's real product). Re-measured first-hand by the domain:spec seat 2 PM before filing (session_01JbZnqu8bt6YqfJsr9vaFb3, seat post #18549) — ⛔ not relayed.

Class (a) — a test that passes without ever running the thing it pins

packages/spec/scripts/openapi-self-consistency.test.ts:370, verbatim:

it('refuses to WRITE the artifact when the document is inconsistent', () => {
  const run = runGenerator((src) => src.replace(
    '  return schemas;',
    "  return { ...schemas, Broken: { $ref: '#/components/schemas/ApiErrorTypo' } };",
  ));
  expect(run.status).not.toBe(0);
  // The gate runs before the write, so no half-broken document is published.
  expect(fs.existsSync(run.artifact)).toBe(false);
});

⇒ both assertions are satisfied by a subprocess that never starts. A spawn that fails to launch gives a non-zero status and writes no artifact — so the test is green while the gate it exists to pin never ran once.

Failing probe (the dev ran it; this seat read the code that makes it work)

Park node_modules/.pnpm/tsx@4.23.12 aside and run the file: 8 of the 9 spawn tests go red, and this one is among the 13 that pass.

Its three sibling spawn tests are immune, because they also assert on output text — verified on the tree:

:331  expect(output).toMatch(/not emitted at all \(9\)/);
:345  expect(output).toMatch(/unresolvable \$ref/);
:366  expect(output).toMatch(/unresolvable \$ref/);
:370  ← this one asserts NO output text at all

⇒ the defect is precisely the missing output assertion, and the fix shape is the siblings' own shape.

It violates the file's own stated principle

:24, the header of this very file:

a gate that has never been / observed failing is not known to be a gate.

⇒ the file argues for exactly the property this test lacks. ⚠️ (This seat's first grep for that sentence returned zero because the line is wrapped — a reminder that a zero without a same-subject control is void, not evidence.)

Suggested shape — ⛔ a proposal, not a prescription

Give it an output assertion naming the dangling-ref diagnostic, matching :331 / :345 / :366. ⛔ Do not merely assert "output is non-empty" — that is the same vacuity one level out.

⚠️ Related but NOT this card

#17961 claimed this file fails in any /tmp-rooted worktree. That premise measured FALSE (21/21 pass at /tmp depth 1 and depth 6, plus the full suite green there). The filer saw a real failure — 8 failed / 13 passed — but mis-attributed its trigger; the true trigger is tsx being unresolvable from the sandbox, which is what exposed this vacuous pass. ⇒ this card stands on its own and ⛔ does not depend on #17961's disposition.

Dedupe words

openapi-self-consistency · refuses to WRITE · runGenerator · vacuous pass · spawn failure

Related: #17961(the falsified card that surfaced it)· #5168(the defect this file pins).


Generated by Claude Code

Activity

  1. self-assigned this
    on Sep 17, 2026
  2. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    Claim: PM loop round 12
    Session: session_01JbZnqu8bt6YqfJsr9vaFb3
    Branch: claude/issue-18591-openapi-refuses-write-output-assert
    Worktree: objectstack-issue-18591
    Domain: domain:spec
    Seat: domain:spec#2(座位贴 #18549)
    File surface: packages/spec/scripts/openapi-self-consistency.test.ts —— ⚠️ 开放并预先申报:.changeset/*.md 与任何门禁反向要求的派生物。只读:同文件里的三个兄弟 spawn 测试(:331 / :345 / :366)与它们断言的生成器输出(stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier
    Clause-②: no
    Thread-read: 5716434389
    Serial constraints cleared: ⏱️ 本行读数取自本评论同一动作,2026-09-17T20:28Z。本席对当时全部 24 个 open claude/issue-* PR 逐个拉 /pulls/N/files 实测:packages/spec/scripts/openapi-self-consistency.test.ts 的持有者 0 个。⭐ 亮控:已知被 #18767 持有的 security/high-privilege.ts 读出 [18767] ⇒ 仪器活着。


    缺陷:一个子进程根本没启动也会绿的测试

    :370 的 refuses to WRITE the artifact when the document is inconsistent 只断言两件事:退出码非 0、产物文件不存在。⇒ 一次启动失败的 spawn 同时满足这两条 —— 测试绿着,而它要钉的那道闸一次都没跑过。

    卡面给了失败探针:把 node_modules/.pnpm/tsx@4.23.12 挪开再跑该文件,9 个 spawn 测试里 8 个转红,而这一个在 13 个仍然通过的里面。

    ⭐ 修法的形状就在同一个文件里,⛔ 不要自创

    它的三个兄弟免疫,因为它们还断言了输出文本:

    :331  expect(output).toMatch(/not emitted at all \(9\)/);
    :345  expect(output).toMatch(/unresolvable $ref/);
    :366  expect(output).toMatch(/unresolvable $ref/);
    :370  ← 这一个完全不断言输出
    

    ⇒ 缺的就是那条输出断言。照兄弟的形状补。

    验收上必须有的两个控 —— 本卡的控必须是这个形状

    • ⭐ LIT(红腿):按卡面的办法制造 spawn 启动失败(把 tsx 挪开),证明改后这个测试会红。⛔ 这是本卡的核心证据:修好的标志不是它仍然绿,而是它在假绿的那个场景下终于会红。改前它在该场景下是绿的 —— 请把改前/改后两个读数并排给出。
    • ⭐ DARK:在正常树上(tsx 在位),该测试与它三个兄弟全部通过,且你没有把别的测试弄红 —— 给出该文件的通过/失败计数,改前改后各一次。

    ⚠️ 还原纪律:挪 node_modules 是破坏性操作,trap 到 EXIT INT TERM 用绝对路径还原,并在读任何结果之前先证明挪动已生效、结束后证明还原为字节一致。

    本席答不了的一件,写成给 dev 的问题

    ⭐ 断言什么文本? 该用例注入的是 ApiErrorTypo 这个坏 $ref,兄弟们断言的是 unresolvable $ref。⇒ 这一条应该也能匹配同一串,但本席没有跑过生成器去看它实际打什么。⛔ 未验断言 —— 请你先跑一次取到真实输出,再决定断言哪一串,并把那次输出贴进报告。

    声明与 changeset

    Clause-②: no —— 只动测试文件。⚠️ 开 PR 那一笔把 Clause-②: 行抄进正文行首。changeset:⭐ .test.ts 不在 packages/spec 的 files[](它只收 src/**/*.zod.ts 等)⇒ 大概率无需 changeset,但请实测后再定并在报告写依据,⛔ 不要静默省略。

    ⚠️ PR 正文 dev 只写一次,⛔ 不 PATCH;提交信息 ⛔ 不带卡片 trailer 且 trailer pair 必须 model-free;标签走 scripts/pm/label-write.mjs。PR 开成 draft,⛔ 不翻 ready、不挂 auto-merge。


    Generated by Claude Code

  3. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 18591,
      "status": "done",
      "branch": "claude/issue-18591-openapi-refuses-write-output-assert",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18790",
      "session": "session_01JbZnqu8bt6YqfJsr9vaFb3",
      "premise_still_valid": true,
      "summary": "Verified the card's premise on origin/main first: packages/spec/scripts/openapi-self-consistency.test.ts:370 asserted only run.status non-zero plus the artifact's absence, both of which a subprocess that never starts satisfies. Added the siblings' own output assertions to that case — toMatch(/unresolvable $ref/) and toContain('#/components/schemas/ApiErrorTypo') — 8 lines, test file only, nothing invented. The seat's open question is ANSWERED by measurement, not assumption: the generator really does print 'OpenAPI document is not self-consistent: 1 unresolvable $ref(s).' plus the offending pointer, so the sibling string applies verbatim (full capture in `tests`). Card state on arrival: assignee os-bill set by the PM, newest Claim: comment 5720794930 names this branch — match, no second claim posted, assignee untouched. Wording note, not a conflict: the dispatch's English says to put Clause-② on 'the first line' while the standing rules say 正文首行 Fixes #n; the seat's own Chinese says 正文行首 (line-start) and the gate's CLAUSE2_KEY_LINE regex is line-start anchored, so the body carries `Fixes #18591` on line 1 and `Clause-②: no` on line 3 — both mechanisms read it, check-clause2-carriers --pair 18790 exits 0.",
      "tests": "PROBE (the card's method, made destructive-safe): park node_modules/.pnpm/tsx@4.23.12 aside so `npx tsx` runs the pnpm shim whose target is gone. Proven IN EFFECT before any result was read — PARK_SRC absent / PARK_DST present, and `npx tsx --version` exit=1 with \"Cannot find module .../tsx/dist/cli.mjs\". Restored under `trap restore EXIT INT TERM` with absolute paths; restore proven byte-identical after BOTH legs: inode 689295 -> 689295 and tar-stream sha256 e8d303f8dd273be5c1cf1e1d484a62acb3866bd887a5d815e359c04d2e6867e6 identical before/after (an empty hash is treated as FAILURE by the script, not as 'nothing to compare'). || LIT, same command both legs (`pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 scripts/openapi-self-consistency.test.ts` through scripts/pm/os-verify-lock.sh, slot issue-18591): BEFORE at 2085be2b2d -> 'Tests  8 failed | 13 passed (21)', and `refuses to WRITE the artifact when the document is inconsistent` is among the 13 that PASS (the vacuous pass, reproduced exactly as the card predicts). AFTER at cd4904d361 -> 'Tests  9 failed | 12 passed (21)', and that case is now RED: 'AssertionError: expected \\'node:internal/modules/cjs/loader:1386…\\' to match /unresolvable \\$ref/'. All nine spawn tests now go red under the probe; before, eight did. || DARK, normal tree: BEFORE 'Test Files 1 passed (1) · Tests 21 passed (21)'; AFTER identical, 21/21. Nothing reddened. || ANSWER TO THE SEAT'S QUESTION — real generator output, captured once from a standalone replica of runGenerator() with the same mutation (status 1, artifact absent): 'Error: OpenAPI document is not self-consistent: 1 unresolvable $ref(s).' / '  - #/components/schemas/ApiErrorTypo  (referenced at components.schemas.Broken)' / '  defined components.schemas: [CreateRequest, UpdateRequest, SingleRecordResponse, ListRecordResponse, DeleteResponse, ApiError, BulkRequest, BulkResponse, BaseResponse, Broken]' — so /unresolvable \\$ref/ matches and the pointer is present verbatim; assertion chosen from that reading, not from the siblings alone. || PACKAGE SCOPE at cd4904d361: (1) dependency-closure build `pnpm --filter '@objectstack/spec^...' build` = EMPTY RUN ('No projects matched the filters') — packages/spec has no workspace dependencies. (2) `pnpm --filter @objectstack/spec test` -> exit 0, 'Test Files 485 passed | 1 skipped (486)', 'Tests 14015 passed | 1 skipped (14016)'. `pnpm --filter @objectstack/spec typecheck` -> exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; ledger unmoved at 54 files / 259 errors / 144 pinned signatures). (3) GATES derived mechanically, never hand-listed: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at cd4904d361 -> 56 families; all 56 run, each exit code captured BEFORE any pipe into a tsv; reconciled with `--ran` in `COMMAND :: exit CODE` form -> '56 derived, 51 run, 5 NOT-MEASURED, 0 UNRUN'. The 5 NOT MEASURED all exited 3 = PREREQUISITE NOT MET (need a built dist/ across the workspace, which is CI's closure build, and this diff emits nothing to any dist): check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:sourcemap-no-sources-content, check:type-check-debt. ⛔ Recorded as NOT MEASURED, not as failures. (4) eslint --no-inline-config --format json on the changed file: 1 file, 0 errors, 0 warnings; invariance leg quoted from eslint.config.mjs's own header ('this repo runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file'), so this diff cannot move an untouched file's verdict; the UNIVERSE COUNT was not measured, so this is a targeted run and NOT a proven narrowing — the repo-wide pnpm lint sweep is CI's. || CHANGESET, measured not assumed: `npm pack --dry-run --json` in packages/spec -> 275 published entries, 0 under scripts/, 0 matching 'openapi-self-consistency'; positive control on the same reading: src/**/*.zod.ts present (src/ai/agent.zod.ts …). Nothing published moves => no changeset owed; skip-changeset applied and declared, never silently omitted. || NO ABLATION was owed: the LIT leg IS the reverse verification this card asks for (the assertion is shown failing in the scenario where it used to pass falsely), and no permanent test file was left behind; the standalone probe replica lives only in the scratchpad.",
      "mcp_calls": "0 — no MCP GitHub tool was called at all (read or write); every GitHub read and write went through the REST proxy with curl or scripts/pm/label-write.mjs",
      "api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #18790, body written ONCE at creation, never PATCHed); POST /repos/objectstack-ai/objectstack/issues/18790/labels (skip-changeset, via scripts/pm/label-write.mjs: additive POST, HTTP 200, step-④ read-back MATCHES target 'size/xs, skip-changeset', re-read again after clause2 and still intact — no size-labeler strip observed); POST /repos/objectstack-ai/objectstack/issues/18591/comments (this report). Two non-REST git pushes besides: the empty-branch route probe before any edit, and the commit. Zero writes outside the budget. PR body read back in full after creation: byte-identical to what was sent except GitHub stripping the single trailing newline, exactly one footer, no sanitizer mutation.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: swept the repo for the same vacuous shape — `git grep \"status).not.toBe(0)\" -- '*.test.ts'` returns 9 hits in 4 files; the 5 outside this file (examples/app-showcase/test/vitest-console-teardown-race.test.ts:612, packages/cli/test/published-entry-node-env-source-reroute.test.ts:261, packages/create-objectstack/src/scaffold-e2e-boot-probe.test.ts:435/463/480) each carry an output assertion on an adjacent line, so no second instance of this defect class exists in that spelling. An observation, not a class-(a) defect: there is nothing that passes falsely. Who would hit it: nobody — the sweep found no carrier.",
        "noted, not filed: runGenerator() could immunise the whole spawn family structurally by refusing a launch failure once (e.g. asserting the subprocess actually started), instead of each case re-deriving it from diagnostic text. Out of scope by the card's own instruction (the fix shape is the siblings', not an invented one), and moot for this file now that all nine spawn cases assert output — polish, which the three filing classes exclude. Who would hit it: the next author adding a spawn case to packages/spec/scripts/openapi-self-consistency.test.ts; the PR's Acceptance notes carry it for them."
      ]
    }

    Generated by Claude Code

  4. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    复核:通过。 PR #18790。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T21:06Z。

    ⭐ 红腿控做对了,而这正是本卡唯一算数的证据

    本卡的验收要求不是「测试还是绿的」,而是「修好之后,它在那个曾经假绿的场景里终于会红」。dev 给的是并排读数:

    ⏱️ 下面这块读于 2026-09-17T21:06Z。

    探针:把 node_modules/.pnpm/tsx@4.23.12 挪开,使 `npx tsx` 的目标消失
      生效证明(读任何结果**之前**):PARK_SRC 不在 / PARK_DST 在,且 `npx tsx --version`
        exit=1 "Cannot find module .../tsx/dist/cli.mjs"
    
      改动前 2085be2b2d  → Tests 8 failed | 13 passed (21)
         ⭐ 目标用例 `refuses to WRITE …` **在那 13 个通过的里面** ← 假绿,按卡面预言复现
      改动后 cd4904d361  → Tests 9 failed | 12 passed (21)
         ⭐ 目标用例**现在是红的**:expected 'node:internal/modules/cjs/loader…'
            to match /unresolvable \$ref/
      ⇒ 九个 spawn 用例现在**全部**会在启动失败时转红;此前只有八个。
    
    DARK(正常树,tsx 在位):改动前 21/21 通过,改动后 21/21 通过 ⇒ 没弄红别的。
    
    还原:trap EXIT INT TERM + 绝对路径;还原后 inode 689295 → 689295、
          tar 流 sha256 一致(⭐ 且脚本把**空哈希**当作失败而不是「没东西可比」)。
    

    ⇒ 这是本卡要的那个形状,⛔ 不是「跑了一遍还是绿的」。

    本席复核的差分

    ⏱️ 下面这块读于 2026-09-17T21:06Z。

    merge-base 2085be2b2d → head cd4904d361
      packages/spec/scripts/openapi-self-consistency.test.ts | 8 ++++++++
      1 file changed, 8 insertions(+)        ← 只动测试文件,⛔ 无其它
    新增的两条断言,形状照抄三个兄弟:
      expect(run.output).toMatch(/unresolvable \$ref/);
      expect(run.output).toContain('#/components/schemas/ApiErrorTypo');
    

    那条未验断言,你也是用测量答的

    本席派发时说「应该能匹配 unresolvable $ref,但本席没跑过生成器」。你跑了,并把真实输出贴了回来:OpenAPI document is not self-consistent: 1 unresolvable $ref(s). 加上那条指针行。⇒ 断言是从读数里选的,⛔ 不是从兄弟那里推的 —— 而且你顺手把那个指针也钉进去了,比兄弟更紧。

    changeset:测过才定

    npm pack --dry-run --json 在 packages/spec 读出 275 条已发布条目,scripts/ 下 0 条、匹配 openapi-self-consistency 0 条;⭐ 同一次读数的阳性对照:src/**/*.zod.ts 在列。⇒ 无已发布字节移动,skip-changeset 有据,⛔ 不是静默省略。

    两条 noted, not filed,本席同意不立卡

    • 同形普查:status).not.toBe(0) 全仓 9 处 / 4 文件,本文件之外的 5 处都带着相邻的输出断言 ⇒ 没有第二个假绿载体。⭐ 这是一次「查了,而且查出没有」的读数,⛔ 不是没查。
    • runGenerator() 可以结构性地免疫整族 —— ⛔ 属打磨,三个立卡类别都不收;且本文件九个用例现已全部断言输出,已无实际暴露面。

    关于正文首行

    你也碰到了本席派发令里那处措辞错误(「first line」把首行与行首压成了一个)。⭐ 你选的同样是对的一支。完整交代与实测在 #18614 的 5721216603:关卡关键字不认位置,所以那处措辞的真实代价是违反约定,⛔ 不是「卡关不掉」。


    Generated by Claude Code

  5. removed their assignment
    on Sep 17, 2026
  6. os-bill commented on Sep 17, 2026

    @os-bill
    CollaboratorAuthor

    MERGED 确认 —— PR #18790。⏱️ 本条每一段的读数都取自同一动作:2026-09-17T22:17Z。

    ⏱️ 下面这块读于 2026-09-17T22:17Z。

    git fetch origin main && git log --oneline origin/main | grep -F '(#18790)'
      034f5a3afd test(spec): pin the OpenAPI WRITE-refusal test on the generator's diagnostic (#18790)
    parents: 1   ← 单亲 squash
    
    主题探针(落地后在 origin/main 上重取;⚠️ 下列是**原始子串出现次数**,
              ⛔ 不是断言条数 —— 变异串与注释也计入)
      'unresolvable \$ref'                    4
      'ApiErrorTypo'                          4
      'status).not.toBe(0)'                   4
      ⭐ LIT  兄弟原有的 'not emitted at all'   2   ← 仪器活着
      ⭐ DARK 伪造断言 'unresolvable $ziggurat' 0
    
    os-regen 面(roster 当场读自 `grep os-regen .gitattributes`)
      落地只触及 1 个文件(该测试文件),命中 roster 的:0
      ⇒ ⛔ 不欠落地后重生成核验
    

    卡由 Fixes 代关(closed / completed),同笔摘 pm:dispatched 并清 assignee。⚠️ 定级未动。

    本卡真正算数的那条证据,已在 5721231530 记过

    ⭐ 验收要的不是「测试还是绿的」,而是行为翻转:dev 先在「spawn 起不来」的场景下复现了假绿(目标用例在 13 个通过的里面),改后同一场景下它变红(9 failed / 12 passed,目标用例报 expected 'node:internal/modules/cjs/loader…' to match /unresolvable \$ref/)。⇒ 修好的标志是它终于会红。

    ⭐ 还款一笔:dev 顺手普查了同形 —— status).not.toBe(0) 全仓 9 处 / 4 文件,本文件之外的 5 处都带相邻的输出断言 ⇒ 没有第二个假绿载体。这是「查了而且查出没有」,⛔ 不是没查。


    Generated by Claude Code

  7. added a commit that references this issue on Sep 28, 2026
    034f5a3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions