Found while implementing #5633. Filed unassigned, observation-class (finding, no pm:queue): no shipped behaviour is wrong — this is test fixtures describing a container the schema rejects, so the coverage they advertise is weaker than it reads.
What
packages/lint/src/lint-flow-patterns.test.ts binds the loop item with itemVar in three pre-existing fixtures:
LoopConfigSchema (packages/spec/src/automation/control-flow.zod.ts) is a strictObject, and the declared key is iteratorVariable. itemVar is neither declared nor aliased, so it is reported as an unrecognized_key rather than dropped (#4001):
REJECTED itemVar unrecognized_keys=[["itemVar"]] (the only issue)
PARSES iteratorVariable unrecognized_keys=[]
REJECTED itemVariable unrecognized_keys=[["itemVariable"]] (declared alias -> guidance)
(itemVariable is deliberately registered as an alias so the edit-distance suggester cannot send an author to indexVariable — see the comment above LoopConfigSchema's aliases. itemVar is not in that set.)
Why the tests do not notice
Region collection reads config.body. collectFlowGraphs / regionSlotsOf never look at the item-binding key, so a fixture spelling itemVar descends and gets scanned exactly like a correct one — every assertion in those cases passes for the right reason about the rule, while the fixture describes a loop an author cannot write. Nothing in the suite parses these fixtures, so nothing goes red.
That is the same shape this very file already documents for a trigger descriptor — the #4966 note above the time-relative case, which added expect(TimeRelativeTriggerSchema.safeParse(...).success).toBe(true) precisely because "a fixture the runtime would refuse teaches the wrong shape to every reader of this file". This is that finding one container down.
Scope, and what is already fixed
Fixtures added by #5633 (PR #5695) are already correct: re-spelled to iteratorVariable, given the label that FlowNodeSchema requires on the body nodes, and pinned against LoopConfigSchema in both directions (fixture parses green; the near-miss spelling really is rejected, so the pin has teeth). That PR deliberately did not touch the three pre-existing fixtures — they are not exposed by its change, and re-spelling someone else's fixtures is scope creep on a build-gating PR.
So what remains is exactly those three call sites.
Fix
Mechanical: itemVar to iteratorVariable at the three sites, plus the label the body nodes need if the fixture is to parse fully, plus a LoopConfigSchema pin on the shared loopBodyFlow helper so the whole family cannot rot back. Worth checking in the same pass whether other container fixtures in the flow-lint tests spell undeclared keys on parallel branches or try_catch regions — the audit is one safeParse per fixture and is the durable half of the fix.
Note the flow-level label requirement is a separate and much broader convention in this file: essentially none of its fixtures are full-FlowSchema-green (FlowSchema requires label on the flow and on every node), because the suite passes hand-written raw literals rather than parsed stacks. Bringing the whole file to full-flow-parse-green is a different, larger decision and is not proposed here — the narrow claim is that a key a fixture writes should be a key the schema accepts.
Related
Found while implementing #5633. Filed unassigned, observation-class (
finding, nopm:queue): no shipped behaviour is wrong — this is test fixtures describing a container the schema rejects, so the coverage they advertise is weaker than it reads.What
packages/lint/src/lint-flow-patterns.test.tsbinds the loop item withitemVarin three pre-existing fixtures:loopBodyFlow()— the sharedloop-body helper introduced by flow lint rules never descend into aloopbody — the whole family is blind to nested nodes (8 real inert conditions shipped pastflow-inert-node-condition) #5383, used by most of that PR's nested cases;#5383block;#5482(flow-multi-write-unfiltered) block.LoopConfigSchema(packages/spec/src/automation/control-flow.zod.ts) is astrictObject, and the declared key isiteratorVariable.itemVaris neither declared nor aliased, so it is reported as anunrecognized_keyrather than dropped (#4001):(
itemVariableis deliberately registered as an alias so the edit-distance suggester cannot send an author toindexVariable— see the comment aboveLoopConfigSchema'saliases.itemVaris not in that set.)Why the tests do not notice
Region collection reads
config.body.collectFlowGraphs/regionSlotsOfnever look at the item-binding key, so a fixture spellingitemVardescends and gets scanned exactly like a correct one — every assertion in those cases passes for the right reason about the rule, while the fixture describes aloopan author cannot write. Nothing in the suite parses these fixtures, so nothing goes red.That is the same shape this very file already documents for a trigger descriptor — the
#4966note above the time-relative case, which addedexpect(TimeRelativeTriggerSchema.safeParse(...).success).toBe(true)precisely because "a fixture the runtime would refuse teaches the wrong shape to every reader of this file". This is that finding one container down.Scope, and what is already fixed
Fixtures added by #5633 (PR #5695) are already correct: re-spelled to
iteratorVariable, given thelabelthatFlowNodeSchemarequires on the body nodes, and pinned againstLoopConfigSchemain both directions (fixture parses green; the near-miss spelling really is rejected, so the pin has teeth). That PR deliberately did not touch the three pre-existing fixtures — they are not exposed by its change, and re-spelling someone else's fixtures is scope creep on a build-gating PR.So what remains is exactly those three call sites.
Fix
Mechanical:
itemVartoiteratorVariableat the three sites, plus thelabelthe body nodes need if the fixture is to parse fully, plus aLoopConfigSchemapin on the sharedloopBodyFlowhelper so the whole family cannot rot back. Worth checking in the same pass whether other container fixtures in the flow-lint tests spell undeclared keys onparallelbranches ortry_catchregions — the audit is onesafeParseper fixture and is the durable half of the fix.Note the flow-level
labelrequirement is a separate and much broader convention in this file: essentially none of its fixtures are full-FlowSchema-green (FlowSchemarequireslabelon the flow and on every node), because the suite passes hand-written raw literals rather than parsed stacks. Bringing the whole file to full-flow-parse-green is a different, larger decision and is not proposed here — the narrow claim is that a key a fixture writes should be a key the schema accepts.Related
flow-runas-unscoped(severity error) still searches only top-level nodes — a scheduled flow whose data ops all live in aloopbody passes the build and is refused at run time #5633 / PR fix(lint):flow-runas-unscoped跨区域搜索它的数据节点证据 (#5633) #5695 — where this surfaced; its own fixtures are already correct and pinned.lint-flow-patterns.test.ts的 fixture 教了一个永远绑不上的timeRelative描述符(#4001 第一类发现的第八例) #4966 — the same finding class on a trigger descriptor, in the same test file.loopbody — the whole family is blind to nested nodes (8 real inert conditions shipped pastflow-inert-node-condition) #5383 /multi: true且filter为空的 delete_record / update_record 是「按声明清空整个对象」,authoring 期零诊断 —— #3810 的守卫按「条件被抹掉」判定,不按「条件为空」判定 #5482 — the PRs that introduced the three fixtures.