test(eval): sidecar eval harness + distributed-seams suite (V2-19)#2202
Conversation
Before: nothing exercised the email agent through the v2 sidecar/daemon path, and no test guarded the daemon<->sidecar contracts as a set — the frozen §0.2 event vocabulary, the relay's crash/cancel terminal, and the auth leg were each pinned only by their own issue's tests, with no cross-seam coherence check. After: an eval harness drives POST /v1/<agent>/query over REST and asserts the canonical event *sequence* against baselines committed in the agent package (serial by construction, per the CLAUDE.md eval rule), plus a single distributed-seams suite that guards the contracts across seams and self-skips the ones not yet merged. Harness (src/gaia/eval/sidecar_harness.py): pure core (SSE parse, ordered- subsequence match tolerant of LLM-variable event counts, cross-process SerialEvalLock) split from a thin live driver, mirroring behavior_harness. A missing/unreachable backend raises loudly — never a silent green pass. Seams covered now: relay<->translator<->harness vocabulary coherence; the relay's synthetic crash terminal parses as a canonical error; the daemon->sidecar bearer is delivered via a private env channel, never argv. Skipped-pending-merge (each keyed on the source's real presence, so the skip flips to a real assertion when its issue lands): secret-file delivery (#2149), model-slot broker (#2151), custody scoping (#2153), migration idempotency (#2155). The on-hardware golden path (UI -> daemon -> sidecar -> Lemonade) is an opt-in real_model test; a new Ubuntu CI job runs the deterministic suite. Part of #2014 Closes #2180
|
Verdict: Approve This is a well-built, test-only PR: it adds a sidecar eval harness that drives the email agent's Bottom line: loud-on-absence throughout (no silent green passes), serial-by-construction via a real cross-process lock, and baselines committed next to the route they pin. I verified the always-run tests reference symbols that actually exist in the landed relay/manager/translator modules, and that 🔍 Technical detailsVerification performed
🟢 Minor (optional, non-blocking)
Strengths
|
|
Two harness issues worth fixing before merge — both silent-failure shaped, which matters most in an eval harness:
Lesser: the live golden-path fixture's broad The rest of the suite is honest and the seam coverage is real — these are the only pieces that can go green while lying. |
…row the golden-path skip Three silent-failure seams in the sidecar eval harness could all drift or regress while the tests stayed green: - The seam-coverage manifest test asserted two hardcoded `True` literals and mere JSON-serializability, never the computed probe values — so the covered-vs-pending split could silently diverge from reality. It now pins the live probes to an explicit expected map, so a merged seam flips a probe, fails the test, and forces the manifest + real test to be updated. - SerialEvalLock's stale-lock reclaim was TOCTOU-racy: two contenders that both read the same dead holder pid could each unlink and recreate, ending with both acquiring — the exact parallel-eval Lemonade race-evict the lock prevents. The reclaim now renames the lock into a per-pid token (atomic; only one contender can move the file), re-verifies the stale pid, and re-contends on any interference. Adds a unit test simulating the losing contender. - The live golden-path fixture wrapped `registry.ensure` in a broad `except Exception -> pytest.skip`, turning real sidecar-startup regressions into green skips on the opt-in hardware run. It now skips only on the unconfigured-environment errors (missing binary / dev deps / unsupported platform); health-timeout, version-mismatch, integrity, and HTTP failures propagate as failures.
kovtcharov
left a comment
There was a problem hiding this comment.
All three silent-failure seams are fixed: the seam-coverage test now asserts the full manifest dict (fails when a pending seam merges, forcing the update — no longer a tautology), SerialEvalLock uses a verify-at-unlink reclaim so two contenders can't both acquire, and the golden-path skip is narrowed to missing-env/deps. Suite runs 34 passed / 4 skipped without live infra. Verified on the branch.
…has landed The distributed-seams suite (#2202) is a forcing function: each pending-seam placeholder raises the instant its source module appears, so the PR that lands the seam must replace the placeholder with a real assertion. Two seams have landed and left the suite red: - model-slot broker (#2151) — this PR's net-new gaia.daemon.broker - ~/.gaia migration (#2155/#2200) — already on main; #2202 merged after it and never flipped the placeholder, so main's Distributed Seams job is currently red Replace both placeholders with real assertions — the broker serializes two concurrent different-model lease requests (no race-evict of the single Lemonade slot) and grants an interactive lease ahead of a queued background one; the migration run-twice-is-a-no-op against a real populated pre-v2 fixture — and move both entries from the pending map to covered so the manifest reflects reality. Secret-file (#2149) and custody (#2153) stay pending and correctly skipped.
Why this matters
Before this, nothing exercised the email agent through the v2 sidecar/daemon path — every eval ran the loop in-process — and no test guarded the daemon↔sidecar contracts as a set. The frozen §0.2 event vocabulary, the relay's crash/cancel terminal, and the auth leg were each pinned only by their own issue's tests, so a drift between seams (e.g. a front-door and the sidecar disagreeing on what "done" looks like) could slip through green.
After: an eval harness drives
POST /v1/<agent>/queryover REST and asserts the canonical event sequence (not a brittle final string) against baselines committed in the agent package — serial by construction per the CLAUDE.md eval rule. Plus a single distributed-seams suite that guards the contracts across seams and self-skips the ones not yet merged, with each skip keyed on the source's real presence so it flips to a live assertion the moment its issue lands.The harness (
src/gaia/eval/sidecar_harness.py) keeps its pure core (SSE parse, ordered-subsequence match tolerant of LLM-variable event counts, a cross-processSerialEvalLock) split from a thin live driver, mirroringbehavior_harness. A missing/unreachable backend raises loudly — never a silent green pass.Seams: covered vs. skipped-pending-merge
error/host/v1/*per-agent scopingThe on-hardware golden path (UI → daemon → sidecar → Lemonade) is an opt-in
real_modeltest (tests/integration/eval/test_sidecar_eval.py); a new Ubuntu CI job (test_distributed_seams.yml) runs the deterministic suite on every touching PR.Test plan
python -m pytest tests/unit/test_sidecar_harness.py— 27 passed (pure core: SSE parse, sequence match, serial lock, baseline load; no Lemonade)python -m pytest tests/integration/test_distributed_seams.py -m distributed_seams -rs— 6 passed, 4 skipped (skips print their pending-merge reason)python -m pytest tests/integration/eval/test_sidecar_eval.py— 1 skipped (loud opt-in reason; runs on hardware withGAIA_SIDECAR_EVAL_LIVE=1)python util/lint.py --all --fix— black/isort/flake8 clean on the new filesGAIA_SIDECAR_EVAL_LIVE=1 python -m pytest tests/integration/eval/test_sidecar_eval.py -m real_model(golden path through the real sidecar)Part of #2014
Closes #2180