langgraph-scenario-lab
Behavioral scenario testing for production LangGraph applications: write a scenario as plain Python, run it against the real graph, and assert on the execution trace.
scenario → real graph execution → trace → assertions → report.
Why
Features
- Scenarios as plain Python — no YAML, no JSON, no custom DSL.
- Trace-based assertions — answer, tool calls, node path, state (incl.
nested
result.state["a"]["b"].equals(...)), status events, errors, sources. - Fault injection —
lab.fail/lab.mockto test retries, fallbacks, and network failures against the real graph. - LLM mocking —
lab.mock_llm("classifier", {...})replaces the model call inside a graph node with a canned response for deterministic routing tests; the default always uses the real model. - Budget assertions —
result.llm.calls <= 3,result.llm.tokens <= 5000,result.latency < 10,result.retries,result.interrupts. - Record / Replay — run
--mode recordonce, then--mode replayfor fast, deterministic regression checks without re-contacting tools. - Multi-turn scenarios —
lab.scenario().user(...).run(...). - Custom domains — product-specific entities exposed as
result.<name>, registered viaScenarioLab(domains=[...]). - Rich terminal report — colourised live progress, exit code 0/1/2.
- pytest integration — an autouse
labfixture; same suite convention.
Install
uv pip install langgraph-scenario-lab
# or
pip install langgraph-scenario-lab
Requires Python ≥ 3.11.
Quickstart
A suite is a directory with lab.py (builds the lab) and scenarios/
(auto-collected test_*.py files):
scenarios_lab/
├── lab.py # def build_lab() -> ScenarioLab
└── scenarios/
└── test_small_talk.py
lab.py:
from langgraph_scenario_lab import ScenarioLab
def build_lab() -> ScenarioLab:
from examples.demo_graph import build_graph
return ScenarioLab(build_graph())
scenarios/test_small_talk.py:
from langgraph_scenario_lab import ScenarioLab
def test_small_talk(lab: ScenarioLab) -> None:
result = lab.run("Hi")
result.answer.matches(r"hi|help")
result.tools.never_called("search_knowledge")
result.status.matches(r"^Thinking")
result.path.contains("answer")
Run it:
scenario-lab run # live, against the real graph
scenario-lab run --mode record # live + save recordings under the suite
scenario-lab run --mode replay # deterministic: no graph calls, no tool calls
Scenario Lab
────────────────────────────────────────
✓ test_small_talk
8 passed, 0 failed
See docs/quickstart.md for the full walkthrough.
CLI
scenario-lab run [target] [--mode live|record|replay] [--record-dir DIR]
[--replay-dir DIR] [--jsonl FILE] [--quiet] [--log-file FILE]
[--log-level debug|info|warning|error] [--name-filter SUBSTR]
scenario-lab version
target— suite directory (defaultscenarios_lab/), ascenarios/subdirectory, atest_*.pyfile, orfile::test_name.--mode record/--mode replayimplement the record/replay cycle.--jsonlappends one JSON line (scenario, status, trace) per run.--name-filterre-runs only the scenarios whose name contains the substring.- Exit codes:
0all passed,1failures,2suite/usage error.
A failing assertion prints a red FAILURES section with the expected vs
actual (reproduce with
scenario-lab run examples/demo_suite/scenarios/test_failing_example.py):
Full reference in docs/cli.md.
Writing scenarios
def test_rag(lab: ScenarioLab) -> None:
result = lab.run("What is Nimbus?")
result.answer.contains("Nimbus")
result.sources.nonempty()
result.tools.called("search_knowledge")
result.nodes.visited("query_rewrite")
result.status.sequence(r"^Thinking", r"^Searching", r"^Composing")
Fault injection
def test_retry(lab: ScenarioLab) -> None:
lab.fail("search_knowledge", TimeoutError("flaky"), times=1)
result = lab.run("What is flaky search?")
result.errors.expected("search_knowledge")
assert result.invocations["search_knowledge"] == 2 # retried on the graph
def test_mock(lab: ScenarioLab) -> None:
lab.mock("search_knowledge", return_value="Nimbus fake docs")
result = lab.run("What are the mock docs?")
result.answer.contains("Nimbus fake docs")
Multi-turn
def test_context(lab: ScenarioLab) -> None:
scenario = lab.scenario()
scenario.user("My favorite color is blue.")
result = scenario.run("What is my favorite color?")
result.state.has("messages")
Custom domains
Expose product entities as result.<key> without touching the core:
# scenarios_lab/domains.py
from langgraph_scenario_lab import DomainModel
class StateSourcesDomain(DomainModel):
key = "my_sources"
def bind(self, trace):
return list(trace.state.get("sources") or [])
def my_sources() -> StateSourcesDomain:
return StateSourcesDomain()
# scenarios_lab/lab.py
def build_lab() -> ScenarioLab:
from domains import my_sources
return ScenarioLab(build_graph(), domains=[my_sources()])
# scenarios/test_domain.py
def test_sources(lab: ScenarioLab) -> None:
result = lab.run("What is Nimbus?")
assert result.my_sources == ["https://docs.nimbus.local/nimbus"]
pytest integration
The package registers a pytest11 plugin. With the suite on disk:
pytest scenarios_lab/scenarios
The autouse lab fixture serves the suite's lab, clears faults/mocks between
tests, and auto-discovers lab.py walking up from the test file. Pass an
explicit suite with --scenario-suite path/to/suite.
Record / replay
-
Record a golden baseline:
scenario-lab run --mode record # saves scenarios_lab/recordings/*.json
-
Change the prompt or graph, then replay the old trace against the new assertions (no tool calls, fully deterministic):
scenario-lab run --mode replay
Recordings are JSON traces keyed by the last human/user message; per query the cleanest (error-free) recording wins.
Diagnostics
result.diff(baseline)renders a BEFORE/AFTER execution diff when behavior changed (node path, statuses, tool calls, errors, answer).--jsonlgives machine-readable per-scenario results.--log-file+--log-levelmirror the rich progress to a durable log.
About the fixtures
examples/demo_graph.py— a compact Nimbus/Vega RAG graph used by the docs walkthrough and the dogfood suite.examples/support_graph.py— a non-trivial example: a support chat with intent routing, knowledge-base retrieval + ranking, subscription and escalation tools, aRetryPolicy, extra state keys (intent,queries,source_documents,sources), andstatusstream events. Read its docstring to see where each asserted-on piece of data comes from.examples/demo_suite/— a richly commented scenario suite (15 cases) covering the assertion catalog — answer, tools, path, status, state, errors, sources, custom domains, fault injection, multi-turn — wired againstsupport_graph. Run it withscenario-lab run examples/demo_suite.scenarios_lab/is the dogfood suite used by the tests and the CLI.
Development
uv sync --extra dev
ruff check && ruff format --check
uv run python -m pytest tests/ scenarios_lab/scenarios/ -q
CI (.github/workflows/ci.yml) runs the same checks on Python 3.11–3.13 and
smoke-installs the built wheel in a clean venv. Releasing is tag-driven:
publish.yml builds and publishes to PyPI via trusted
publishing whenever a v* tag is
pushed.
uv build # local wheel + sdist sanity check
uvx --from build twine check dist/* # verify long description metadata
git tag v0.2.0 && git push origin v0.2.0
Documentation
- Changelog
- Constitution — the design contract the code is built against
- Quickstart
- Example suite: support chat — 15 commented scenarios against a non-trivial graph
- CLI reference
- Execution modes: live / record / replay
- Assertion catalog
- Fault injection
- Custom domains
- pytest plugin
- Architecture
- API reference
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langgraph_scenario_lab-0.2.0.tar.gz.
File metadata
- Download URL: langgraph_scenario_lab-0.2.0.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6dfa46f02aa9a09f2195060eb6d8c13f94431e76ae84dc66033f3b5de354b86c
|
|
| MD5 |
a6bbeaee86d5ef3eb05369d161ef4035
|
|
| BLAKE2b-256 |
df7d037fc15b874074a1e6c15203410f8c31eaeb58da3530e0dbe38999787f26
|
Provenance
The following attestation bundles were made for langgraph_scenario_lab-0.2.0.tar.gz:
Publisher:
publish.yml on bzdvdn/langgraph-scenario-lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_scenario_lab-0.2.0.tar.gz -
Subject digest:
6dfa46f02aa9a09f2195060eb6d8c13f94431e76ae84dc66033f3b5de354b86c - Sigstore transparency entry: 2501475377
- Sigstore integration time:
-
Permalink:
bzdvdn/langgraph-scenario-lab@9b4862e2db205bb2c6e7405dee3cdfc263a769a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bzdvdn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9b4862e2db205bb2c6e7405dee3cdfc263a769a4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file langgraph_scenario_lab-0.2.0-py3-none-any.whl.
File metadata
- Download URL: langgraph_scenario_lab-0.2.0-py3-none-any.whl
- Upload date:
- Size: 53.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43a881045b96a808002534f1ae1deffbba021cc10e256b76eb2d9ae4b433aab7
|
|
| MD5 |
edd8afb8cb545e0613e64de5e178c7aa
|
|
| BLAKE2b-256 |
0077c4bea45a6add825cf1d97d4839acd93625b1f887ea0fa16bc2744a4f9ee3
|
Provenance
The following attestation bundles were made for langgraph_scenario_lab-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on bzdvdn/langgraph-scenario-lab
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_scenario_lab-0.2.0-py3-none-any.whl -
Subject digest:
43a881045b96a808002534f1ae1deffbba021cc10e256b76eb2d9ae4b433aab7 - Sigstore transparency entry: 2501475404
- Sigstore integration time:
-
Permalink:
bzdvdn/langgraph-scenario-lab@9b4862e2db205bb2c6e7405dee3cdfc263a769a4 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/bzdvdn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9b4862e2db205bb2c6e7405dee3cdfc263a769a4 -
Trigger Event:
push
-
Statement type: