Skip to content

test(spec): 修掉 flow fixture 教的三种跑不通的形状 (#4924) - #5502

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4924-flow-fixture-shapes
Aug 5, 2026
Merged

test(spec): 修掉 flow fixture 教的三种跑不通的形状 (#4924)#5502
os-zhuang merged 2 commits into
mainfrom
claude/issue-4924-flow-fixture-shapes

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4924

背景

packages/spec/src/automation/flow.test.ts 的示范 flow 里,四个节点写着 executor 一个都不读的 config 键。它们全绿,是因为 fixture 断言的是 FlowSchema / FlowNodeSchema,而 FlowNodeSchema.config 按 ADR-0018 刻意是开放的 z.record(z.unknown())(node.type 对插件开放,插件 executor 自带 configSchema)。所以这不是一个会红的测试,是一份会被照抄的教材。

FlowNodeSchema.config 没动 —— 按 issue 明示,它的开放是设计,不是遗漏。

改了什么(四处,各带原地注释)

节点 改前 改后 依据
get_opportunity (get_record) object + recordId objectName + filter: { id: … } + outputVariable: 'opportunity' GetRecordConfigSchema 两个旧键都不声明;object 是 ADR-0087 D2 flow-node-crud-object-alias 在加载期改写的退役拼写,recordId 从来没有 reader。补 outputVariable 是因为下游谓词引用的 opportunity 此前根本没被绑过
check_amount (decision) config.condition 删除,分支落到出边 config.condition 只有 start 节点(触发闸门)会读,其余节点类型上惰性 —— 正是 flow-inert-node-condition(#4414)报的那件事。DecisionConfigSchema 只声明 conditions
auto_approve (update_record) recordId objectName + filter: { id: … } 同上;另外 objectName 是执行期必填,缺了 executor 直接 refuseNode,所以这个节点原本也跑不起来
delete_record 只有 recordId 一个键 objectName + filter: { id: '{item.id}' } 三个里最严重的一个:唯一的「约束」根本不被读 = #3810 的 match-everything delete,穿着一个读起来像约束的键

出边(decision 修复的落点):e3 改成 isDefault: true 的兜底边(与 condition 互斥,同时写会被 flow-default-edge-with-condition 判 error),e4 保留唯一的守卫分支,谓词改写成 bare CEL(opportunity.amount > 100000)—— {…} 模板花括号在 CEL 里是 map 字面量,registerFlow 的表达式校验硬报错,就是 #1491 那个坑。

让它变成一个「会红」的测试

只改 fixture 的话,下一个人照旧可以把 recordId 写回来而测试依然全绿。所以两个 fixture 现在额外把修正后的节点 config 拿去过 executor 运行期真正 parse 的那几份契约(GetRecordConfigSchema / UpdateRecordConfigSchema / DeleteRecordConfigSchema),并断言 decision 无 config、出边恰好一条守卫 + 一条 isDefault、谓词里不含 {

验证

pnpm --filter @objectstack/spec test        →  Test Files 311 passed (311) / Tests 7951 passed (7951)
pnpm --filter @objectstack/spec typecheck   →  tsc --noEmit,无输出
node scripts/check-nul-bytes.mjs            →  OK (5460 tracked text files)

反向验证(方向是事先定好的:恢复旧形状应当变红,因为这些契约是 strictObject,旧键会作为 unknown key 被拒):

  • 三处 CRUD 旧形状全恢复 → flow.test.ts 2 failed(approval flow 断在 GetRecordConfigSchema.safeParse(...).success 为 false;scheduled flow 断在 DeleteRecordConfigSchema)
  • 只把 decision 的 config.condition 放回去 → 1 failed:expected { Object (condition) } to be undefined
  • 只把两条出边放回旧样子 → 1 failed:expected [ { id: 'e3', …(4) }, …(1) ] to have a length of 1 but got 2

三组 pin 各自独立生效,没有一个是靠别的失败遮住的。

关于 changeset

没有加。改动只落在一个 .test.ts 文件里:没有 schema、导出、可授权键或运行时行为的变化,npm 包发的是 dist,消费者拿不到这个文件,CHANGELOG.md 里也无话可说。按 AGENTS.md 的「纯 bug fix 不需要 changeset」,这里连用户可见面都没碰到。

范围外的发现,已单独立 issue

#5500(finding 标签,未指派):同一个文件里还有五类不同缺陷类的形状 —— {节点id.字段} 这套引擎从不绑的输出引用方言、assign_output 因为没有 assignments 包裹而实际创建了两个名叫 variable/value 的变量、loop_records 是 legacy flat-graph loop(所以本 PR 里 delete 节点的 {item.id} 要等它改成 ADR-0031 结构化 body 才真的有值,原地注释已写明)、get_old_records 的字符串 filter(可跑形状需要拍板,没在这里猜)、以及另外三处 brace-CEL 出边条件和两处 object 别名。按 Prime Directive #10 记录,没有在本 PR 里顺手扩范围。


Generated by Claude Code

…4924)

`packages/spec/src/automation/flow.test.ts` demonstrated four flow node
configs whose keys no executor reads. They stayed green because the
fixtures assert `FlowSchema` / `FlowNodeSchema`, and `FlowNodeSchema.config`
is deliberately an open `z.record(z.unknown())` (ADR-0018) — so the file was
teaching material for shapes that cannot run, not a test that could go red.

Corrected, each with an in-place comment saying why:

- `get_record` / `update_record` / `delete_record`: `object` -> `objectName`
  (the ADR-0087 D2 `flow-node-crud-object-alias` spelling), `recordId` ->
  `filter: { id: … }` (CRUD executors locate rows through `filter` only),
  plus the execute-time-required `objectName` the update/delete nodes never
  had. The `delete_record` node had `recordId` as its ONLY key — a
  match-everything delete (#3810) wearing a key that reads like a constraint.
- `decision`: the inert `config.condition` is gone. The key is the trigger
  gate on a `start` node and is read on no other node type
  (`flow-inert-node-condition`, #4414); branching moves to the out-edges —
  one guarded branch plus `isDefault: true` on the fallback.
- the moved predicate is bare CEL (ADR-0032): `{…}` braces parse as a CEL
  map literal and are rejected by `registerFlow` (the #1491 trap).

`FlowNodeSchema.config` is untouched — it is open by design (ADR-0018).

Both fixtures now also parse their corrected node configs against the
per-node-type contracts the executors parse at run time, so re-introducing
any of these shapes fails the test instead of passing it.
@vercel

vercel Bot commented Aug 5, 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 5, 2026 2:34pm

Request Review

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

@os-zhuang os-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/s labels Aug 5, 2026 — with Claude
@github-actions github-actions Bot added the size/s label Aug 5, 2026
@os-zhuang os-zhuang removed the size/s label Aug 5, 2026 — with Claude
@github-actions github-actions Bot added the size/s label Aug 5, 2026
@os-zhuang os-zhuang added domain:spec and removed size/s labels Aug 5, 2026 — with Claude
@github-actions github-actions Bot added the size/s label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31022222345 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (3/3) — 失败步骤: Run this shard's tests(日志不可读,点进 job 看)

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 13 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 5, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 9894a72 Aug 5, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4924-flow-fixture-shapes branch August 5, 2026 16:23
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…s runnable, and pin them (objectstack-ai#5500) (objectstack-ai#5686)

Follows objectstack-ai#4924 / PR objectstack-ai#5502 on the same file. Four of the five classes objectstack-ai#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 objectstack-ai#5500

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
* docs(pm-dispatch): 探活入例行、报告丢失直接验收、座位行自查等五处协议更新

2026-08-05 实测教训回写(维护者指示):

1. step 6 新增「探活」固定动作:完成通知在宿主进程重启下会静默丢失——
   三个在飞 dev 死亡数小时无任何信号;每轮巡检对无产出的 dev 发状态
   询问,「no active task; resumed from transcript」回包即当场复活。
2. step 6 新增「报告丢失直接验收」兜底:PR 全绿 + agent 确认死亡/静默
   ≥2h + 报告未达 ⇒ 直接按 PR 对照 origin/main 验收(objectstack-ai#5550/objectstack-ai#5556 先例)。
3. 座位表协议新增「每轮核对自己的正文行」:协议升级迁移状态可把在任 PM
   记成待认领,叠加惰性回收条款即误回收风险(spec 座位当日实例)。
4. worktree 接手协议前置 SendMessage 复活路径(更便宜、保全上下文),
   接手协议降为 resume 不可用时的后备。
5. step 7 新增两条评审判据:验收判据本身可被 dev 用测量证伪(objectstack-ai#5452 字面
   grep 归零判据不成立,改钉配平不变量);tests/docs-only PR 走
   skip-changeset 标签且 rerun 无效需新 labeled 事件(objectstack-ai#5497/objectstack-ai#5502)。

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

* docs(pm-dispatch): skip-changeset 条目随 objectstack-ai#5625 更新 —— 闸门已实时读标签,教训改记为「载荷 vs 现状」通病标本

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:spec size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec 自己的 flow fixture 教了三种跑不通的形状:CRUD 的 recordId、decision 的 conditionobject(#4001 第一类发现的第七例)

2 participants