Skip to content

(MOT-4107) feat(harness): event-driven integration E2E runner#518

Merged
ytallo merged 36 commits into
mainfrom
feat/harness-conformance-e2e
Jul 22, 2026
Merged

(MOT-4107) feat(harness): event-driven integration E2E runner#518
ytallo merged 36 commits into
mainfrom
feat/harness-conformance-e2e

Conversation

@ytallo

@ytallo ytallo commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Implements the first slice of the harness conformance E2E runner described in the harness evaluation spec. The runner boots an isolated iii stack, drives scenarios through the public harness surface, and grades persisted evidence instead of relying on internal test hooks.

Scope

  • Boots a fresh stack per scenario with the pinned engine, queue, iii-directory, session-manager, context-manager, and harness workers.
  • Replaces only the router::* boundary with a strict scripted worker.
  • Covers streamed text, exactly-once function execution, multi-turn traces, and the console-oriented scenario surface.
  • Produces deterministic transcripts, requests, responses, router calls, traces, execution reports, and teardown artifacts.

Event-driven coordination

  • Adds the harness::ready trigger so readiness is explicit and supports late subscribers.
  • Observes function registration, trace-store changes, and terminal turn completion through triggers.
  • Uses acknowledged trigger-registration RPCs as barriers before starting scenario stimuli.
  • Waits for trace changes and process exits instead of polling readiness, trace state, or child health on fixed intervals.
  • Uses SIGCHLD on Unix and keeps bounded cleanup checks for descendant process groups.
  • Removes fixed boot delays and makes readiness failures report the missing signal or function.

Validation

  • Harness formatting, Clippy with -D warnings, and all-feature tests pass.
  • The integration runner unit and contract tests pass.
  • Real stack runs pass for E2E-001 and E2E-002 using the local iii binary.

Fixes MOT-4107

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 22, 2026 10:41pm
workers-tech-spec Ready Ready Preview, Comment Jul 22, 2026 10:41pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a deterministic harness-integration Rust crate: typed scenario/recorder/router contracts, process and stack supervision, recorder/readiness/scripted-router services, scenario expansion/compilation, phased execution and reporting, a scenario catalog with CLI. It also persists hook-mutated arguments across held/released calls in harness core, adds Playwright console E2E infrastructure and UI test hooks, and wires CI/Makefile/README for the new suite.

Changes

Harness integration test framework

Layer / File(s) Summary
Core utilities
.../src/canonical.rs, deadline.rs, artifacts.rs, artifacts/*, matcher.rs
Canonical JSON hashing, deadline-bounded polling/timeouts, run-relative artifact persistence, and JSON matcher/normalizer evaluation.
Typed contracts
.../src/types/*, tests/schemas.rs
Versioned authored/compiled scenario, recorder, frame, script, and result schemas with committed golden schema tests.
Process/stack orchestration
.../src/process/*, stack/*, tests/supervisor.rs
Process spec/child/supervisor teardown and stack layout/config/manifest/supervisor boot an isolated engine+worker stack per run.
Recorder & readiness
.../src/recorder/*, readiness/*, tests/readiness.rs
Durable recorder events with response gating and readiness catalog/contract/probe validation of engine discovery/config.
Scripted router & expansion
.../src/scripted_router.rs, client.rs, expand*, fixtures/*, tests/scenario_compilation.rs
A deterministic scripted router plus compile/expand of authored scenarios into router scripts/tools/prompts, validated by fixture/stream tests.
Scenario execution & reporting
.../src/runtime.rs, scenario/*, evidence_data.rs, services.rs, tests/determinism.rs
Phased execution (probe/arm/send/fault/release/await/collect), floor/verify checks, and classified result/execution reporting.
Scenario catalog & CLI
.../src/scenarios/*, main.rs, lib.rs
Typed builders for six registered scenarios plus run/validate/render/serve CLI commands.

Estimated code review effort: 5 (Critical) | ~120 minutes

Hook mutation persistence in harness core

Layer / File(s) Summary
Held argument mutation & checkpoint persistence
harness/src/trigger.rs, types/turn.rs, hooks/runner.rs, functions/function_trigger.rs, turn_loop.rs, deferred.rs
PendingInfo/CallCheckpoint gain held_arguments, hook Hold outcomes carry mutations, and turn_loop/deferred apply mutated arguments through hold/checkpoint/release.

Console E2E testing infrastructure

Layer / File(s) Summary
Harness-stack fixture & specs
console/web/e2e/*, playwright.config.ts, package.json, pnpm-workspace.yaml, tsconfig.e2e.json, vite.config.ts
A Playwright fixture spawns serve-mode harness and new specs exercise durable hydration, exactly-once function calls, and streamed text.
UI test hooks & session merge fix
console/web/src/components/**, hooks/use-conversations.ts
data-* attributes for test selection and a session-list merge that preserves concurrently created sessions.

CI and build tooling for integration tests

Layer / File(s) Summary
CI job, Makefile, and docs
.github/workflows/ci.yml, harness/Makefile, .../integration/Cargo.toml, README.md
CI builds the pinned engine and verifies report hashes; Makefile adds scrapling support and integration targets; crate manifest/README document usage.

Sequence Diagram(s)

sequenceDiagram
  participant Runner as ScenarioRunner
  participant Stack as Stack
  participant Recorder as Recorder
  participant Router as ScriptedRouter
  participant Engine as EngineClient

  Runner->>Stack: boot isolated engine+workers
  Runner->>Recorder: configure + reset
  Runner->>Router: start scripted generations
  Runner->>Engine: probe / arm readiness
  Runner->>Engine: harness::send (scenario trigger)
  Engine->>Router: router::chat
  Router-->>Engine: streamed frames + terminal response
  Engine->>Recorder: TargetCall event
  Runner->>Engine: await harness::status (terminal)
  Runner->>Runner: collect transcript/evidence
  Runner->>Runner: floor::floor_failure / verify
  Runner->>Stack: teardown
  Runner->>Runner: write result.json / execution.json
Loading

Estimated code review effort: 5 (Critical) | ~150 minutes

Possibly related PRs

  • iii-hq/workers#437: Both PRs modify harness/Makefile's worker-stack selection/build/install/restart logic, with this PR layering scrapling venv handling and integration targets on top.

Poem

A rabbit hops through scripted lore,
Deadlines, sinks, and one crash-proof door.
Held arguments now safely wait,
Released with mutations, right on the plate. 🥕
Console e2e blinks awake —
Ship it clean, no crumbs to bake! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.88% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an event-driven integration E2E runner for the harness.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/harness-conformance-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 47 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

ytallo added 5 commits July 17, 2026 10:29
Implements the first slice of the harness conformance spec
(tech-specs/2026-07-15-harness-evaluation/conformance-e2e.md): a
standalone runner at harness/evals/conformance that boots a fresh
isolated stack per scenario (pinned engine + real queue,
session-manager, context-manager, iii-directory, harness), replaces
only the router::* boundary with a strict scripted worker, and grades
public evidence with pure-code invariants.

- Scenarios C-E2E-001 (streamed text) and C-E2E-002 (allowed function
  executes exactly once) pass end-to-end in ~6s each.
- make -C harness conformance-e2e III_BIN=<engine> entry point;
  non-required harness-conformance CI job builds the engine pinned in
  engine.lock (never downloads) and uploads results.
- Fault-injection plumbing (engine SIGKILL + respawn), quarantine flag,
  and the calls_closed invariant land with the runner for
  crash-recovery scenarios.
- Spec updated in place: status header plus implementation-verified
  corrections (system-prompt template hashing, boot order, resolved
  config subset checks, 0-based request_id steps, durable-usage
  semantics, invariant registry).
Adds scenario C-E2E-507 (scenarios/crash-recovery-507): the model
dispatches one allow-listed call whose execution takes 8s; the engine
is SIGKILLed mid-call and respawned from the same config and data
directories. Invariants assert the issue's EXPECTED recovery behavior:
the side effect runs at most once (holds today), the interrupted
function_call is closed by a durable function_result, and the turn
reaches a terminal usable state (both fail today).

Marked quarantine: true — excluded from --scenario all so CI stays
green; run explicitly with --scenario C-E2E-507. Unquarantine when the
fix lands to make it the permanent crash-recovery regression gate.

Observed on engine 0.21.8-next.1 while bringing this up (recorded in
spec note 13): the in-flight harness-turn job stays stranded in the
surviving queue worker's inflight set (attempts 0, 30-minute
visibility timeout), the turn record dies with the engine's in-memory
iii-state, harness::status returns null, and the transcript keeps the
dangling function_call — the session poisoning reported in the issue,
plus a wedge the issue's setup did not hit.

Refs: #507
…505/#506

Console recording (diagnostics only, never an oracle): --console-bin
spawns the console worker per-run; --record-console captures the chat
view via headless system Chrome (playwright-core + tools/console-recorder,
deps preinstalled — the runner never downloads during a test). Send holds
until the recorder page loads so even seconds-long turns are fully on
video; recordings land at scenarios/<id>/console-recording.webm.

Hook-chain scenario support: recorder.extra_functions (run-scoped
controlled functions with declared decisions), scenario bindings
(trigger bindings created after harness boot, held until visible in
engine::registered-triggers::list), scenario release (resolve a held
call via harness::function::resolve once it parks), and payload_subset
on the target.calls invariant.

Quarantined repros (assert EXPECTED behavior; fail until fixed):
- C-E2E-505: a pre-trigger hook returning hold + mutations has the
  mutations discarded at parse (hooks/runner.rs parse_output).
- C-E2E-506: releasing a held call with resolve{action:execute} runs
  the ORIGINAL transcript arguments, skipping hook mutations
  (deferred.rs find_call_arguments).
Both verified live: hook-envelope invariants pass (chain order proven),
the target-payload invariant fails with the exact reported values.

Refs: #505, #506
The harness-evaluation tech spec moved to the iii repo; this branch now
carries only the test implementation. The runner's operational docs and
the implementation-verified spec corrections (system-prompt templating,
boot order vs the registry snapshot, resolved-config subset checks,
0-based request_id steps, durable-usage semantics, invariant registry,
fault seeds, hook-chain scenario support, quarantine policy) live in
harness/evals/conformance/README.md; code/CI references updated.
Drop the changed-paths gate (with engine and worker builds cached the
job is cheap enough to gate every PR on the public harness contracts)
and run the conformance crate's unit tests in the job — the crate has
no iii.worker.yaml, so the discover-driven rust matrix never covers it.
Document the quarantine lifecycle in the README: repro scenarios are
un-quarantined in the same PR as their fix.
ytallo added 8 commits July 18, 2026 14:59
The default --artifacts-dir is relative and children are spawned with
per-run working directories, so the engine resolved its generated
config path against the wrong cwd and the documented direct CLI could
not boot (CI masked it by passing an absolute path). Canonicalize the
artifacts dir and every CLI-supplied binary before anything spawns.
Adding scrapling to STACK broke build, install-local and cargo-clean,
which assume every worker has a Cargo.toml and a Rust binary. Split
STACK into RUST_WORKERS/PYTHON_WORKERS: cargo targets iterate the Rust
subset, build routes scrapling to prepare-scrapling, install-local and
cargo-clean skip it with an explicit message.
head() sliced the canonical JSON at byte 200, which panics when the cut
lands inside a multibyte character — exactly while reporting a mismatch,
with the router mutex held (poisoning follow-up evidence collection).
Back the cut off to a char boundary and pin it with a regression test.
…tract

The frame mirror required partial on every delta variant, but llm-router
defines it as Option and emits slim deltas today — a valid slim fixture
was rejected and the streaming scenario only ever exercised the legacy
fat form. Mirror the Option, regenerate the golden schemas, and switch
C-E2E-001 to slim deltas so the E2E covers the format providers emit.
An empty scenarios dir (or one with every scenario quarantined) ran
zero scenarios and exited 0, letting the CI gate silently become a
no-op. Empty selection is now a runner_error (exit 3) in both run and
--validate-only modes. Also cover both delta wire forms (slim and
legacy fat) in the fixture validation tests.
…crash

harness::send and function::resolve failures returned contract_failure
without consulting stack.early_exit(), so an engine or harness crash
mid-RPC produced exit 2 instead of the documented process_crash/exit 3.
Check for a dead child on both RPC error paths and once more before the
final classification — a stack that did not outlive Collect cannot be
trusted for grading, and an early exit outranks an ordinary timeout
(spec step 7).
The generic "token" key fragment rejected the mandatory
max_output_tokens model field — i.e. every structurally valid cassette;
the clean-path test only passed because it scanned a reduced object.
Match credential token keys by exact name (folded -/_) instead, keep
the other fragments, and make the clean-path test scan a complete
RouterCassetteV1.
transcript.calls_closed is vacuously true when the crash eats the
function_call message entirely, and nothing required the second
recovery generation to be consumed. Require the full recovered turn:
message counts, a durable result closing call-1, the "recovered"
assistant text, and both scripted generations consumed.
main's harness now stamps `terminal: bool` on turn-completed payloads
(false = non-final completion while a wake is armed). The recorder's
strict lifecycle parse rejected the unknown field, so no lifecycle
delivery was ever recorded and every scenario failed the floor in CI —
the suite catching a real wire-contract change, as designed.

LifecycleEventV1 gains the required `terminal` field, and the floor now
also requires `terminal: true`: these scenarios run single, final turns.
… observe driver (#556)

* (MOT-4107) refactor(integration): simplify boot readiness and add the observe driver

Boot readiness moves from structural contract probing (readiness/, ~850
lines: golden schemas, config-entry diffing, queue-topic checks) to
presence-only discovery (discovery.rs, ~90 lines): wait for the harness
function surface plus the workers it calls mid-turn to be registered,
nothing more. Completion is now event-driven — Arm binds
harness::turn-completed once and Await blocks on delivery instead of
polling harness::status every 250ms, with one status call afterward as
the durable-state confirmation the floor checks.

Replaces the Console/serve driver with Observe: the integration keeps
owning stimulus (harness::send after a start.json signal) and Playwright
owns the Console process and DOM assertions directly, spawning
CONSOLE_BIN itself instead of asking the integration binary to do it.
--console-bin, spawn_console, and the HTTP-port plumbing are gone from
the crate; ObserveReadyV1/ObserveResultV1 replace the old serve types.
Console e2e specs call stack.start() instead of
stack.trigger('harness::send', ...).

Also:
- Router function goldens are read directly from
  llm-router/tests/golden/schemas/*.json via include_str! instead of a
  local copy embedded in readiness contracts.
- --repeat/INTEGRATION_REPEAT is gone (CLI, Makefile, CI): the
  byte-stable scrub is covered by tests/determinism.rs, so booting the
  full stack twice per scenario was a meta-test, not a contract check.
- ModelFixtureV1 drops pricing, reasoning_efforts, thinking_budgets,
  input_limit, and display_name — fields the compiler always left None
  and nothing downstream (the scripted router or the harness's own
  Model parse) ever reads.
- Recorder::snapshot() drops its unused after_sequence filter; a few
  internal-only helpers (Client::call_with_timeout, Deadline::at/cap,
  the stack config re-exports) move to pub(crate) or private;
  readiness_deadline is renamed setup_deadline now that the module it
  named is gone.

No scenario behavior change. Validated with cargo fmt/clippy (clean),
cargo test (96 passing), and live Direct-driver runs (E2E-001, E2E-002)
against a local stack.

* (MOT-4107) refactor(integration): focus conformance suite on core flows

* (MOT-4107) refactor(integration): add typed scenario DSL

* (MOT-4107) refactor(harness): move integration runner into test workspace

* (MOT-4107) test: stabilize shell and integration process tests
Adds UI-002 (multi-turn-traces): a function-call turn followed by a
second Console-triggered turn on the same session must stay two
distinct, durable traces rather than collapsing into one.

- DSL: Scenario::terminal_turns(count) declares how many discrete
  terminal turns a fixture expects; Request::turn_request_step(n) pins
  a generation's request id to a specific turn instead of the
  continuous single-turn ordinal.
- floor::lifecycle_failure takes the expected turn count: at-least-once
  retries for the same turn_id are accepted if their normalized
  payloads are identical, but distinct turns must each appear, stay
  ordered, and the latest must bind to run.turn_id.
- Recorder::wait_for_lifecycle_turns(expected, deadline) waits for N
  distinct turn ids instead of the first lifecycle event;
  wait_for_lifecycle is now its single-turn special case.
- Playground driver: refresh_external_completion waits for every
  declared terminal turn on shutdown (Playground can keep running
  after its first completed turn) before confirming durable status.
- Console: GroupedTraceList exposes data-trace-group-{value,count,
  errors} for the new Playwright assertions; ChatDock's default width
  is now half the viewport instead of a fixed 440px, giving the wider
  grouped trace view room to breathe.

Validated with cargo fmt/clippy (clean) and cargo test (77 passing) in
harness/tests/e2e; tsc typecheck (app + e2e), vitest (1052 passing),
and biome (clean) in console/web.
@ytallo ytallo changed the title (MOT-4107) feat(harness): integration E2E runner (first slice) + issue repro scenarios (MOT-4107) feat(harness): event-driven integration E2E runner Jul 22, 2026
@ytallo
ytallo merged commit 81d14fd into main Jul 22, 2026
50 checks passed
@andersonleal
andersonleal deleted the feat/harness-conformance-e2e branch July 23, 2026 11:30
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.

2 participants