Skip to content

feat(workflow): loop container canvas and run details by round - #184

Merged
EricWvi merged 9 commits into
ora-space:mainfrom
zhangxinhao:feat/688-loop-canvas-round-view
Oct 26, 2026
Merged

EricWvi merged 9 commits into
ora-space:mainfrom
zhangxinhao:feat/688-loop-canvas-round-view

Conversation

@zhangxinhao

@zhangxinhao zhangxinhao commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Refs ora-space/circus#688

Based on main after #179 (ecf0d23). Six commits, frontend only: no backend files, no migrations, no change to the node_states shape.

What changes

Loop container in the editor

  • A Loop is drawn as a frame (LoopNodeFrame), not a 230px card. The frame's size is authored and is mirrored onto initialWidth/initialHeight when you use the corner resize or when expandParent grows it. It is never smaller than its measured members, plus padding.
  • Clicking an output port of a body member opens an append menu. Each Condition case and ELSE has its own port. insertLoopMember places the new node one column to the right of its source and keeps it clear of siblings. It writes both parentId and containerId, wires the new node from the same port and grows the Loop. The menu offers agent, condition, aggregator and loopExit. It never offers Output, because the publish gate rejects that with loop_body_output_unsupported.
  • The inspector's "add early exit" action uses the same seam, after the right-most member. Previously it placed the exit at a fixed coordinate.
  • Deleting a Loop removes its body and edges in one undoable step. An outer node dropped onto a Loop frame returns to where it started.

Connection scope gate
isValidWorkflowConnection now resolves each endpoint's scope with the backend rule: a non-empty containerId wins, otherwise parentId. It refuses an edge out of a loopExit, an edge across scopes (top level, a loop body, an iteration region), and an iteration entry that targets anything outside its own region. The canvas also passes the gate to React Flow, so a port does not light up for an illegal target while you drag.

Run details by round (loop ADR D5, iteration ADR D5)

  • run/run-rounds.ts is the only place that projects a container's round document into rounds. An archived index wins over the top-level copy of the final round, so that round appears once. A member that has no state in a round is inactive; it never borrows a state from another round. A malformed document falls back to the flat view.
  • Loop and Iteration frames show a round summary. Selecting a container lists its rounds with their outcomes. Picking a round shows that round's members (status, output, error), plus the item and ledger error for an Iteration, and repaints the members on the canvas. A selected member gets the same round switch. A container you have not picked a round for follows its latest round.
  • Outcomes come only from committed facts. A successful Loop does not persist why it ended, so:
    • a loopExit that succeeded is shown as an early exit;
    • a Loop that ended before the ceiling, or at the ceiling under fail, is shown as "condition met";
    • a Loop at the ceiling under succeed is shown only as "completed".
  • When a Loop's round document records a success reason (condition_satisfied / max_iterations / loop_exit, as ADR D3/D4 name them), the view uses it, so "rounds used up" can be told apart from "condition met" at the ceiling. The approved D1 shape does not carry the field yet; adding it on the backend is tracked separately.
  • The session entry that D5 mentions is not offered. node_states carries no session id; the session, tool and file evidence for workflow nodes is tracked separately, and D5's session entry is recorded as covered by that work.

ADR status

This PR implements the UI parts of decisions/cloud/workflow/20261025-loop-and-loop-exit-execution.md and decisions/cloud/workflow/20261025-iteration-foreach-execution.md, with the round view (D5) apart from the session entry. It does not touch 20261025-variable-aggregator-execution.md. All three ADRs stay approved: real-stack acceptance is still outstanding, and the specs updates are tracked by separate runs.

Verification

  • npm run check (format, lint, typecheck, 1429 tests with coverage, module docs, exported docs, knip, jscpd, build) passes on this head; check:modules --base upstream/main passes.
  • New tests:
    • runtime/loop-graph.test.ts: placement, avoiding siblings, both ownership keys, edge port, growth, refused sources.
    • runtime/connection-validation.test.ts: table of accepted and rejected scope edges.
    • run/run-rounds.test.ts: the ADR D1 example, the final round both archived and at top level, early exit, ceiling under fail and succeed, cancellation, size-limit failure, the iteration ledger under continue and fail, an empty source, malformed documents.
    • editor/loop-node-frame.test.tsx: frame, port menu contents, read-only canvas, cascade delete.
    • Two run-detail page tests for the round summary and round switching.
  • Real stack: built from this branch (before the rebase, same frontend) on a full Ora cluster. Fast-node runs for loop (until met, exhausted under fail and succeed, early exit), iteration (all succeed, fail, continue, over 100 items, empty source) and aggregator (first assigned in declared order, no match) all finished as the ADRs say, and their run pages showed the round summaries and per-round members correctly. One cross-line issue was found and is being fixed separately: the publish gate rejects the iteration-entry edge that the editor (and desktop) write.
  • Checked in headless WebKit with a harness page: a new loop holds its starter agent; a body port opens the menu; inserting an early exit wires it and grows the frame; the run view summary reads "第 2 / 5 轮 · 早退"; switching to round 1 shows the exit as not reached.

The canvas only refused self-loops and duplicates, so an author could draw
edges the publish gate always rejects: out of a loopExit, from the top level
into a loop body or iteration region, between two containers, or from an
iteration's internal start out of its region. The gate now resolves each
endpoint's scope with the backend's rule (non-empty containerId, else
parentId), counting only owners that are a loop or iteration in the graph,
and refuses those edges at the gesture.
… ports

A Loop rendered as an ordinary 230px card, so its body members spilled out of
it and the only way to add a member was the inspector's early-exit button,
which dropped the node on a fixed coordinate.

- LoopNodeFrame draws the Loop at its authored size (grown by expandParent or
  the corner resize, both mirrored onto initialWidth/initialHeight), with
  outer ports on the title strip and the round ceiling.
- Every output port of a body member, each Condition case and ELSE included,
  opens the append menu on click. insertLoopMember places the new node one
  column right of its source, clear of siblings, owned through both keys,
  wired from the same port, and grows the Loop. The menu offers agent,
  condition, aggregator and loopExit; never Output, which the publish gate
  refuses inside a loop body.
- The inspector's early-exit action uses the same seam after the right-most
  member, so the exit is wired and placed instead of stacked on a constant.
- Deleting a Loop removes its body and edges in one undoable step; outer
  nodes dropped onto a Loop frame return to where they started.
- The canvas passes the scope gate to React Flow so illegal targets do not
  light up while dragging a connection.
The run viewer decoded node_states row by row and dropped the `loop` and
`iteration` documents, so a container's rounds were invisible and the final
round's members, kept both archived and at the top level, could only be read
once by accident.

parseRunContainers turns each container's round document into a list of
rounds with every member's state, and is the single place the archive and
the top-level active round are de-duplicated: an archived round index wins,
members missing from a round are inactive, and nothing is borrowed from
another round. Outcomes come only from committed facts. A successful loop
does not persist why it ended, so a succeeded loopExit reads as an early
exit, ending before the ceiling (or at it under `fail`) as the condition met,
and the ceiling under `succeed` only as completed. A malformed document
leaves the container to the flat view.
- Loop and Iteration frames on the run canvas carry a round summary:
  "round 2 / 5 · exited early", or "3 / 5 items · 1 failed".
- Selecting a container lists its rounds with their outcomes; picking one
  shows that round's members with status, output and error, plus the item
  and ledger error for an Iteration, and repaints the members on the canvas.
- Selecting a member offers the same round switch for its container.
- A container the reader has not picked a round for follows its latest
  round, so a running loop keeps showing the round under way.
- The run canvas draws a Loop with the editor's frame at its authored size.

The session entry the ADRs mention is not offered: node_states carries no
session id, and session evidence for workflow nodes is tracked separately.
The palette group's default box is 300px tall, but Cloud's agent card is
taller than desktop's, and React Flow does not run expandParent on a member's
first measurement. A new loop therefore clamped its starter agent into a box
too small for it. The frame now grows to at least the box its measured
members need (with the same padding insertion uses), so it always holds them.
Loop ADR D3/D4 name a success reason (condition_satisfied, max_iterations,
loop_exit) that the approved D1 shape does not yet carry. When a loop's round
document records one, the run view uses it, which also tells "rounds used up"
apart from "condition met" at the ceiling under `succeed`. Without it the view
keeps inferring from committed facts; an unknown value is ignored.
@zhangxinhao
zhangxinhao force-pushed the feat/688-loop-canvas-round-view branch from d27158a to ef70ccc Compare October 26, 2026 13:07
@zhangxinhao
zhangxinhao marked this pull request as ready for review October 26, 2026 13:07

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次审查的是 head ef70ccc(基于 upstream main ecf0d23,6 个提交,只改前端)。

我对照了 loop / iteration 两份 ADR(D1、D3–D6 / D1、D2、D5),Go 侧的 validateWorkflowContainers、workflowContainerID、advanceWorkflowLoop、archiveWorkflowLoopRound、settleWorkflowIterationRound、rejectWorkflowStateOverflow,以及 desktop 的 connection-validation.ts、workflow-iteration-containment.ts、workflow-iteration-graph.ts。本地跑了 npx vitest run(本 PR 涉及的 8 个测试文件,116 个用例全部通过)和 npm run typecheck(通过)。

应修

1. Loop 框按成员实际高度放大后,只改了画出来的大小,没有写回 initialWidth/initialHeight;放置拒绝和拖入弹回仍按旧尺寸判断

  • 位置:frontend/src/features/workflows/editor/loop-node-frame.tsx:90-95(框的显示尺寸取 max(authored, width, contentBox)),对比 runtime/loop-graph.ts:133 的 loopFrameSize(只读 initialWidth ?? width),它被 runtime/iteration-containment.ts:51(拖动后弹回)、editor/workflow-iteration-region.ts:166(从节点面板拖放时拒绝落在框内)、run/run-overview-canvas.tsx:240(运行页框尺寸)使用。
  • 场景:新建一个 Loop。工厂写入 initialHeight = 300,起始 agent 在 y=145。按 c6f6448 的提交说明,Cloud 的 agent 卡片比 300 的框能容纳的更高,所以 contentBoxSelector 把框画到 300 以上,例如 360。applyIterationFrameResize 只在 resizing 或 setAttributes(expandParent)时写回尺寸,普通测量不会写回,提交说明也写了 expandParent 不会在首次测量时触发,所以 initialHeight 一直是 300。这时:
    • 从节点面板把一个 Agent 拖放到框的下沿(y 在 300 到 360 之间):workflowPointInsideIterationFrame 按 300 判断,认为不在框内,于是在顶层建出这个节点。它的 zIndex 高于框,画出来就在 Loop 框里面,看起来像 body 成员,但文档里它是顶层节点。用户再从 body 成员往它连线时,scope 检查会直接拒绝,界面上没有任何提示。
    • 把一个顶层节点拖到同一块区域,applyIterationDragRules 同样按 300 判断,不会把它弹回原位。
    • 运行页给 Loop 节点设了 style = 620×300,但框按内容画到 360,框会超出 React Flow 节点的范围,点击区域和 fitView 用的都是 300。
  • 为什么要修:PR 描述里写的是“frame 尺寸在 resize 或 expandParent 时写回 initialWidth/initialHeight,并且不小于成员实际尺寸”。现在后一半只在显示层成立。画布代码自己的约定是“画布不能显示出文档里没有的成员关系”(dropAtClientPosition 的注释),这里正好违反了。修法可以二选一:成员测量结果超过 authored 尺寸时,按 grow 写回一次 initialWidth/initialHeight(和 expandParent 走同一条路径);或者让 loopFrameSize 的调用方在有 measured 时取 max(authored, measured)(desktop 的 containment 就是 measured ?? width ?? initialWidth)。另外请补一条测试:现在 loop-node-frame.test.tsx 只断言了 620×300,把 c6f6448 的逻辑整段删掉这条测试仍然通过。

建议

  1. Iteration 最后一轮被标成“失败”的条件可以更准确(run/run-rounds.ts:252)。现在的条件是“最后一条归档的账本条目是 failed,且容器状态是 failed”。在 continue 策略下,被吸收的失败项之后如果容器又因为别的原因失败(例如 iteration_invalid_round,或下一项开始时体积超限、此时还没有任何成员状态),这一项会从“失败(已继续)”被改显示成“失败”,而真正导致失败的下一项不出现在轮次列表里。iteration 文档里有 strategy,失败轮在 fail 策略下 round 不递增,所以可以改成“strategy === 'fail' 且 last.index === round”才标 failed。这个窗口比较窄,不阻塞合并。Loop 也有同类情况:round 已加一、新一轮还没有成员状态时容器失败或取消,appendActiveRound 不会补出这一轮,上一轮(实际是“进入下一轮”)会被 finalLoopOutcome 标成失败或取消。按现在调度器的写法,这个状态基本不会被提交,可以只加一句注释说明。
  2. 成功原因字段的位置需要和后端约定(run/run-rounds.ts:206)。前端读的是 nodeStates[loopId].loop.reason,ADR D3/D4 只写了 reason = "max_iterations" / "loop_exit",没有规定放在哪个字段。请在跟踪后端补字段的那个 issue 里写明是 loop.reason,免得后端写到别处(例如 output 里,那会和 outputs 的键冲突)以后前端一直读不到,界面又不会报错。
  3. Iteration 轮次详情没有显示账本条目的 output(run/run-round-views.tsx:187-199)。iteration ADR D5 要求展开后按 entries 展示每项的 item、status、output、error。现在显示了 item、error(status 通过轮次结果标签表达),但这一项收集到的值(entry.output)没有显示,只能从成员输出里去找。建议在 item 下面加一行。
  4. Inspector 的“添加早退”在最右侧成员是 Condition 时会接在 ELSE 上(runtime/loop-graph.ts:73,loopExitInsertion 传的 sourceHandle 是 null,后端把无 handle 的出边当作 else)。这是可以接受的默认,但用户不会知道接在了哪个分支上。可以考虑在这种情况下跳过 Condition,或者在提示里说明接在 ELSE 上。
  5. 拖入弹回和面板放置拒绝在 Loop 上没有测试。PR 描述里写了“外部节点放到 Loop 框上会回到原位”,但 iteration-containment.test.ts 没有改,Loop 分支(iteration-containment.ts:49-53)和 workflowPointInsideIterationFrame 的 Loop 分支都没有用例覆盖,删掉这两段现有测试仍然全部通过。建议各补一条。

