test(spec): 让 flow.test.ts 剩下的跑不通 fixture 形状真的能跑,并逐条钉住 (#5500) - #5686
Merged
Conversation
…s runnable, and pin them (#5500) Follows #4924 / PR #5502 on the same file. Four of the five classes #5500 listed were real and are fixed; the first class's stated mechanism did not survive verification and is reported instead of forced. - assignment (`assign_output`): `{ variable, value }` -> `{ assignments: { … } }`. logic-nodes.ts normalizes three shapes and its last branch makes top-level config keys the variable NAMES, so the old config declared two variables literally named `variable` and `value` while `contactId` (declared `isOutput: true`) was never written. Measured end to end before the fix. - loop (`loop_records`): legacy flat-graph loop -> ADR-0031 structured container. With no `config.body` loop-node.ts reads `config.collection` as a bare VARIABLE NAME, so `'{get_old_records.records}'` matched nothing, bound nothing and fell through; the `loop -> delete -> loop` back-edge was ordinary traversal and `{item.id}` referenced a variable no one set. The delete node moves into `config.body` and `iteratorVariable` binds `item`. - get_record (`get_old_records`): string `filter` -> record form. The contract declares `z.record(z.string(), z.unknown())`, so the string failed safeParse outright, and `DAYS_AGO()` is implemented nowhere. The date window is spelled `{90_days_ago}` — a spec date macro whose slot ownership is declared: a known filter token passes through interpolateFilter verbatim for the query engine's resolveFilterTokens to expand, and date-macros.zod.ts names "flow node filters" as a consumer. `limit` added because it selects the `find`/`records` branch the loop needs. - `object` -> `objectName` at the three remaining sites (ADR-0087 D2 `flow-node-crud-object-alias`). - edge conditions: `{…}` template braces -> bare CEL (ADR-0032 §1a). Verified that registerFlow rejects the braced form outright, so those fixtures could not register at all. Class 1 ("the `{<node id>.<field>}` output dialect is never bound") is NOT implemented, because it is false: engine.ts writes every node's `result.output` under `<nodeId>.<key>` and template.ts resolves that flat key, which crud-nodes.ts documents as the default binding. `{create_contact.id}` resolves and is kept verbatim. The real defect at the other cited site was narrower — get_record only emits `records` on the `limit > 1` branch — and is fixed above. Each fix carries a contract assertion, and each was reverse-verified by restoring the broken shape. That caught a phantom in the first draft of the brace pin: ExpressionInputSchema normalizes a bare-string predicate into a `{ dialect, source }` envelope, so asserting `toContain` on the parsed `condition` object passed regardless of the predicate. The pins now read `condition.source`. Refs #5500
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
os-zhuang
marked this pull request as ready for review
August 6, 2026 00:24
os-zhuang
enabled auto-merge
August 6, 2026 00:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5500
承 #4924 / PR #5502 在同一文件上的工作。issue 列的五类里,四类属实并已修;第 1 类的机制说法经实测不成立,按 Prime Directive 6 如实回报而不硬套模板。文件面仅
packages/spec/src/automation/flow.test.ts。行号已按当前
origin/main重新定位(issue 里的行号以 #5502 合入时为准,main 已经移动):object:在:118/:485/:527(issue 写的是 117/433/474),brace-CEL 出边在:978/:1000/:1001。第 1 类:前提被证伪,未按 issue 的分析实现
issue 说「
{节点id.字段}这套输出引用方言,引擎从来不绑」。不成立。service-automation/src/engine.ts:4243-4247对每个节点的result.output逐键写入variables.set(`${node.id}.${key}`, value)。builtin/template.ts:91的resolveToken有一条扁平键兜底(if (variables.has(trimmed)) return variables.get(trimmed);),正好读这个键。builtin/crud-nodes.ts:156-158的模块文档把它写成默认绑定:「Writes the result back to the variable context underoutputVariable(or undernodeId.id/nodeId.recordsby default)」。实测(临时装一个 probe executor 快照变量表,跑完即删):
即
{create_contact.id}能解析,因此:499那个 token 原样保留,只在旁边把机制写清楚。issue 引的另一处
:492'{get_old_records.records}'确实解析不到,但原因窄得多、也不是「方言不绑」:get_record只有在limit > 1时才走find并输出records列表,否则走findOne输出单个record(builtin-node-config.zod.ts:208-209的契约文档明写)。所以修法是给该节点补limit,见下。第 2 类:assignment 缺
assignments包裹 —— 属实logic-nodes.ts:129-132规范化三种形状,最后一条分支是「没有assignments包裹时,顶层 config 键就是变量名」。先证红(实测):改为
config: { assignments: { contactId: '{create_contact.id}' } }。第 3 类:legacy flat-graph loop —— 属实,且比 issue 说的更彻底
loop-node.ts:70-80的 back-compat 分支不只是「不迭代」:它把config.collection当作裸变量名(variables.get(collectionName))而非模板,所以'{get_old_records.records}'连$loopItems都没设上。实测:改成 ADR-0031 结构化容器:
delete_record移入config.body,iteratorVariable: 'item'才是真正绑{item.id}的东西;主图里loop -> delete -> loop的回边随之删除,loop 的普通出边就是 after-loop 续接。第 4 类:字符串
filter—— 别名部分照修,形状部分已测出有定论的答案object:->objectName:机械照修。字符串filter实测直接 safeParse 失败:关于形状:issue 提的候选是
{ created_at: { $lt: '{TODAY() - 90}' } },并担心$lt的 Zod 声明不含 string。实测下来这个担心指错了地方,而且有一个不用猜的更好答案:GetRecordConfigSchema.filter是z.record(z.string(), z.unknown())(builtin-node-config.zod.ts:219),值一律unknown—— 两个候选都 parse 绿,ComparisonOperatorSchema在这条路上根本不参与校验。{90_days_ago}命中DATE_MACRO_PARAM_RE,isKnownFilterToken('90_days_ago') === true,于是interpolateFilter(template.ts:216-235,Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810 的槽位归属移交)把它原样透传给查询引擎的resolveFilterTokens展开;date-macros.zod.ts:26-28明确把「flow node filters」列为该机制的消费者。实测两个候选的落点:
采用 B。这不是猜:它是 spec 为这个槽位声明的方言,两处文档都点名。
另补
limit: 200+outputVariable: 'oldRecords'——limit是让上游产出数组的契约细节(否则 loop 拿到单条 record 会直接报「did not resolve to an array」),不是调参。残留的契约问题已单独立 finding:#5685 ——
ComparisonOperatorSchema的$gt/$gte/$lt/$lte声明为number | date | FieldReference不含 string,而平台自己的日期宏解析器只能产出字符串(filter-tokens.ts:18-19的规范示例就是{ $gte: '2026-01-01' }),一方调用方(lifecycle-service.ts:991、outbox-sweep.ts:160)也一律传.toISOString()。该 schema 零运行期消费者,今天不产生失败,故按 observation-class 记录、未在本 PR 顺手改。第 5 类:
object别名 + brace-CEL 出边 —— 属实三处
object:改objectName:。三处 brace-CEL 出边条件改裸 CEL,先证红(实测registerFlow是硬报错):顺带一提:这条报错本身证明了第 3 类里
collection用花括号是对的 —— 模板槽位要花括号,谓词槽位不要,两者的区别正是这几条钉子要守的东西。钉法与反向验证
每类都按 #5502 在本文件的惯例加了契约断言,并且逐条把坏形状放回去验证确实转红(方向事先预测,全部为「红」):
objectName->objectshould accept node with configassignments->{ variable, value }should accept screen flow for user inputbodyshould accept scheduled flowfilter记录形 -> 原字符串should accept scheduled flowlimitshould accept scheduled flowshould accept conditional edge type/should validate a decision with default and conditional branches反向验证抓到了我自己第一版钉子里的一个幽灵断言,值得记一笔:
ExpressionInputSchema(shared/expression.zod.ts:93)会把裸字符串谓词规范化成{ dialect: 'cel', source }信封,所以对 parse 之后的condition写expect(condition).not.toContain('{')是在对一个对象断言,无论谓词写成什么都恒绿 —— 把花括号放回去时测试照样全绿,才暴露出来。现在两处都改读condition.source,并在注释里写明这个坑。(#5502 原有的那处String(guarded?.condition)读的是未 parse 的原始 fixture,是真断言,未动。)验证
另外对本仓做了同形状普查:
DAYS_AGO别处无出现;其余 brace-CEL 命中点(engine.test.ts:385/391、validate-expressions.test.ts若干)都是故意的负向 fixture(断言必须被拒),正确,未动。变更集
纯测试改动,无用户可见行为变化,未加 changeset(tests-only)。
🤖 Generated with Claude Code
Generated by Claude Code