Skip to content

feat(core): Durable.Testing helpers + fix for string step errors - #18

Merged
kasvith merged 3 commits into
mainfrom
feat/testing
Oct 23, 2026
Merged

kasvith merged 3 commits into
mainfrom
feat/testing

Conversation

@kasvith

@kasvith kasvith commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Durable.Testing, which runs workflows inline in ExUnit and gives assertions for the result (roadmap milestone M03). Writing its tests turned up an engine bug, which is fixed in its own commit.

Durable.Testing

# config/test.exs
config :durable, disable_queue_processing: true

import Durable.Testing

run = run_workflow(OrderWorkflow, %{"order_id" => 42})
assert_waiting_for_input(run, "approval")

:ok = Durable.provide_input(run.id, "approval", %{"approved" => true})
run |> drain() |> assert_status(:completed)
assert assert_step(run, :charge).attempt == 3
  • run_workflow/3 and drain/2 run the workflow in the test process, including parallel branches and child workflows, until nothing is left to do.
    • Sleeps end immediately.
    • Retry backoff is skipped. Backoff.pause/1 checks a process flag, so both step retries and compensation retries skip it.
    • Steps share the test's sandbox, and Mox works without allow/3.
    • drain/2 raises if queue processing is on, because queue workers would race the test for the same runs.
  • assert_status/3, assert_step/4, assert_waiting_for_input/3, assert_waiting_for_event/3 and steps/2. Failed assertions print the run's current step and error. assert_step finds steps inside parallel and branch blocks by their plain name.
  • It's a module you import into your own DataCase, not the Durable.TestCase template the M03 plan described. Users already have a template that sets up the sandbox, and ExUnit only lets a test module use one.
  • New guide: guides/testing.md, linked from HexDocs, the README and the site.

Fix: string step errors left runs stuck in :running

A step returning {:error, "card declined"} passed the raw string to the write that marks the run failed or compensated. The error column is a map, so the write was rejected:

  • Without compensations, cas_update returned {:error, changeset} without raising, so mark_failed's fallback never ran. The run stayed :running until stale-lock recovery.
  • With compensations, the executor raised Ecto.ChangeError.

handle_step_failure/4 now converts the error to a map and makes it safe to store as JSON, the same way the parallel-child path already did. All failure paths go through this one function. The guides show steps returning {:error, "..."}, so this affects production, not just tests.

Roadmap

agents/ROADMAP.md and the milestone docs are updated to match what has shipped since March (1.0, the dashboard and CLI, and now testing).

Known gaps

  • drain/2 doesn't trigger wait timeouts, because the timeout worker only runs with the queue on. This is in the guide and listed as a roadmap follow-up.

Test plan

  • mix test in durable/: 488 passed. The new test/durable/testing_test.exs covers parallel blocks, sleeps, input waits, retries without backoff, child workflows, compensations, string errors and failure messages.
  • mix compile --warnings-as-errors, mix credo --strict, mix format
  • mix docs: no new warnings
  • durable_dashboard test suite (not run locally)

…n running

A step returning {:error, "declined"} handed the raw string to the
failed/compensated write. The error column is a map, so the write was
rejected: without compensations the run stayed :running until stale-lock
recovery; with them, the executor crashed. handle_step_failure/4 now
normalizes and sanitizes the error, as the parallel-child path already did.
run_workflow/3 and drain/2 run a workflow inline in the test process,
including parallel branches and child workflows, until nothing is left to
do. Sleeps end immediately and retry backoff is skipped. assert_status,
assert_step, assert_waiting_for_input and assert_waiting_for_event give
failure messages that show the run's current step and error.

Adds guides/testing.md to HexDocs, the README and the site.
@kasvith
kasvith merged commit b450fa7 into main Oct 23, 2026
5 checks passed
@kasvith
kasvith deleted the feat/testing branch October 23, 2026 01:53
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