已核对,没有问题

  • 连接 scope 检查(runtime/connection-validation.ts)与 Go validateWorkflowContainers 和 desktop isValidWorkflowConnection 逐项对照:
    • 有效容器取 containerId(非空)否则 parentId,与 workflowContainerID 一致;containerId 和 parentId 不一致时以 containerId 为准,与后端一致。
    • loopExit 的任何出边都拒绝,对应 loop_exit_has_outgoing_edge,desktop 相同。
    • 同一容器内成员之间允许;成员到顶层、顶层到成员、不同容器的成员之间、容器节点到自己的成员、成员到自己的容器节点都拒绝,对应 loop_cross_container_edge,desktop 相同。
    • iteration 入口边(source=iteration、sourceHandle="iteration-entry"、目标是它自己的成员)允许;入口 handle 连到区域外、连到别的容器成员、或者由非 iteration 节点发出都拒绝;iteration 的普通出口连到自己的成员也拒绝。前端规则已经符合后端修复之后的预期,desktop 规则相同。
    • 嵌套容器:后端用 loop_nested_composite_unsupported 拒绝,前端的菜单和面板都不提供把 loop / iteration / subflow 放进 body 的入口。
    • 所属容器指向不存在的节点或非容器节点时,前端当作顶层处理,后端会多报一条 loop_cross_container_edge。但这种节点本身已经会触发 loop_member_container_mismatch,发布结果不受影响,所以不算分歧。
    • 测试表的效果:删掉 loopExit 检查、iteration-entry 分支、或“只认 loop/iteration 作为所属容器”这三段中的任何一段,都至少有一个用例会失败。
  • Loop 成员追加菜单:只提供 agent、condition、aggregator、loopExit。后端在 body 里拒绝的种类(output → loop_body_output_unsupported,loop/iteration/subflow → loop_nested_composite_unsupported,多余的 start → loop_body_start_count)都不在菜单里。面板拖放到框内会被拒绝(尺寸问题见应修 1),所以也没有从面板加进 body 的路径。output 和 loopExit 节点没有追加入口。
  • 轮次投影(run/run-rounds.ts),对照 Go 写入的实际形状:
    • Loop 的 round 从 1 开始,iteration 从 0 开始;completed[].index 与之对应,ledger 第 r 项就是第 r 轮。
    • 去重:最后一轮既在 completed 里又留在顶层时(finishWorkflowLoop 用 keep=true 归档,iteration 结束时不清顶层成员),只显示归档那一份。删掉 appendActiveRound 里的去重判断,“final round once”这条测试会失败。
    • 未执行的成员:Go 归档前用 workflowInactiveStates 写成 inactive,前端对缺失的成员也补 inactive,不会拿别的轮的状态来填。
    • Loop 中途失败(body 失败、耗尽且策略为 fail):Go 先归档再失败,前端显示为最后一条归档轮“失败”。取值类失败(loop_value_unset 等)不归档,前端从顶层成员拼出活动轮并标为失败。取消:活动轮标为取消,已归档的轮保持原样。体积超限:rejectWorkflowStateOverflow 回退到上一次提交的状态再标失败,前端按上一次提交的状态显示,原因显示在 inspector 里。
    • loopExit 早退:advanceWorkflowLoop 在判断 until 之前先判断 loopExit,前端也先看 loopExit 是否成功,顺序一致。到达上限且策略为 succeed、又没有记录原因时显示“已完成”(这是已经定下的处理,这里不再提)。
    • Iteration:continue 吸收的失败显示为“失败(已继续)”;fail 策略下失败项是最后一轮且显示为“失败”;空数组时 completed 和 ledger 都是空的,前端显示“没有执行任何一轮”;第 0 轮未归档时从顶层成员读出活动轮,容器在运行但还没有成员时也会显示一个空的活动轮。
    • 续跑:resumeWorkflowRun 目前固定返回 501,不会出现续跑后的轮次文档,不需要处理。
    • 成员 retrying 状态在 parseRunNodeStates 里能识别,活动轮显示为进行中。
  • 删除 Loop:resolveIterationDeletionCascade 通过 parentId 和 containerId 两个字段带走成员和相关边,作为一次撤销操作,不弹确认框;iteration 的确认框只统计 iteration 成员数。加载时 workflowContainerNodes 会按 containerId 补上 parentId,desktop 导入的 Loop 成员也能被带走。
  • Loop 尺寸写回:手动拖角调整尺寸会写回 initialWidth/initialHeight;expandParent 放大只会让尺寸变大不会变小;iteration 框不会被 expandParent 改尺寸(有测试)。新成员的 extent 和 expandParent 不会被保存进图里(有测试)。
  • desktop 往返:Loop 成员同时写 parentId 和 containerId,与 desktop validation.ts 要求的 parentId === containerId 一致;尺寸用 initialWidth/initialHeight,与 desktop 读取的字段相同。
  • insertLoopMember 会校验源节点确实属于这个 Loop,否则原样返回;同一端口的第二个分支会被放到第一个下面,不会重叠(有测试)。

The frame grows to hold its measured members, but drop refusal, drag
containment and the run page still used the authored 300px box. A palette
drop or a dragged node below that edge landed at the top level while being
drawn inside the loop, and edges from body members to it were then refused
without explanation.

loopFrameSize now widens the authored box to what React Flow measured, as
desktop's containment does, so every geometric decision uses the box the
author sees. The members' needed box is a tested pure function
(loopContentBox), and the run page lets React Flow measure the loop frame
instead of pinning it to the authored size.
The inspector action appended the exit after the right-most body member
without naming a port, so a condition there got the exit on its ELSE branch
and the author could not tell which branch now ended the round. The action
now appends after the right-most single-port member; exits on a branch are
added from that branch's own port.
…values

Under `continue`, an absorbed item was shown as failed when the container
later failed for another reason. An item now reads as failed only under
`fail` and only when it is the round the iteration stopped at. Iteration
round details also show the value the item collected, which iteration ADR D5
lists next to the item, status and error. The loop's analogous window is
documented: the scheduler commits the archive and the next round's first
dispatch together.

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次复审的是 head 9804b7a,对比 ef70ccc(新增 3 个提交:d42b375、7ff2202、9804b7a,没有改写历史)。

本地完整跑了一遍 npm run check(格式、lint、类型检查、162 个测试文件 1438 个用例、模块文档、knip、jscpd、构建),全部通过。下面提到的“删掉逻辑后测试会失败”,都是我临时改代码实际跑出来的,跑完已经恢复,worktree 是干净的。

上一轮的应修 1:已修复

现在 loopFrameSize(runtime/loop-graph.ts)取 authored 尺寸和 React Flow 测得尺寸(measured)中较大的一个,和 desktop containment 的 measured ?? width ?? initialWidth 是同一个思路。下面几处都改为使用这个尺寸:

  • 面板放置拒绝:workflowPointInsideIterationFrame 读的是 graphNodes。draft 在 reduceNodeChanges 里通过 applyNodeChanges 应用 React Flow 的 dimensions 变化,toDraftNode 不会删 measured,所以 graphNodes 带有测得尺寸。保存时 toDefinitionNode 只保留 initialWidth/initialHeight,历史快照也会删掉 measured,测得尺寸不会被写进图文档。
  • 拖入弹回:applyIterationDragRules 用的是 getNodes(),也带 measured。
  • 插入成员后放大:grownLoop 以 loopFrameSize 为下限。只要在 Loop 里插入一个成员,当前画出来的尺寸就会写回 initialWidth/initialHeight。
  • 运行页:不再把 Loop 节点固定为 authored 尺寸,改由 React Flow 测量框实际画出来的大小。

验证结果:

  • 把 loopFrameSize 里的 measured 两项改成 0 之后,有 3 条用例失败:新增的面板放置用例(workflow-iteration-region.test.ts,点在 y=340、authored 300、测得 363)、新增的拖入弹回用例(iteration-containment.test.ts),以及 loopFrameSize 自己的用例。作者说“删掉逻辑测试会失败”属实。
  • 首次测量之前,measured 是 undefined,按 0 处理,结果就是 authored 尺寸,和修改前的行为一样。不会因为还没测量而把框缩小,也不会错误拒绝合法的放置。
  • 测得尺寸偏小时(测试里 500×200 对 authored 620×300)会被 max 忽略,框不会缩小(有测试)。

新的应修

没有。

建议

  1. 成员内容变小后,Loop 框在本次会话里缩不回去(editor/loop-node-frame.tsx:70-76,结合新的 loopFrameSize)。框的显示尺寸是 max(MIN, loopFrameSize(getNode(id)), width, contentBox),而 loopFrameSize 现在包含 measured,measured 又是框上一次画出来的 DOM 大小。所以框一旦因为某个成员变大,之后即使把这个成员删掉,或者 Condition 删了几个分支变矮,框仍然保持原来的大小:DOM 没变,React Flow 再测出来还是同一个值。手动拖角缩小不受影响,因为拖动过程中 measured 会被改成拖动的尺寸。刷新页面后恢复正常,因为 measured 没有持久化。影响只是显示上框偏大,放置拒绝和弹回用的尺寸与画出来的一致,不会出错。如果要处理,可以让框自身的显示尺寸只看 max(MIN, authored, contentBox),measured 只在放置、弹回、插入这几处判断里使用。
  2. “添加早退”在 body 里只剩 start 和 Condition 时会接在 start 后面(runtime/loop-graph.ts 的 loopExitInsertion)。start 没有被排除,当它是最右侧的单出口成员时,早退会被接成 start → loopExit。调度器会优先派发已就绪的 loopExit,结果每次都在第 1 轮开头就早退,body 里其他成员都记为 inactive,图能发布但实际什么都不做。没有成员(连 start 都没有)时返回 null,点按钮后什么都不发生,也没有提示。这两种情况都是 inspector 按钮的边角用法,建议在这两种情况下不提供按钮,或者给出提示(例如“请从分支的端口添加早退”)。

已核对,没有问题

  • 9804b7a,iteration 失败标记:条件改为“最后一条账本条目是 failed,strategy === 'fail',且 last.index === round”。对照 Go 代码:fail 策略下 settleWorkflowIterationRound 记失败后直接返回、不递增 round,紧接着 failWorkflowIteration,所以只有导致停止的那一项满足这个条件。continue 下被吸收的项之后 round 已经加一,不会被改标。其他情况:
    • 账本为空时 last 不存在,直接跳过。
    • 取消:Go 在 fail 策略下记失败和标记容器失败是在同一次处理里完成的,不会出现“失败项 + 容器已取消”的组合。即使出现,把那一项标成失败也是事实。
    • 续跑:接口仍固定返回 501,不需要处理。
    • strategy 字段 Go 每次都会写(进入时缺省值写成 fail)。
    • 测试有效:删掉新条件后,“continue 下被吸收的项保持被吸收”这条用例失败。
  • 9804b7a,appendActiveRound 的注释:内容和调度器行为一致,归档和下一轮的第一次派发在同一次提交里完成。
  • 9804b7a,收集值显示:只在账本条目是 succeeded 时显示“本项收集到”,失败项显示错误、不显示收集值。新的 run-round-views.test.tsx 覆盖了这两种情况,删掉这段显示后第一条用例失败。
  • 7ff2202,跳过 Condition:有 Condition 在最右侧时,早退改接在最右侧的单出口成员后面。把排除 Condition 的条件改成 true 后,新用例失败。
  • 运行页去掉固定尺寸:框的 DOM 大小由 LoopNodeFrame 的内层 div 决定,至少 480×260,并且不小于 authored 尺寸和成员需要的尺寸。React Flow 按 DOM 测量,成员的 extent: 'parent' 依据的是测得的父节点尺寸。OverviewFitController 在下一帧调用 fitView,和普通卡片一样依赖测量结果。iteration 仍然是固定尺寸,没有受影响。
  • loopContentBox 抽成纯函数:成员判断改为 containerId(非空)否则 parentId,与后端规则一致;原来只看 parentId,由于加载时会按 containerId 补 parentId,两者结果相同。有直接测试(628×363 的计算,以及容器外的节点会被忽略)。

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants