Skip to content

test(lint): loop fixtures 改拼 canonical 的 iteratorVariable 并上 schema pin (#5700) - #6103

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-5700-loop-fixture-iterator-variable
Aug 7, 2026
Merged

test(lint): loop fixtures 改拼 canonical 的 iteratorVariable 并上 schema pin (#5700)#6103
hotlong merged 1 commit into
mainfrom
claude/issue-5700-loop-fixture-iterator-variable

Conversation

@hotlong

@hotlong hotlong commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #5700

test-only,无 changeset(skip-changeset)。文件面仅 packages/lint/src/lint-flow-patterns.test.ts;未动 packages/spec,未动 packages/lint/src/ 下任何规则源。

前提复核(基于 origin/main@be59695,已含 #5693 → PR #6060)

前提新鲜。三处 fixture 仍以 itemVar 绑定循环项:

站点 原行号 出处
loopBodyFlow() 共享 helper 1103 #5383
嵌套 loop 用例 1163 #5383
嵌套 loop 用例 1518 #5482

LoopConfigSchema(packages/spec/src/automation/control-flow.zod.ts:163)是 strictObject,声明键为 iteratorVariable;itemVar 既非声明键、也不在 aliases(那里只登记了 itemVariable,用途是压过 edit-distance 建议器,避免把想要 ITEM 的作者指向 indexVariable)。实测:

=== outer itemVar (main 上的状态)
    success=false
    - code=unrecognized_keys path=[] keys=["itemVar"]
      msg=Unrecognized key(s) on this loop container config: `itemVar`. Until #4001 an
          undeclared key here was dropped silently — ...

region 收集只读 config.body,所以这些 fixture 照常下潜、断言照常通过。规则本身没错,错的是 fixture 描述了一个作者写不出来的 loop —— #4966 在 trigger 描述符上的同类发现,下沉一层。

:518 / :533(#5695 自带的 pin 与 near-miss)本来就正确,不在本单面内,未改动。

改了什么

  1. 三处改拼 itemVariteratorVariable
  2. body 节点补 label(FlowNodeSchema.label 是必填,fix(lint): flow-runas-unscoped 跨区域搜索它的数据节点证据 (#5633) #5695 对自带 fixture 的同样处理);flow lint rules never descend into a loop body — the whole family is blind to nested nodes (8 real inert conditions shipped past flow-inert-node-condition) #5383 嵌套用例的 loop_touchpoints 节点本身也缺 label,一并补上。属于「补声明」而非「重拼」:fixture 本就不是 spec-valid,补齐后它只剩那一处故意埋的缺陷(decision 的惰性 config.condition / reset 的无 filter multi: true)。
  3. 三处 loop config 提成具名常量并各自 pin LoopConfigSchema 全绿。提成常量是为了让 pin 读到 fixture 真正下潜的那个对象,而不是一份会静默漂移的重抄副本。

为什么嵌套用例要各自 pin(实测,不是推断)

FlowNodeSchema.config 声明为 z.record(z.string(), z.unknown()) —— 开放记录。所以外层 parse 会整块接受内层容器的 config:

=== 外层 canonical,内层 loop 写 itemVar
    success=true      ← 外壳 pin 全绿,看不见内层
=== 同一份内层 config 直接 parse
    success=false
    - code=unrecognized_keys keys=["itemVar"]

这条盲区已经作为一条断言写进测试(does NOT see into a nested container's config — the inner pins are load-bearing),免得后来者把顶层一条 pin 误当成整棵树的覆盖。

门槛的选择:full green 还是 key 级

容器键位审计(本单 durable 半边)

按容器 slot 全表清点(FLOW_REGION_SLOTS:loop.body / parallel.branches / try_catch.try / try_catch.catch):

容器 文件内 fixture 数 结论
loop 6 3 处本单修复;loopConfig() @510 与两层嵌套 @622/627 早已 canonical 且实测全绿,未动
parallel 3 审计干净 —— 0 处未声明键;3 处都已加审计断言
try_catch 0 文件内没有该容器的 fixture,如实报告,不为凑数造一个

parallel 三处的 name / nodes / edges,以及 twin_regions 分支边上的 condition / isDefault,实测全部是已声明键。

审计断言有牙:往一个分支塞 bogusKey: 1,断言按名报红 —— 详见下方回归验证第 4 段。

全量测试

pnpm --filter @objectstack/lint test:

 Test Files  61 passed (61)
      Tests  1457 passed | 4 skipped (1461)

基线(改动前,同一 worktree)为 1453 passed / 4 skipped;+4 恰为本 PR 新增的 4 条 it

回归验证(方向先声明,后运行)

预先声明的三条预期:(1) 还原 site 1 → 外壳 pin 报红;(2) 还原 site 2 → 只有它自己的内层 pin 报红,外壳 pin 保持绿(这才证明内层 pin 是承重的,而非冗余);(3) 还原 site 3 → 同理。三条全部命中。

1. 还原 site 1(loopLeadsConfig 改回 itemVar)

× pins loopBodyFlow's `loop` config against LoopConfigSchema
× does NOT see into a nested container's config — the inner pins are load-bearing
 Tests  2 failed | 110 passed (112)

2. 还原 site 2(#5383 嵌套)

× pins that inner loop container against LoopConfigSchema (#5700)
 FAIL  #5383 — flow-inert-node-condition descends into a loop body > pins that inner loop container ...
 Tests  1 failed | 111 passed (112)

只红了内层那一条,外壳 pin 仍然绿 —— 与上面 FlowNodeSchema.config 是开放记录的实测一致。

3. 还原 site 3(#5482 嵌套)

× pins that inner loop container against LoopConfigSchema (#5700)
 FAIL  lintFlowPatterns — unbounded bulk write (#5482) > inside a nested region (#5383 / #5635) > pins that inner loop container ...
 Tests  1 failed | 111 passed (112)

4. 审计断言有牙(往 twin_regions 分支塞未声明键)

× does NOT merge two regions into one bag — a shared node id is not a fan-out
AssertionError: expected [ Array(1) ] to deeply equal []
+   "branches.0: Unrecognized key(s) on this parallel branch: `bogusKey`. ..."
 Tests  1 failed | 111 passed (112)

四处均已还原,末次全量为上面的 61 files / 1457 passed。

其余门禁

门禁 结果
pnpm --filter @objectstack/lint typecheck 干净(tsc --noEmit 无输出)
npx eslint packages/lint/src/lint-flow-patterns.test.ts 干净(exit 0)
node scripts/check-nul-bytes.mjs OK (scanned 5839 tracked text file(s); ... no raw ASCII control bytes)
越过门禁的自扫 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' 无命中

不在本单范围


Generated by Claude Code

…in (#5700)

三处 pre-existing fixture 用 `itemVar` 绑定循环项,而 `LoopConfigSchema` 是
strictObject、声明键为 `iteratorVariable`,`itemVar` 既非声明键也非 alias,会被
报成 `unrecognized_keys`(#4001)。region 收集只读 `config.body`,所以这些 fixture
照常下潜、断言照常通过 —— 规则本身没问题,但 fixture 描述的是作者写不出来的
`loop`,等于把覆盖面讲得比实际强(#4966 在 trigger 描述符上的同类发现,下沉一层)。

改动:
- `loopBodyFlow()` 共享 helper、#5383 嵌套用例、#5482 嵌套用例三处改拼 canonical;
- body 节点补上 `FlowNodeSchema` 必需的 `label`(#5695 对自带 fixture 的同样处理);
- 把三处 loop config 提成具名常量并各自 pin `LoopConfigSchema` 全绿。

嵌套用例为什么要各自 pin(实测,非推断):`FlowNodeSchema.config` 声明为
`z.record(z.string(), z.unknown())` 开放记录,外层 parse 会整块接受内层容器的
config —— 只 pin 外壳时,内层 `loop` 写 `itemVar` 外壳 pin 依然全绿。该盲区已作为
一条断言写进测试,免得后来者把顶层一条 pin 误当成整棵树的覆盖。

顺带的容器键位审计(本单 durable 半边):文件内 3 处 `parallel` fixture 全部只用
已声明键,0 处 `try_catch` fixture;审计断言按 key 级门槛(`refusedKeys`)落,
沿用 `validate-security-posture.test.ts` 已写下的「拒绝的 KEY 与拒绝的 VALUE 是
两回事」判据 —— 全文件 full-`FlowSchema`-green 是 #5700 明确另案的更大改动。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 7, 2026 1:21am

Request Review

@github-actions github-actions Bot added the size/m label Aug 7, 2026
@hotlong hotlong added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 7, 2026 — with Claude
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants