Skip to content

🧪 test(flows): the authoring model end to end over the durable engine - #194

Merged
roninjin10 merged 17 commits into
mainfrom
e2e-suite
Aug 13, 2026
Merged

🧪 test(flows): the authoring model end to end over the durable engine#194
roninjin10 merged 17 commits into
mainfrom
e2e-suite

Conversation

@roninjin10

Copy link
Copy Markdown
Contributor

Summary

The end-to-end suite over the durable engine: packages/flows/test/e2e-authoring.test.ts exercises the whole decided API through real @smthrs/* resolution — all ten scenarios:

  1. counter trampoline: multi-round lineage, parent_run_id chain, ordinals, final value
  2. crash/resume mid-lineage: at-most-once, no double effects
  3. branch both directions on real values
  4. planned-value trap DX (GraphBuildError naming the node)
  5. recursion_requires_boundary
  6. .child(): real child execution, parent edge, suspension, replay
  7. Sleep park/wake · 8. WaitFor park/wake
  8. maxRounds → typed terminal failure · Node.catch arms
  9. Graph.buildPlan.compilePlanStorePlanDiff round-over-round growth

No source changes — tests only.

Depends-on: #193

Verification

  • e2e 20 tests; full packages/flows 3 files / 171 tests at 100% coverage (independently re-verified); conformance 117
  • Cross-checked suites: flow 20/212 · plan 6/85 · engine 22/165 · engine-store 73/481; whole-repo check + lint green

…Error

The authoring half of the plan: seven closure-free node variants (Succeed,
All, Map, AndThen, FlowCall, ActivityCall, Branch) with digested deferred
functions in out-of-band tables, the Planned<T> branded placeholder whose
proxy throws GraphBuildError on any computation (toPrimitive, valueOf,
toString, toJSON, application), and the GraphBuildError vocabulary.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit 64624ac)
Activity.make gains a string-first overload: a named, schema-typed atom
(payload/success/error/tier/idempotencyKey) whose implementation attaches
separately via .toLayer(execute) through the existing registration and
activity machinery, and whose .call(payload) records an ActivityCall node
for plans. The inline options form is unchanged.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit b883814)
…otations

Flow.make accepts an optional plan-time body (required once the migration
completes), Flow.prototype.call records a FlowCall node, the trampoline
outcome vocabulary lands as pure data (Flow.done / flow.to / Flow.park over
the existing Waiting reasons), and Capabilities / EffectsDeclaration /
Placement join the annotation keys.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
Docs: docs/specs/Concepts/Trampoline Loops.md
(cherry picked from commit ad7df62)
Evaluates a body against its real payload with strict Planned placeholders
for step results, expands branch arms as static topology, splices inline
flow calls with capability intersection, rejects recursive calls with
recursion_requires_boundary, reads effects/capabilities/placement from the
authoring annotations, and emits NodeDrafts that Plan.compile keys directly.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit f8bf150)
Interpreter.layer(flow) registers a generic handler over the FlowRuntime
port: Graph.build against the real payload, dependency-order driving of the
compiled drafts, activity calls through the existing persistence machinery
with declared implementations resolved from an Implementations table, branch
predicates on real values running only the taken arm, Ref-path projection
threading, and journaled replay on execution-id reuse. Bodied flows reject
toLayer; behavior comes from exactly one place.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit 42ee5ce)
(cherry picked from commit 72f9b1e)
A bodied flow's root value settles as Complete (done), Suspended (park,
existing Waiting vocabulary), or the new encoded Handoff (to): the engine
plans the target and runs it as the next round. Rounds chain under one
lineage via parent_run_id with deterministic (lineage, ordinal) execution
ids, the settle-and-create seam is one storage transaction, maxRounds is
declared on the origin flow and enforced at the handoff with a typed
terminal failure, and the root caller's polling follows handoff chains
transparently to the final value.

Docs: docs/specs/Concepts/Trampoline Loops.md
(cherry picked from commit acafd2e)
(cherry picked from commit e1f2cc8)
flow.child(payload) records a non-expanded boundary node whose material
carries the child's tag, payload, and declared envelope. The interpreter
runs it as a genuine child execution with a deterministic id derived from
the parent execution and node address, parent edges and suspension
propagate through the existing machinery, and placement conflicts on
inline .call() now error with placement_requires_boundary directing to
.child().

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit f09bc16)
(cherry picked from commit b4ec945)
The branch move applied to the error channel: Node.catch protects a
subgraph with a failure arm that is plan-visible via an explicit failure
edge reason, filters errors by digested schema (missing filter = the full
typed error channel), never intercepts defects or compensation failures,
and settles the protected node's planned reference with the caught error
so failure-arm payloads consume it without re-running failed work.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
(cherry picked from commit 47ffb00)
(cherry picked from commit 37d8dc2)
(cherry picked from commit 417035e)
(cherry picked from commit dc5ad88)
Every handler-flow in packages/flow is gone: a Flow.make declaration with
a toLayer handler was the attachment-decides shape the authoring model
rejects, so each one splits into the two nouns it always was — a declared
Activity that does the work, and a bodied Flow whose body names it.
Suites drive them through Interpreter.layer over one shared
Activity.layerImplementations table, added to the runtime fixture as
layerWired.

The fixture also threads a dispatch's interrupted flag back to the flow,
the way it already threads the waiting classification: a recorded
interruption is a durable outcome, and the flag DurableDeferred.await
sets on the instance in scope must reach the flow or a terminal outcome
reads as an external suspension.

FlowDefinitionGaps' definition-level withRollback cases pinned the legacy
shape and now pin the new one: Flow.Execution<Tag> is a phantom only
Flow.toLayer discharges, so the combinator is reached from a declared
activity's implementation through a named cast that says so.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
Depends-on: 417035e
(cherry picked from commit 9e7b75e)
(cherry picked from commit 2dd7f2d)
…un model

Engine and engine-store test fixtures move off handler-style flows onto
declared activities, bodied flows, Interpreter.layer, and shared activity
implementation tables. Behavior-identical; no production sources changed;
run-store had no legacy call sites.

(cherry picked from commit e043ff4)
(cherry picked from commit 9afadd5)
… call sites

Every remaining handler-flow outside packages/flow, engine, engine-store, and
run-store moves to the two-noun model: a declared Activity that does the work,
and a bodied Flow whose body names it, driven by Interpreter.layer over one
Activity.layerImplementations table.

The seven examples are documentation as much as code, so each declared atom is
named for what it does rather than wrapped as "<flow>/activity": Greet, Bundle,
Assess, Release, Measure, Post, CompileTarget. Where the old handler WAS a
single sealed activity (05, and the fork suite's fork-once), the inline
activity becomes the declaration itself, so the journal shape and the sealed
cache key are unchanged and the fork still replays with one dispatch. Where it
was a composite (02, 03, 04, 06), the composite becomes the declared atom's
implementation and the inner inline activities stay exactly as they were.

Example 01 now provides NodeCrypto: an activity dispatch is recorded under a
derived step identity, so the memory engine needs a Crypto that a handler-only
flow never asked for. Example 03's Summary field is renamed handlerEntries ->
stepEntries because the counter now counts the step implementation's entries.

The time-travel EngineIntegration fixture keeps its composite handler as ONE
declared activity rather than four body nodes, so the evidence its assertions
fold stays the shape it was written against; the body's own step adds one
dispatch, and the four attempt/anchor/fork counts move from 3 to 4 and 2 to 3
accordingly. SqlForkExecution drops engine.register for the ordinary layer
composition over Layer.succeed(FlowRuntime, engine).

Docs: docs/specs/Concepts/Unified Flow Authoring.md
Depends-on: e043ff4
(cherry picked from commit a3f2bda)
(cherry picked from commit bfd8891)
The README and the introduction both opened on a handler-flow, which is the
shape example 01 no longer has. Both quick starts now declare the Activity,
attach its implementation, and give the Flow a body that names it, over
Interpreter.layer and the implementation table — the same program the example
runs. The examples page follows: the 01 row, the 03 assertion, and the reading
order say declaration, implementation, and body instead of handler, and the 06
row drops the entry counts the added step node changed.

Docs: README.md, docs/pages/index.md, docs/pages/examples.md
Depends-on: a3f2bda
(cherry picked from commit e41c07b)
(cherry picked from commit d4b4d3c)
The two nouns stop overlapping. `Flow.make` requires `body`, so every
declaration carries the pure plan-time function that IS its behavior, and
`Flow.Any.body` is no longer optional. `toLayer` leaves the flow surface
entirely: an Activity carries an implementation, attached separately as a
Layer, and a Flow carries a body, driven by `Interpreter.layer`. Attaching a
second, opaque behavior to a flow is not refused at run time any more — there
is nothing to attach it with.

What the optional-body stage needed goes with it. `Flow.Bodied` described a
flow that had a body, which is now every flow, so `Flow.Flow` is that type.
`Flow.BodyDefinesBehavior` was the defect `toLayer` raised on a bodied flow.
`InterpreterError`'s `missing_body` code and `Interpreter.layer`'s check are
gone because the argument has a body by construction. Graph building drops its
body-less callee branches: an inline call is spliced unless its declaration did
not survive beside its AST, which is now the only leaf inline call and the only
call with no body digest to fold in.

The runtime's own registration seam stays, internal. `Activity.toLayer` still
registers the activity's flow form so an activity is executable as a durable
execution of its own; that form is now an ordinary bodied flow whose body is
the one call to the activity, registered through `FlowRuntime.register`
directly rather than through a flow-level `toLayer`.

Call sites: no legacy handler-flow survived waves A-C, but the engine and
engine-store suites that drive `FlowRuntime.register` directly declare flows as
port fixtures, where the handler under test is the behavior. Those declarations
take an inert body. `execute`, `poll`, `interrupt`, and `resume` are unchanged.

Docs: docs/specs/Concepts/Unified Flow Authoring.md
Depends-on: e41c07b
(cherry picked from commit 10ca198)
(cherry picked from commit 27f0f20)
…oved

The requirement `Flow.make` now enforces is asserted where a reader looks for
it. A declaration that omits `body` does not compile, pinned by the directive
tsc rejects when the call succeeds, and `Flow.Any["body"]` is non-nullable, so
no consumer is left an optional-body branch to write. Beside it, what 10ca198
removed is pinned absent: `Flow.Bodied` no longer resolves as a type,
`BodyDefinesBehavior` is off the flow namespace, and `InterpreterError`'s code
union is the five codes that remain.

`@smthrs/plan` gets the breaking-change entry the flow one implies. A flow
call's `KeyMaterial.body` no longer has a body-less shape, so `StepKey.content`
folds a body digest into every flow-call node whose declaration survived beside
its AST, and the keys `Plan.compile` derives for the calls that carried none
move with it.

The prose that still described a flow handler is corrected. `withRollback`
applies inside the flow execution; the implementations table is what a
composition driving behavior registered directly with the runtime does not
need; and the four site pages that showed a body-less `Flow.make` or a
flow-level `toLayer` now show an activity carrying the implementation and
`Interpreter.layer` driving the body. Each sample was compiled against the real
packages before it landed.

Docs: docs/specs/Concepts/Unified Flow Authoring.md, docs/pages/architecture.md, docs/pages/api/flow.md, docs/pages/api/flows.md, docs/pages/api/engine.md
Depends-on: 10ca198
(cherry picked from commit 95b82ec)
(cherry picked from commit da8e044)
One suite in the barrel package that drives the real SQLite-backed stores:
the counter-to-100 lineage and its parent_run_id chain, a lineage re-driven
after the worker that opened a round went away, both branch arms on real
values, the planned-value refusals and the field access they still allow,
the recursion refusal and the handoff it points at, a .child() boundary with
its parent edge and suspension, Sleep and WaitFor parking and waking, the
round budget, both Node.catch arms, and Graph.build to Plan.compile to
PlanStore with a PlanDiff of round-over-round growth.

Docs: docs/specs/Concepts/Unified Flow Authoring.md, docs/specs/Concepts/Trampoline Loops.md
Depends-on: 95b82ec
(cherry picked from commit a4f2936)
(cherry picked from commit 55430c6)
(cherry picked from commit 868e112)
(cherry picked from commit 7b9488e)
@roninjin10
roninjin10 changed the base branch from flow-body-required to main August 13, 2026 01:13
@roninjin10
roninjin10 merged commit 4d7801b into main Aug 13, 2026
0 of 2 checks passed
@roninjin10
roninjin10 deleted the e2e-suite branch August 13, 2026 01:35
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.

1 participant