* Add Monocle behavioral test suite Trace-based tests under tests/monocle/ asserting against the agent's Monocle execution traces: 4 offline tests load a recorded trace by file (with_trace_source), one per curated question, plus 1 live end-to-end test. Fluent structural asserts (agent, tools, input/output, token/duration budget); additive only, no app-code changes. * Address review: restructure suite, move under backend/tests/monocle/ Responds to the review on this PR. Behavioural coverage is now the live tests, not frozen fixtures. Keep one offline test as a worked example of the fluent assertion API (loads a recorded trace by file, no keys), and add two live tests that drive the agent end-to-end through DeerFlowClient and assert on the trace the real run emits (web-research and sandbox paths). The live tests skip without OPENAI_API_KEY or the app. Other review fixes: - Move the suite under backend/tests/monocle/ so backend pytest collects it. - Split helpers into _helpers.py; conftest is fixtures-only (run_agent), with Monocle setup owned by the validator and .env load scoped to the live path. - Resolve the model from config.yaml instead of hardcoding gpt-4o; skip the live tests when config.yaml is absent. - Keep one trace with a stable name (web_research_ev_battery.json); drop the other three (removes ~2,200 lines of fixture blobs). - Drop the flaky wall-clock duration bound on live runs. - Keep monocle_test_tools in a standalone requirements.txt rather than the backend dev group: it hard-depends on the ML eval stack (torch, transformers, sentence-transformers, ~48 packages, +950 lines in uv.lock), so isolating it keeps the app's locked deps clean. importorskip skips the suite when absent. * docs(monocle tests): explain the golden-trace workflow Add a "How this is meant to be used" section: capture a run you are happy with as a golden, labelled trace, turn it into assertions (the offline example), then point the same assertions at the live agent so every later run has to reproduce that behaviour. * Address review: fix docstring pytest paths, state the suite is not run in CI The docstring commands now use the backend/tests/monocle/ form (matching the README, which also gains the backend-dir uv variant), and the README states explicitly that the suite is skipped in CI and run on demand. * Address review: document the committed trace, pin assertions context - README: new section on the committed trace. It is a full, unmodified real-run recording (system prompt of the recording date + fetched web content, no credentials), committed whole so the offline example parses a genuine trace. The offline assertions are pinned to this trace and the monocle_apptrace 0.8.8 span shapes; re-record when prompt, tools, or model change. - README: note that the monocle_trace_asserter fixture comes from monocle_test_tools' auto-registered pytest plugin (pytest11 entry point). - test_deerflow.py: comment why web_fetch asserts min_count=2 rather than the recorded exact count of 5 (fetch counts vary run to run; keep it a floor). - requirements.txt: loose pin python-dotenv>=1.0. * Address review: make live tests explicit opt-in, drop OpenAI-only gate Two execution-gating fixes from review: - Live tests are now opt-in via MONOCLE_LIVE_TESTS=1 (default off). Previously the documented offline command collected the live tests too, and on a configured checkout (.env + config.yaml present) they would run for real, spending model tokens and hitting the network. Now the plain `pytest backend/tests/monocle/` run cannot go live regardless of what credentials are present; test_live_gate_defaults_off pins the gate. - The run_agent fixture no longer requires OPENAI_API_KEY. config.yaml resolves the model, which may be any provider (Anthropic, Gemini, Volcengine, ...), so a hard-coded OpenAI gate skipped valid configurations and passed invalid ones. Credentials are validated by the configured model itself. README and docstrings updated to match: offline command is offline by construction, live is MONOCLE_LIVE_TESTS=1, credentials described as the configured model's rather than OpenAI's. Verified both modes: default run is 2 passed 2 skipped with no network; opted in, all 4 pass with real end-to-end runs. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
DeerFlow behavioural tests (Monocle Test Tools)
Trace-based tests for DeerFlow. Monocle records each run as a structured trace (the agent invocation, every tool call, token usage, timings), and these tests assert against that trace with Monocle Test Tools.
How this is meant to be used
Instrument the agent with Monocle and run it against a question. Once it answers the way you expect and makes the agent and tool calls you expect, capture that run as a trace. That trace is a golden, labelled reference for the question: a record of correct behaviour, not just sample data. You turn it into assertions (the offline example shows how), and then you point those same assertions at the live agent for the same question, so every later run has to reproduce that behaviour. The offline test is where you pin down what good looks like; the live test is what enforces it against a real run.
Layers
The suite has two:
- One offline example (
test_assertion_api_example) loads a recorded trace from file and shows the full fluent vocabulary in one place. It needs no keys and no network. Because it asserts against frozen JSON, it guards the trace format and the asserter wiring, not DeerFlow's behaviour. Treat it as the worked example for writing your own assertions. - Two live tests drive the agent end-to-end and assert on the trace the real
run emits. These are the behavioural guards: a change that alters routing, tool
selection, or token cost is caught here. They are explicit opt-in via
MONOCLE_LIVE_TESTS=1and skip by default, so a plain run never spends model tokens or hits the network, even on a fully configured checkout.
Layout
test_deerflow.py— the offline example + two live testsconftest.py— therun_agentfixture (live path only)_helpers.py— paths andrun_deerflow()traces/— the recorded trace the offline example loadsrequirements.txt— standalone dependencies
The committed trace
traces/web_research_ev_battery.json is a full, unmodified recording of a real
run, committed whole so the offline example parses a genuine trace. That means
it embeds the DeerFlow system prompt as of the recording date and the content
the run fetched from the web, alongside the span structure the assertions read.
It contains no credentials.
The offline assertions are pinned to this exact trace and to the
monocle_apptrace 0.8.8 span shapes: the LangGraph agent span name, the tool
names, and the input phrasing. A rename of any of those breaks the offline test
even when behaviour is unchanged; re-record the trace when the prompt, tools, or
model change.
Run
monocle_test_tools hard-depends on the ML eval stack (torch, transformers,
sentence-transformers), so it is a standalone requirements.txt install rather
than a backend dependency. When it is absent (e.g. a plain backend venv) the
whole suite skips cleanly via pytest.importorskip.
Because that dependency is deliberately absent from the backend deps, none of
these tests run in CI — make test collects and skips the whole module,
including the offline example. This is an on-demand suite: install the
requirements and run it locally (or wire a dedicated CI job with the
requirements installed) when changing agent behaviour, tools, or routing.
# from the repo root
pip install -r backend/tests/monocle/requirements.txt
# offline — no network, no keys; the live tests skip unless opted in
pytest backend/tests/monocle/
# opt in to the live behavioural tests (real model calls + web requests)
MONOCLE_LIVE_TESTS=1 pytest backend/tests/monocle/
Or, following the backend convention (from backend/, with uv):
uv pip install -r tests/monocle/requirements.txt
uv run pytest tests/monocle/ # offline
MONOCLE_LIVE_TESTS=1 uv run pytest tests/monocle/ # + live
The live tests are opt-in by design: without MONOCLE_LIVE_TESTS=1 they skip
even on a checkout where credentials and config.yaml are present, so the
default command can never spend tokens or write to a sandbox. When opted in,
they still skip if the DeerFlow app is not importable or config.yaml is
missing. Model credentials are validated by the configured model itself —
config.yaml may select any provider (OpenAI, Anthropic, Gemini, and so on),
so there is no hard-coded key requirement. DeerFlow's web_search is
DuckDuckGo and needs no key of its own.
The monocle_trace_asserter fixture is provided by monocle_test_tools' own
pytest plugin, which registers automatically on install (a pytest11 entry
point); no pytest_plugins configuration is needed.
Add your own test
- Run DeerFlow under Monocle and capture a trace of a run you are happy with
(Monocle writes trace JSON to
.monocle/by default). - For an offline example, move it into
traces/and load it withmonocle_trace_asserter.with_trace_source("file", trace_path=path). - For a behavioural test, drive the agent live via the
run_agentfixture andmonocle_trace_asserter.validator.test_workflow(run_agent, {"test_input": (...)}). - Assert with the fluent API:
called_agent(...),called_tool(...),contains_input/contains_any_output(...),under_token_limit(...),under_duration(..., span_type="workflow").
Evaluations (note)
Structural assertions are the coverage here. Content/quality evaluations are
not wired in this suite because, on the current monocle_test_tools, local
evals do not compose with file-loaded traces:
- Declarative
test_spans[].eval(comparer:"metric") is silently ignored byvalidator.validate()—_evaluate_spanhas no call sites, so an assertion that should fail (e.g. a required keyword that is absent) passes vacuously. - The fluent
check_eval()path is wired for the Okahu eval-service signature (filtered_spans=), which the local evaluators (keyword_presence, etc.) do not accept — it raisesTypeError.
So local evals are omitted rather than added as vacuous no-ops. The Okahu eval
layer (needs OKAHU_API_KEY) remains an option for content grading.