Skip to content

test(gemini): live cassette hook-system stress suite - #2013

Merged
gold-silver-copper merged 2 commits into
mainfrom
test/gemini-hook-system-stress
Jul 5, 2026
Merged

test(gemini): live cassette hook-system stress suite#2013
gold-silver-copper merged 2 commits into
mainfrom
test/gemini-hook-system-stress

Conversation

@gold-silver-copper

@gold-silver-copper gold-silver-copper commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

A Gemini cassette-backed stress-test suite for the merged hook system (v2, #2012): 30 long, realistic multi-turn workflows recorded against a real Gemini model and replayed deterministically (no API key needed for replay). Five cassette files under tests/providers/gemini/cassette/ (hook_stress, hook_stress_context, hook_stress_patch, hook_stress_tools, hook_stress_streaming) + 31 cassette fixtures, driven by a shared deterministic-fixtures module (hook_stress_support.rs). No production code changes.

Inspiration-library test patterns studied

I studied how six libraries test their hook/middleware/callback/guardrail systems and adopted the transferable assertion strategies (a recorded real Gemini is the deterministic script the fake-model scripting can't be):

Library What their hook tests stress Strategy adopted
LangChain (v1 agent middleware) onion order, override immutability, tool arg-rewrite/skip, PII redaction Paired positive+negative redaction; ordered call-log
LangGraph (prebuilt/pregel) pre/post-model hooks, tool interceptor Flow matrix Prove mutations via downstream observable effect (Gemini won't echo settings)
OpenAI Agents (lifecycle/guardrail) callback counts+order, guardrail short-circuit Zero-downstream invariant for skip/terminate; per-event scratchpad counters
pydantic-ai (capabilities/TestModel) log-spy hook, capture-what-reached-the-model The cassette is the deterministic multi-turn script
Semantic Kernel (filters) registration-order breadcrumb, skip zero-downstream Execution-order breadcrumb (event, turn); paired counters
Vercel AI SDK (generate/stream-text) multi-step loop, per-step callbacks, override precedence Ordered lifecycle event list; matrix the RequestPatch merge/precedence

What the new Gemini hook stress tests cover

hook_stress.rs (6) — the original core: HookContext + Scratchpad across a long chain; RequestPatch extra_context + active_tools narrowing; chained RewriteArgs→observe→RewriteResult redaction; streaming lifecycle ordering + is_streaming parity; per-turn atomic call/result pairing; Skip zero-execution.

hook_stress_context.rs (8) — HookContext identity (stable run_id, advancing turn, is_streaming, agent_name incl. the unset case); a shared Scratchpad written by one hook and read by a second, growing across turns; internal_call_id correlation of ToolCallToolResult; two observe-only hooks both firing; add_hook appending across builder + request; CompletionCall patch accumulation; active_tools set-intersection across two narrowing hooks.

hook_stress_patch.rs (4) — preamble override; tool_choice=Required (first turn only); per-turn history replacement injecting a prior fact; a multi-field patch (preamble + extra_context).

hook_stress_tools.rs (6) — single-key and chained RewriteArgs; chained RewriteResult (redact→wrap) and truncation; Terminate from a ToolResult (post-execution — the tool body ran first); and model-driven recovery from a tool error (the tool errors with guidance → the model retries → succeeds).

hook_stress_streaming.rs (6) — TextDelta / StreamResponseFinish / ModelTurnFinished on the streaming surface; ModelTurnFinished on tool turns; RewriteResult redaction reaching the FinalResponse; active_tools narrowing and Skip on the streaming driver; and blocking-vs-streaming answer parity (two cassettes).

Aspects covered: HookContext (all accessors + unset name), Scratchpad (cross-hook, cross-turn, keyed correlation), HookStack composition (multi-hook order, both-fire, add_hook append, chained rewrites, patch accumulation, active_tools intersection), RequestPatch (preamble / temperature / tool_choice / active_tools / extra_context / history), tool lifecycle (arg rewrite / result redact-wrap-truncate / skip / terminate-on-result / tool-error recovery / execution start), streaming lifecycle + blocking-vs-streaming parity.

Bugs found and fixed

No hook-system bugs. Every scenario passed on live recording, confirming the merged system behaves as documented. One footgun surfaced: forcing tool_choice=Required on every turn loops until max_turns (each turn re-forces a tool call). It's expected per the per-turn/re-fire semantics, so rather than change production code I captured the intended pattern with a FirstTurnPatch fixture and a comment. No production code changed.

Cassette safety

  • Recorded with RIG_PROVIDER_TEST_MODE=record + GEMINI_API_KEY, then auto-scrubbed and safety-checked by the recorder before writing (the run fails if unsafe artifacts remain).
  • Manually reviewed: key query param → [REDACTED]; thoughtSignature/responseId → placeholders; grep across all fixtures for x-goog-api-key/gemini_api_key/AIza/authorization/bearer/set-cookie and the raw key value all return empty.
  • Descriptive names, deterministic prompts (low temperature, simple arithmetic tools), targeted record commands only.

Commands run

  • Record (targeted, live): RIG_PROVIDER_TEST_MODE=record cargo test -p rig --all-features --test gemini hook_stress_<mod> -- --test-threads=1 → all passed.
  • Replay (no key): cargo test -p rig --all-features --test gemini hook_stress -- --test-threads=130 passed (0.18s, deterministic).
  • Full provider suite: cargo test -p rig --all-features --test gemini gemini::cassette -- --test-threads=1115 passed, 0 failed.
  • cargo fmt (clean) · cargo clippy -p rig --all-features --test gemini (clean).

Remaining risks / flakiness considerations

  • Replay is byte-identical/deterministic (hooks use no clocks/RNG). Variability exists only at re-record time, which is why model-shaped values use loose assertions — a re-recording with a different call count/ordering still passes.
  • Deferred as hard-to-force-live or already covered: invalid-tool-call recovery (Retry/Repair/Skip/Fail via unregistered/disallowed calls — Gemini won't emit those on purpose; covered by agent_run_recovery.rs + rig-core agent::runner unit tests) and structured-output Tool-mode finalization (structured_output.rs + rig-core unit tests for ModelTurnFinished content and the output-tool stream item). Per the guidance to prefer a few rich workflows over many flaky tiny live tests.

Adds a Gemini cassette-backed stress suite
(tests/providers/gemini/cassette/hook_stress.rs) that exercises the merged hook
system (v2, #2012) across long, realistic multi-turn workflows recorded against
real Gemini and replayed deterministically:

- HookContext identity (stable run_id, advancing turn, is_streaming, agent_name)
  and a shared Scratchpad threaded across two cooperating hooks and many turns.
- RequestPatch: extra_context injection + active_tools narrowing + temperature,
  proven by downstream effects (the injected fact reaches the answer; the
  filtered-out tool never executes).
- Chained tool lifecycle: RewriteArgs -> observe -> RewriteResult redaction, with
  paired positive/negative assertions (the marker reaches the model; the raw
  result does not; the transcript keeps the model's original args).
- Streaming lifecycle ordering (tool call -> execution start -> tool result ->
  final response) and is_streaming parity vs the blocking surface.
- Per-turn atomic call/result pairing and the Skip zero-execution invariant.

Assertions follow tools_support's loose-assertion convention (exact equality only
for rig-synthesized values), so the cassettes survive re-recording; hooks are
deterministic so replay requests stay byte-identical. Cassettes are auto-scrubbed
and safety-checked by the recorder (key -> [REDACTED]; ids/signatures
placeholdered) and were reviewed manually.

No hook-system bugs surfaced: every scenario passed on the first live recording,
confirming the documented behavior. Inspired by how LangChain, LangGraph, OpenAI
Agents, pydantic-ai, Semantic Kernel, and the Vercel AI SDK test their
hook/middleware/guardrail systems (ordered breadcrumb logs; proving mutations via
downstream effects; paired positive+negative redaction; zero-downstream skip
invariants).
Broadens the Gemini hook-system stress suite from 6 to 30 recorded workflows
across four themed cassette files, driven by a shared deterministic-fixtures
module (hook_stress_support.rs):

- hook_stress_context (8): HookContext identity (stable run_id, advancing turn,
  is_streaming, agent_name incl. the unset case); a shared Scratchpad written by
  one hook and read by a second, growing across turns; internal_call_id
  correlation of ToolCall/ToolResult; two observe-only hooks both firing;
  add_hook appending across builder + request; CompletionCall patch accumulation;
  and active_tools set-intersection across two narrowing hooks.
- hook_stress_patch (4): preamble override; tool_choice=Required (first turn
  only); per-turn history replacement injecting a prior fact; multi-field patch
  (preamble + extra_context).
- hook_stress_tools (6): single-key and chained RewriteArgs; chained
  RewriteResult (redact -> wrap) and truncation; Terminate from a ToolResult
  (post-execution); and model-driven recovery from a tool error.
- hook_stress_streaming (6): TextDelta / StreamResponseFinish / ModelTurnFinished
  on streaming; ModelTurnFinished on tool turns; RewriteResult redaction reaching
  the FinalResponse; active_tools narrowing and Skip on the streaming driver; and
  blocking-vs-streaming answer parity (two cassettes).

All recorded live against Gemini and replaying deterministically; every patch /
rewrite / skip effect is proven by a downstream-observable change, and
model-shaped values use loose assertions (exact only for rig-synthesized ones).
Cassettes are auto-scrubbed + safety-checked by the recorder and were reviewed.

A footgun surfaced along the way: forcing tool_choice=Required on *every* turn
loops until max_turns (each turn re-forces a tool call). Captured with a
first-turn-only patch fixture (FirstTurnPatch) so the intended pattern is shown.
No hook-system bugs found; every scenario confirms the documented behavior.
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Jul 5, 2026
Merged via the queue into main with commit 562353a Jul 5, 2026
6 checks passed
This was referenced Jul 5, 2026
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