Skip to content

Open Source Workflows — Test Plan

Status: authoritative since v0.7 (Phase 6, 2026-04-17). Audience: engineers adding/changing WorkflowExecutionService, lens kinds, template seeds, or workflow RPCs. Scope: behavioural matrix that any workflow runtime (browser hook, CF Worker, future server executor) must satisfy.

This document enumerates the scenarios every implementation of the workflow execution engine MUST cover before a release. Each row names:

  1. A scenario — the observable behaviour being tested.
  2. The layer where the test lives (unit, contract, integration, e2e).
  3. The acceptance criteria (what "passes" looks like).
  4. The fixtures or seed data the test depends on.

When adding a new lens kind, fan-in strategy, fail-propagation mode, or moderation policy, append a new row — do not retrofit an existing one.


1. Scheduler Correctness (unit)

#ScenarioAcceptance
1.1Linear chain (A → B → C) executes in topological orderexecuteWorkflow emits node_completed in A, B, C order; results[i].attempts === 1.
1.2Diamond (A → B, A → C, B → D, C → D) runs B and C in parallelWith a 50ms-latency provider, D's started_at - A.completed_at ≤ B.duration + C.duration / 2 + slack.
1.3Cycle is rejectedEngine throws WorkflowCycleError before any node runs.
1.4Disconnected nodes are executedTwo independent roots both complete; edge set is empty.
1.5Cancellation mid-wave marks outstanding nodes cancelledctx.signal.abort() between waves → nodeResults[next].status === 'cancelled'.

2. Input / Output Contracts (contract)

#ScenarioAcceptance
2.1Missing required input fails fast (no provider call)Node status = failed, error = 'input_contract_violation', provider mock called 0 times.
2.2Wrong type input (e.g. string for number) fails fastSame as 2.1, errors list includes the field name.
2.3Output envelope missing data.foo when contract requires it → failederror = 'output_contract_violation', outputData.contractErrors[0].field === 'foo'.
2.4Unknown fields in output are allowed (schema is additive)Node status = completed.
2.5fn_get_version_contracts resolves inherited contract from kind:* tag when output_contract is NULLResolved contract defaults to the kind's canonical shape.

3. Fan-In Merge Strategies (unit)

#StrategyTwo-parent outputMerged value
3.1last_write_wins (default)"A", "B""B"
3.2concat"A", "B""A\nB"
3.3array"A", "B"["A", "B"]
3.4json_object (keyed by sourceOutputKey){x:1}, {y:2}{x:1, y:2}

4. Fail Propagation Policies (unit)

#Policy on C (child of B that failed)Expected C status
4.1propagate (default)failed, error = 'upstream_failure'
4.2skipskipped (distinct from cancelled)
4.3substitute_defaultcompleted with envelope = node.config.defaultEnvelope

5. Retry + Timeout (unit)

#ScenarioAcceptance
5.1Provider throws provider_error twice, succeeds on 3rd; retry.attempts = 3, backoff = 10msattempts === 3, status === 'completed'.
5.2Provider exceeds timeoutMs = 50msNode fails with timeout; AbortSignal on the provider is aborted.
5.3retry.retryableCauses = ['timeout'], error cause = contract_violated → no retryattempts === 1, status failed.
5.4Global ctx.signal aborts during retry sleepNode finalises as cancelled, not retried further.

6. Conditional Edges (unit)

#Condition on edge B → CB outputC executes?
6.1{ type: 'truthy' }""no (skipped)
6.2{ type: 'equals', value: 'ship' }"ship"yes
6.3{ type: 'contains', value: 'error' }"no errors"yes (contains is substring)
6.4{ type: 'present' } on a node that erroredB skipped → edge not traversed → C skipped.

7. Idempotency (integration)

#ScenarioAcceptance
7.1fn_start_workflow_run(... p_idempotency_key := 'k1') called twice with same inputsSecond call returns the same run_id; no duplicate workflow_node_results.
7.2Same key but different p_inputsSecond call raises conflicting_idempotency_key.
7.3Retry after 24h (expired)New run_id is issued.

8. Moderation (integration)

#ScenarioAcceptance
8.1Pre-call moderation blocks with category: 'violence'Provider not called; node status = failed, error = 'moderation_blocked', outputData.moderation.reasons populated.
8.2Post-call moderation flags outputNode status = failed, artifact is NOT persisted to media.objects.
8.3config.moderation = 'off' on the nodeGateway skipped even when global enableModeration = true.

9. Streaming (integration)

#ScenarioAcceptance
9.1IStreamingExecutionProvider.stream() emits 3 chunks then donePartialOutputSink.onChunk called 3 times; workflow_node_results.output_data.partial reflects the last chunk.
9.2Stream aborts via signalFinal node status = cancelled, no done chunk recorded.

10. Kind-Specific Providers (integration)

#Lens KindScenarioAcceptance
10.1kind:textOpenAI + Anthropic + Google with BYOKAll three return envelopes with artifactKind = 'text'.
10.2kind:imageFalAI local BYOK returns blob URLenvelope.media.url.startsWith('blob:'), persistNodeMediaArtifact uploads to media.objects.
10.3kind:videoFalAI local BYOKSame as 10.2 with mediaType='video'.
10.4kind:researchResearchProvider with stubbed retrieval backendEnvelope data.findings is an array of {claim, source} objects; data.summary is non-empty.
10.5kind:pdfPdfExportProvider with 3-section manifestEnvelope media.mime === 'application/pdf', media.bytes > 0, blob URL resolvable, resulting PDF has ≥ 3 pages.
10.6kind:routingRouter lens picks branch B over C based on inputsOnly B's subgraph runs; C's nodes end skipped.
10.7kind:orchestrationPlanning lens emits a JSON plan that a downstream transform consumesNo contract violations; plan fields propagate.
10.8kind:validationValidation lens on malformed input returns valid: falseDownstream node sees envelope.data.valid === false and can branch via conditional edge (6.x).

11. Observability (integration)

#ScenarioAcceptance
11.1execution.vw_workflow_run_timeline for a 3-node runReturns 2 (run_started/completed) + 2*3 (node_started/completed) = 8 rows.
11.2fn_tag_workflow_run by non-ownerRaises insufficient_privilege.
11.3partial_success tag is written when any node failed but engine completedworkflow_run_tags has one row with tag='partial_success'.
11.4high_token_usage tag fires when sum(output_tokens) > thresholdTag written with severity='warn'.

12. Template Workflows (e2e)

#ScenarioAcceptance
12.1WorkflowsPage renders "Start from template" stripuseTemplateWorkflows() returns ≥ 4 items after seeding.
12.2Clicking a template forks into the caller's workspaceuseForkWorkflow resolves with new run-editable workflow; original is read-only.
12.3Forked 7-stage chain executes end-to-end with BYOKAll 7 nodes reach completed; final PDF artifact is uploaded to media.objects.

Fixtures

Shared test fixtures live under lenserfight/libs/infra/execution/src/lib/__fixtures__/:

  • provider.mock.ts — scriptable IExecutionProvider (returns queued outputs or throws queued errors).
  • moderation.mock.ts — scriptable ModerationGateway.
  • dag.samples.ts — pre-built WorkflowNode + WorkflowEdge arrays for sections 1, 3, 4, 6.
  • contracts.samples.ts — representative LensInputContract / LensOutputContract pairs for section 2.

Running the Matrix

bash
# Unit / contract suites
npx nx test infra-execution

# Integration suites (requires a running Supabase instance)
npx nx test feature-workflows --testNamePattern='execution'

# E2E template smoke test (Playwright)
pnpm -F lenserfight e2e --grep @workflows-templates

Any new scenario MUST be added to this file in the same commit as the implementation. Reviewers reject PRs that ship engine changes without the corresponding row.