Skip to content

fix(engine): thread parent cancellation token into sub_workflow children - #31

Merged
senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/675-subworkflow-cancel-token
Aug 12, 2026
Merged

fix(engine): thread parent cancellation token into sub_workflow children#31
senamakel merged 6 commits into
tinyhumansai:mainfrom
oxoxDev:fix/675-subworkflow-cancel-token

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The bug

Cancelling a workflow run doesn't stop a running sub_workflow child. run_sub_workflow always built the child behind a freshly-constructed CancellationToken, so operator cancellation never crossed the sub_workflow boundary — the child kept scheduling node work while the parent wound down, and effectful nodes could run after the operator hit Cancel. Worse, when the child eventually finished it reported its own cancelled outcome as a hard error, falsely failing a run the operator merely stopped.

The fix

  • NodeContext gains an owned token (a clone of the run's cancellation token), handed to executors.
  • run_sub_workflow forwards the parent token through into the child's build_and_run, and the child's own node contexts receive it — so a nested sub_workflow inherits the token transitively at any depth.
  • The cancelled branch is split: when a child winds down under the parent's own token, run_child now emits no output (mirroring the top-level cancelled-node contract) and lets the parent settle as cancelled. A child that reports cancelled without the parent's token being set still errors defensively — so a genuine child-internal failure is not masked.

Tests (T1–T5) + genuineness

Added test(engine) covering the cross-boundary matrix (T1–T5). T1 and T2 fail on the pre-fix engine and pass after — they prove the propagation actually crosses the boundary rather than just exercising the happy path. Transitive depth inheritance is covered.

Honest caveat

Cancellation is boundary-only: settle is bounded by the in-flight node's remainder — an already-running node completes before the child winds down. This is exact parity with how top-level run cancellation already behaves; there is no mid-node preemption here and none is claimed.

Open API-shape question (maintainer's call)

The fix adds a bare token field to NodeContext. If you'd prefer a RunControls { token, … } struct instead, so #617's later interrupt channel can extend it without re-churning the ~40 literal NodeContext { … } construction sites, that's an easy swap — your call. I kept it a bare field to stay minimal, but I'm happy to wrap it.

Related

Downstream consumer: tinyhumansai/opencompany#675 (this crate is vendored there via openhuman). The opencompany-side regression test is the end-to-end proof that cancellation now propagates through the vendored engine.

Summary by CodeRabbit

  • New Features
    • Improved cancellation handling for nested workflows, including transitive cancellation and fan-out execution.
    • Cancelled child workflows now stop cleanly without producing unwanted output.
  • Bug Fixes
    • Parent cancellation is consistently propagated through workflow execution.
    • Independent child cancellations continue to report errors correctly.
  • Chores
    • Updated the package version to 0.6.1.
    • Expanded cancellation-related test coverage.

A sub_workflow node ran its child via run_sub_workflow, which always started
the child behind a freshly-constructed CancellationToken. Cancelling the parent
run therefore never crossed the sub_workflow boundary: the child kept scheduling
node work while the parent wound down, and on the child's completion the node
reported the child's cancelled outcome as a hard error, falsely failing a run
the operator merely stopped.

Carry the run's token to executors on NodeContext (an owned clone), forward it
through run_sub_workflow into the child's build_and_run, and hand it to the
child's own node contexts so a nested sub_workflow propagates it transitively.
When a child winds down under the parent's own token, run_child now emits no
output (mirroring the top-level cancelled-node contract) and lets the parent
settle as cancelled; a child that reports cancelled without the parent's token
being set still errors defensively.
Drive a real run_cancellable over a parent -> sub_workflow(child) graph and
prove the parent's token reaches the child: cancelling mid-flight skips the
child's downstream node (T1), propagates through two nesting levels (T2), leaves
an uncancelled run untouched (T3), short-circuits the sub_workflow node when the
token is pre-cancelled so the child never starts (T4), and keeps the defensive
independent-cancel error arm (T5). The cancel is made deterministic under
parallel load by holding the in-flight node open until the token flips rather
than racing a wall-clock sleep.
Cancellation now propagates into sub_workflow children (previous commits);
publish it as 0.6.1 so the OpenCompany host picks it up within its "0.6" req.
@oxoxDev

oxoxDev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@graycyrus this is the tinyflows-side fix for the sub_workflow cancellation propagation (downstream: tinyhumansai/opencompany#675). Requesting your review — couldn't add you via the reviewer field (no write access from the fork). Note the open API-shape question in the description re: bare token field vs a RunControls { … } struct for #617's interrupt channel.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@oxoxDev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eaebb4fa-9a32-4713-9449-a11be8db4b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 7077a3c and f579915.

📒 Files selected for processing (1)
  • src/nodes/integration/sub_workflow.rs
📝 Walkthrough

Walkthrough

This change adds cancellation tokens to NodeContext, propagates parent cancellation through nested sub-workflows, distinguishes cooperative and independent child cancellation, updates affected test contexts, and changes the package version from 0.6.0 to 0.6.1.

Changes

Nested workflow cancellation

Layer / File(s) Summary
Context and engine propagation
src/nodes/mod.rs, src/engine.rs
NodeContext now stores a cancellation token. Engine execution forwards cloned tokens to node executors and diagnostic resolution.
Sub-workflow cancellation behavior
src/nodes/integration/sub_workflow.rs
Nested workflows propagate parent cancellation. Cancelled child outputs are discarded, while independent child cancellation remains an error. Tests cover direct, transitive, pre-cancelled, and uncancelled execution.
Node context fixture updates and release version
src/nodes/control_flow/*, src/nodes/integration/*, Cargo.toml
Affected test contexts initialize cancellation tokens. The package version is updated to 0.6.1.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NodeContext
  participant Engine
  participant ChildWorkflow
  participant ChildNode
  NodeContext->>Engine: provide parent CancellationToken
  Engine->>ChildWorkflow: forward token to nested execution
  ChildWorkflow->>ChildNode: execute with cloned token
  ChildNode-->>ChildWorkflow: cancellation or result
  ChildWorkflow-->>Engine: empty output or error
Loading

Possibly related issues

  • tinyhumansai/opencompany/675 — The change addresses parent cancellation propagation through NodeContext and nested sub-workflow execution.

Suggested reviewers: senamakel

Poem

A rabbit threads tokens through the flow,
Nested workflows receive them as they go.
Parent cancellation stops the chain,
Cancelled outputs leave no stain.
Independent errors still remain.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: propagating the parent cancellation token into sub-workflow children.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nodes/mod.rs`:
- Around line 42-50: Make the new public NodeContext token field
backward-compatible by avoiding a required field in downstream struct literals,
or update the crate version to 0.7.0 to declare the breaking API change.
Preserve the owned CancellationToken behavior required by nested workflow
execution and cancellation propagation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ef4d8a91-bbf2-41de-afe7-fc08ba2c5a0a

📥 Commits

Reviewing files that changed from the base of the PR and between 3355bbe and 76233d4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • src/engine.rs
  • src/nodes/control_flow/condition.rs
  • src/nodes/control_flow/dedup.rs
  • src/nodes/control_flow/loop_node.rs
  • src/nodes/control_flow/merge.rs
  • src/nodes/control_flow/split_out.rs
  • src/nodes/control_flow/switch.rs
  • src/nodes/control_flow/transform.rs
  • src/nodes/integration/agent.rs
  • src/nodes/integration/code.rs
  • src/nodes/integration/http_request.rs
  • src/nodes/integration/memory.rs
  • src/nodes/integration/output_parser.rs
  • src/nodes/integration/sub_workflow.rs
  • src/nodes/integration/tool_call.rs
  • src/nodes/mod.rs

Comment thread src/nodes/mod.rs
Comment on lines +42 to +50
/// The run's cooperative-cancellation token (see
/// [`crate::engine::CancellationToken`]). An **owned clone** of the run
/// token, not a borrow — an executor that spawns nested engine work (today
/// only [`sub_workflow`](crate::nodes::integration)) must thread a clone
/// into that child run, so a parent cancel winds the whole subtree down at
/// the next node boundary instead of orphaning it. Executors that touch the
/// outside world within a single node need not consult it; the engine
/// already checks it at the node boundary before this node runs.
pub token: CancellationToken,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the module and type are exported by the crate.
rg -nP '^\s*pub(\([^)]*\))?\s+mod\s+nodes\b|^\s*pub\s+use\s+.*\bnodes\b' src Cargo.toml || true
rg -n -C 3 'pub struct NodeContext|NodeContext\s*\{' src

# Confirm the planned release version.
rg -nP '^\s*version\s*=' Cargo.toml

Repository: tinyhumansai/tinyflows

Length of output: 26134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

lib = Path("src/lib.rs").read_text()
nodes = Path("src/nodes/mod.rs").read_text()
cargo = Path("Cargo.toml").read_text()

print("pub_mod_nodes:", bool(re.search(r"(?m)^\s*pub\s+mod\s+nodes\s*;", lib)))
m = re.search(r"(?s)pub struct NodeContext<'a>\s*\{(.*?)\n\}", nodes)
if not m:
    raise SystemExit("NodeContext definition not found")
fields = re.findall(r"(?m)^\s*pub\s+(\w+)\s*:", m.group(1))
print("NodeContext_public_fields:", fields)
print("package_version:", re.search(r'(?m)^\s*version\s*=\s*"([^"]+)"', cargo).group(1))
PY

git diff --unified=3 -- src/nodes/mod.rs Cargo.toml

Repository: tinyhumansai/tinyflows

Length of output: 281


Use a compatible API design or release as 0.7.0.

NodeContext is public through crate::nodes, and the required token field breaks downstream struct literals. Version 0.6.1 cannot contain this API break.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nodes/mod.rs` around lines 42 - 50, Make the new public NodeContext token
field backward-compatible by avoiding a required field in downstream struct
literals, or update the crate version to 0.7.0 to declare the breaking API
change. Preserve the owned CancellationToken behavior required by nested
workflow execution and cancellation propagation.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

             $0.0873 · 104,618 in / 23,426 out · 74,374 cached (71%) · z-ai/glm-5.2, deepseek/deepseek-v4-pro
critique:    $0.0429 · 41,664 in  / 13,751 out · 32,213 cached (77%) · z-ai/glm-5.2, deepseek/deepseek-v4-pro
security:    $0.0269 · 34,227 in  / 5,978 out  · 21,285 cached (62%) · z-ai/glm-5.2
tests:       $0.0080 · 13,345 in  / 1,593 out  · 9,542 cached (72%)  · z-ai/glm-5.2
description: $0.0080 · 13,891 in  / 1,580 out  · 10,190 cached (73%) · z-ai/glm-5.2

#[test]
fn t5_defensive_independent_cancel_arm_is_present() {
let src = include_str!("sub_workflow.rs");
assert!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique likely

T5 source-string guardrail is tautological and cannot detect deletion of the arm

The T5 test uses include_str!("sub_workflow.rs") to assert that the production source still contains the defensive cancel arm, but include_str! pulls in the entire file — including the test module itself. The string "if ctx.token.is_cancelled() {" appears verbatim inside the test's own assert! call, so src.contains(...) is trivially true even if the production arm is deleted. The stated guardrail ("a future refactor cannot delete it") does not actually fire: a refactor that removes the run_child arm while leaving the test untouched would still pass, which is exactly the silent false-completion the test claims to prevent.

[RULE] unchanged ·

run: &Value::Null,
nodes: &nodes,
caps: &caps,
token: crate::engine::CancellationToken::new(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium security uncertain

Propagate the parent cancellation token instead of creating a new one in the l

The diff adds token: crate::engine::CancellationToken::new() to the execution context built inside the loop node's run method. CancellationToken::new() creates a token in the non-cancelled state; it has no link to any parent token that an engine or caller might use to cancel the workflow. Because this is a loop node, the body (or sub-nodes) executed with this context will be effectively uncancellable from the outside. If the loop condition or iteration source is influenced by workflow data or external input, a caller cannot interrupt a runaway loop, creating a denial-of-service path. The loop node should propagate the cancellation token it received from its own execution context rather than minting a new one.

[RULE] Keep the workflow model declarative; no arbitrary embedded scripting—code execution is a sandboxed capability. ·

@tinysweeper

tinysweeper Bot commented Aug 12, 2026

Copy link
Copy Markdown

What this change touches

18 files, +466 -7 across 5 components. It reaches 6 untouched components (60 graph nodes walked).

flowchart LR
  n0["src/nodes/integration<br/>8 files +422 -5<br/>1 finding"]:::flagged
  n1["src<br/>1 file +16 -1"]:::changed
  n2["src/nodes<br/>1 file +14 -0"]:::changed
  n3["src/nodes/control_flow<br/>7 files +13 -0"]:::changed
  n4["root<br/>1 file +1 -1"]:::changed
  n5["src<br/>3 files reached"]:::impacted
  n6["src/caps<br/>2 files reached"]:::impacted
  n7["extension/tests/e2e<br/>1 file reached"]:::impacted
  n8["src/model<br/>1 file reached"]:::impacted
  n9["src/nodes/control_flow<br/>1 file reached"]:::impacted
  n10["src/nodes/integration<br/>1 file reached"]:::impacted
  n10 -->|34 refs| n5
  n9 -->|20 refs| n5
  n10 -->|18 refs| n2
  n10 -->|18 refs| n6
  n9 -->|16 refs| n2
  n9 -->|10 refs| n6
  n10 -->|9 refs| n8
  n10 -->|8 refs| n7
  n9 -->|7 refs| n7
  n9 -->|7 refs| n8
  n2 -->|4 refs| n5
  n5 -->|4 refs| n2
  n5 -->|4 refs| n6
  n2 -->|3 refs| n6
  n5 -->|3 refs| n8
  n6 -->|3 refs| n5
  n2 -->|2 refs| n8
  n2 -->|2 refs| n10
  n2 -->|1 ref| n7
  n2 -->|1 ref| n9
  n6 -->|1 ref| n8
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
src/nodes/integration changed 8 +422 -5 1 (medium)
src changed 1 +16 -1
src/nodes changed 1 +14 -0
src/nodes/control_flow changed 7 +13 -0
(root) changed 1 +1 -1
src reached 3
src/caps reached 2
extension/tests/e2e reached 1
src/model reached 1
src/nodes/control_flow reached 1
src/nodes/integration reached 1
Changed files

src/nodes/integration

  • src/nodes/integration/agent.rs
  • src/nodes/integration/code.rs
  • src/nodes/integration/http_request.rs
  • src/nodes/integration/memory.rs
  • src/nodes/integration/output_parser.rs
  • src/nodes/integration/shell_tests.rs
  • src/nodes/integration/sub_workflow.rs
  • src/nodes/integration/tool_call.rs

src

  • src/engine.rs

src/nodes

  • src/nodes/mod.rs

src/nodes/control_flow

  • src/nodes/control_flow/condition.rs
  • src/nodes/control_flow/dedup.rs
  • src/nodes/control_flow/loop_node.rs
  • src/nodes/control_flow/merge.rs
  • src/nodes/control_flow/split_out.rs
  • src/nodes/control_flow/switch.rs
  • src/nodes/control_flow/transform.rs

(root)

  • Cargo.toml

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. label Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/nodes/integration/shell_tests.rs (1)

110-117: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the ShellScript variant in the path test.

At Line 116, the test checks only the echoed string. MockShell in src/caps/mock.rs:93-109 emits the same stdout for ShellScript::Inline and ShellScript::Path. The test can pass if script_path is incorrectly sent as inline content. Use a runner that matches ShellScript::Path and verifies "scripts/build.sh" before returning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nodes/integration/shell_tests.rs` around lines 110 - 117, Update
a_script_path_reaches_the_host_verbatim_for_validation to use a runner that
matches the received ShellScript::Path variant, verifies its value is
"scripts/build.sh", and only then returns the expected output. Do not rely
solely on stdout, since MockShell emits identical output for ShellScript::Inline
and ShellScript::Path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/nodes/integration/shell_tests.rs`:
- Around line 110-117: Update
a_script_path_reaches_the_host_verbatim_for_validation to use a runner that
matches the received ShellScript::Path variant, verifies its value is
"scripts/build.sh", and only then returns the expected output. Do not rely
solely on stdout, since MockShell emits identical output for ShellScript::Inline
and ShellScript::Path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 061c2a6b-bad5-40bc-a510-5c5952747f3e

📥 Commits

Reviewing files that changed from the base of the PR and between 76233d4 and 7077a3c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • Cargo.toml
  • src/nodes/integration/shell_tests.rs
  • src/nodes/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • Cargo.toml
  • src/nodes/mod.rs

The include_str! check matched its own assertion strings, so deleting the
run_child arm would not have failed the test. Slice at the test module.
@oxoxDev

oxoxDev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressed the review, and one open call for @graycyrus as crate owner.

CI: the Rust SDK failure was a merge-with-main break — main's new shell node (#28) added a NodeContext literal without the new token field. Merged main in and added the field (shell_tests.rs); --all-features is green again.

T5 (tinysweeper — tautological guardrail): valid, fixed in f579915. The include_str! check now slices at mod tests and asserts against the production region only, so deleting the run_child arm actually fails the test instead of matching the assertion's own strings.

loop_node.rs:183 (tinysweeper — propagate the parent token): false positive. That line is inside #[cfg(test)] mod tests (the run_with helper), not the loop node's execute. Loop nodes route to a body port and let the engine drive iteration — they construct no execution NodeContext and mint no token, so there is nothing to propagate. A fresh token in a test helper is correct.

Semver (CodeRabbit — 0.6.1 vs 0.7.0): your call. NodeContext is public, so strictly a new required field is a breaking change and wants 0.7.0. In practice it's engine-constructed — downstream implements NodeExecutor and receives &NodeContext; both hosts (openhuman/opencompany) construct zero NodeContext literals (verified), so nothing downstream breaks. If you'd rather be semver-strict for crates.io consumers, I'll bump to 0.7.0 and update the tinyflows = "0.6" reqs in the two host bumps (openhuman #5520 / opencompany #767) in the same chain. Let me know which you prefer.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking. Approving.

             $0.0903 · 59,619 in / 30,498 out · 43,733 cached (73%) · z-ai/glm-5.2
critique:    $0.0262 · 19,472 in / 8,492 out  · 14,013 cached (72%) · z-ai/glm-5.2
security:    $0.0224 · 11,965 in / 7,886 out  · 8,649 cached (72%)  · z-ai/glm-5.2
tests:       $0.0295 · 13,823 in / 10,627 out · 9,894 cached (72%)  · z-ai/glm-5.2
description: $0.0122 · 14,359 in / 3,493 out  · 11,177 cached (78%) · z-ai/glm-5.2

.split("mod tests")
.next()
.expect("source file has a body before its test module");
assert!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique confident

T5 guardrail cannot detect deletion of the run_child wind-down arm

The T5 test claims to guard both the parent-cancel wind-down arm and the defensive error arm in run_child, but the string check for the wind-down arm is satisfied by a different copy of the same guard in execute, so deleting the run_child guard would not fail the test.

The test checks:

production.contains("if ctx.token.is_cancelled() {")
    && production.contains("run is halted rather than falsely completed"),

The string "if ctx.token.is_cancelled() {" appears in the production region (before mod tests) in two places: once inside execute at the post-try_join_all check:

if ctx.token.is_cancelled() {
    return Ok(NodeOutput::empty());
}

and again inside run_child at the wind-down check:

if ctx.token.is_cancelled() {
    tracing::debug!(
        node = %ctx.node.id,
        "sub_workflow: child wound down under the parent's cancellation; emitting no output"
    );
    return Ok(None);
}

If a future refactor deletes the run_child copy (and its return Ok(None)), the contains check still passes because the execute copy remains. The second half of the assertion ("run is halted rather than falsely completed") is specific to the error arm, but since the two halves are ANDed and the first half is always true regardless of whether the run_child guard exists, the test cannot detect deletion of the wind-down arm it claims to protect.

This was raised as a prior finding and is not fixed in the current diff; the test was added in this PR, so it is this author's concern.

[RULE] Record design decisions in local/docs/11-decisions.md. ·

@senamakel
senamakel merged commit 4357473 into tinyhumansai:main Aug 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants