Skip to content

Add detection-layer eval fixtures replayed from real cluster states - #12

Open
ericjohanson-langchain wants to merge 1 commit into
mainfrom
feat/detection-eval-fixtures
Open

ericjohanson-langchain wants to merge 1 commit into
mainfrom
feat/detection-eval-fixtures

Conversation

@ericjohanson-langchain

Copy link
Copy Markdown
Contributor

The 31-example dataset in evals/ describes incidents in prose and checks whether the interactive agent reasons about them correctly. It cannot reach the collector — and the collector is where every production bug so far has lived.

Bug shipped Reachable by a prose-scenario eval?
restarts >= 5 flagging healthy pods No
Stale events about deleted pods No
Collector gathering no utilization data No
Collector gathering no PVC data No
OverallSeverity rejecting info No
Fixed-scale HPA read as at-max No

None of those involve the agent reasoning. They are detection failures, and detecting them needs real cluster state, replayed.

What a fixture is

A recorded cluster state plus a hand-written expectation, replayed through the production _classify_pod, event_is_current, and _format_snapshot.

Relative time, not absolute. Timestamps are stored as minute offsets from capture and reconstructed against a fixed now. The classifier is recency-based, so absolute timestamps would make a fixture silently change verdict as it aged. There is a test asserting two replays at different now values agree.

Allowlist projection, not redaction. Only fields the classifier and renderer read are copied out of the Kubernetes objects, so annotations, env values, mounted secret names, and image references cannot reach a fixture by construction. Event messages are dropped wholesale rather than sanitized, since they are free text from arbitrary controllers and can carry hostnames, IPs, and connection strings. This repo is public, so capture_snapshot.py redacts namespace, pod, and node names to stable pseudonyms by default and --no-redact prints a warning.

A refactor this forced

event_is_current() is now extracted from _collect_cluster_data. The age and deleted-pod checks were inline, which meant a fixture could not exercise them: replay copied post-filter events straight through, and the first version of this suite passed a stale event. Both the collector and the replay harness now call the same predicate.

Does it actually catch anything

The first fixture reconstructs the 2026-08-11 incident: 10 pods Running and Ready with 18-43 lifetime restarts, every last termination Completed exit 0, plus a 51-minute-old InvalidImageName event for an already-deleted pod.

Verified by monkeypatching the pre-fix classifier back in:

with the FIXED classifier:        unhealthy: 0
with the OLD (buggy) classifier: unhealthy: 10   <- fixture fails, names the bug

171 tests passing.

Not in this PR

Capturing further fixtures from the live cluster needs cluster credentials. Auto-recording critical findings to a LangSmith dataset is blocked separately: LANGSMITH_API_KEY is in neither the cluster secret nor the deployment env, which is why the pod logs LangSmithMissingAPIKeyWarning.

The 31-example dataset in evals/ describes incidents in prose and checks whether
the interactive agent reasons about them correctly. It cannot reach the collector,
and the collector is where every production bug so far has lived: restart_count
treated as a fault signal, stale events about deleted pods, missing utilization
and PVC data, an HPA read as saturated because min equals max. None of those are
reachable from a scenario string, because none of them involve the agent
reasoning. They are detection failures.

This adds the missing layer. A fixture is a recorded cluster state replayed
through the production _classify_pod, event_is_current, and _format_snapshot, with
a hand-written expectation of what the right answer is.

Two design choices worth keeping.

Relative time, not absolute. Every timestamp is stored as an offset in minutes
from capture and reconstructed against a fixed `now`. The classifier is
recency-based, so a fixture holding absolute timestamps would silently change
verdict as it aged, and a regression test that drifts is worse than none. There is
a test asserting two replays at different `now` values agree.

Allowlist projection, not redaction. Only the fields the classifier and renderer
read are copied out of the Kubernetes objects, so annotations, env values, mounted
secret names, and image references cannot reach a fixture by construction. Event
messages are dropped wholesale rather than sanitized, since they are free text
from arbitrary controllers and can embed hostnames, IPs, and connection strings.
This repository is public, and capture_snapshot.py redacts namespace, pod, and
node names to stable pseudonyms by default; --no-redact prints a warning.

Also extracts event_is_current() out of _collect_cluster_data. The age and
deleted-pod checks were inline, which meant a fixture could not exercise them at
all: replay copied post-filter events straight through and the first version of
this suite passed a stale event. Both the collector and the replay harness now
call the same predicate.

First fixture reconstructs the 2026-08-11 incident: 10 pods Running and Ready
with 18-43 lifetime restarts, every last termination Completed exit 0, plus a
51-minute-old InvalidImageName event for an already-deleted pod. Verified it does
what it is for by monkeypatching the pre-fix classifier back in: 0 unhealthy with
the current code, 10 with the old, so the fixture fails and names the bug.

7 fixture tests plus the parametrised suite. 171 passing overall.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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