feat: agent-proposed test cases from a captured trace - #497
Merged
Conversation
An expected output that is a tool call carries zero content parts, so the content-only comparison scored any tool-free response as a pass. Compare ToolRequests as an unordered multiset, ignoring the call id (expected ids are minted locally by TestSuiteDtoMapper, actual ids come from the provider) and comparing arguments as canonical JSON. Existing cases with tool-call expectations are judged for real from here, so some will correctly start failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
…tion Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
…ersation Adds the ITestCaseSynthesisService seam, the proposal contracts, the test_case_synthesizer prompt, and the system-agent call. Refinement is a real Conversation rather than a re-prompt, so a follow-up instruction makes the agent revise its previous answer instead of starting over. Nothing is persisted: proposals are a suggestion surface, not an entity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Drops proposals naming a call outside the conversation, a call with no response, or a correction with no expected answer; flags an unpassable correction and an unknown tool; dedupes by (call, kind) and caps by relevance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Project-scoped like its siblings (404, never 403, so an id is not an existence oracle) and gated by RequiresFeature(TestCaseSynthesis). Read-only: writes stay on the existing test-suite endpoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
The proposal enums are named TestCaseProposal* because `ProposalKind` is already taken in models.ts by the OPTIMIZATION proposal kind. C# keeps the two apart by namespace; this module is flat, so the distinction lives in the name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
The trace detail gains a "Generate tests" action opening a review panel: the conversation on the left, ranked candidates on the right, a free-text instruction at the bottom. Generation never runs on open — opening a modal must not spend tokens on the project's system endpoint. Extracts SuitePicker out of PromoteModal (keeping its existing testids) so both modals share one destination picker. i18n: the 25 new strings are extracted into the English source catalog but untranslated in de/es/fr/it — i18n:translate needs I18N_TRANSLATE_API_KEY, which is not set in this environment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
…e cases The agent's scoring suggestion is approved separately from the cases, with the blast radius stated: attaching a judge scores every case already in the suite, because a case passes only when EVERY attached evaluator passes. The new-suite alternative sidesteps that, and is hidden when MaxTestSuites leaves no room. Splits the write side into useApproveProposals and the candidate column into ProposalsPane to stay inside the 120-line hook and 150-line component limits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
A read tool over the same endpoint the Generate-tests panel uses, so the chat path gets the same turn selection. Its digest carries agentCallId + kind per candidate because those are exactly what add_to_suite / create_suite need next — the model must be able to act without a second read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Covers the AI docs (optimization-loop Stage 1, domain-concepts, architecture, TRACEY.md), the user manual (test-suites-and-cases, capturing-traces, tracey, evaluators), and the changelog. The manual already explained the unpassable-correction trap for hand promotion; the new section notes that the generate flow avoids it for you. The evaluators page carries an upgrade warning, since existing tool-call expectations were asserting nothing and some cases will correctly start failing. Screenshots still owed — they need the kiosk stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
The panel holds the hook's `abort` as `useEffect(() => abort, [abort])` so that closing it tears down an in-flight generation instead of billing the user for a request they walked away from. But `abort` was rebuilt on every render, so the effect saw a changed dependency on EVERY re-render and ran its cleanup — killing the request that was still running. Any state change during generation triggered it; in the browser the conversation query settling was enough, so the panel never produced a single proposal. `useCallback` gives the identity the effect assumes. The existing specs all resolve the mocked endpoint synchronously, which closes the window entirely — hence a real-browser bug under a green unit suite. The new spec defers the response and switches the destination suite mid-flight, which is the same re-render with no timing to arrange. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Drives the whole Generate-tests flow against the live stack: a seeded three-turn refund conversation, open the deciding trace by ?trace=, generate, approve, and read the destination suite back over the API. This is what caught the abort bug fixed in the previous commit — the panel could never produce a proposal in a real browser while every unit spec stayed green. Runs in its own `llm-generate-tests` project since synthesis is a real model round-trip. The model chooses how many candidates to return (one and two have both been observed for the same conversation), so the spec approves up to two and asserts the suite grew by exactly what was ticked, rather than betting on a count the model never promised. Also gates the new feature on the Free tier: POST test-case-proposals must 402. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
test-driven-improvement's job is to turn a reported defect into a regression test that can actually go green, and the step that decides whether it can is choosing WHICH call to correct. A turn that used tools is several calls; correcting the last one produces a case that is unpassable by construction, and reads exactly like a fix that did not work. The playbook asked the model to pick by eye. It now asks propose_test_cases, passing the user's complaint as `instruction`: the synthesis agent targets the deciding call and flags an unpassable correction before anything is written. A flagged candidate is a stop, not a note. When it also suggests a judge, step 6 starts from that rather than inventing one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
The point of routing test-driven-improvement through propose_test_cases is that it must correct the call that DECIDED, not the summary that reports it — and that is a model judgement, so only a live run can tell you whether the wording works. This scenario puts the trap in front of it: a two-call refund loop where the flagged `Unpassable` candidate is ranked first. Verified against the kiosk endpoint. The baseline (HEAD~1) writes the case against the summary call, exactly the failure the playbook exists to prevent; the working copy calls propose_test_cases, says out loud that the final trace is flagged, and writes against the deciding call. The run also caught the playbook offering to "move to step 8" — its own step numbers are not the user's vocabulary. Guardrail added and re-verified. Scenario-local fixtures because the shared ones contradict what the scenario writes (add_to_suite echoes a hardcoded agentCallId, set_suite_evaluators reports a set it was not sent, get_case_results answers about a different run), which reads as a prompt failure when it is a harness one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
Two shots from the kiosk stack: the panel with two approved candidates and the editable expected tool call, and a candidate carrying the unpassable warning. The second earns its place — "a correction on a summary turn can never pass" is the hardest thing on this page to picture from prose, and the warning states it in the words the product uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
i18n:translate wanted its own I18N_TRANSLATE_API_KEY, so a machine with a working kiosk endpoint still could not translate — and the catalogs shipped untranslated because nobody had the second credential to hand. The KIOSK_LLM_* trio in the repo-root .env is already this machine's general-purpose local model config: the kiosk compose file and the prompt-lab skill both read it. Translation now does too. Precedence is process env, then that .env; I18N_TRANSLATE_* still wins and takes the WHOLE endpoint when set — a base URL borrowed from the kiosk while the key came from elsewhere would send that key somewhere it does not belong. The run prints the model and endpoint it resolved, so which one answered is never a guess. The .env read is a six-line parser rather than a dependency: npm runs this script directly, so nothing loads the file for it, and Compose's KEY=value format needs no more than that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
148 strings (37 x de/es/fr/it), machine-translated through the kiosk endpoint. Reviewed the diff rather than trusting it: ICU placeholders and Lingui tags are intact across all 7,916 translated entries, and the do-not-translate glossary holds except for two French strings where "tool" came back as "outil" — corrected by hand to match the catalog's existing "appel de Tool". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i3fSHNnyWiHqi5dYaFxf9
| const glossary = loadGlossary() | ||
| const { apiKey, baseURL, modelId } = makeModel() | ||
| const { apiKey, baseURL, modelId, source } = makeModel() | ||
| console.log(`Translating with ${modelId} via ${baseURL || 'api.openai.com'} (${source}).`) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Generate tests to the trace detail panel: an agent reads a trace's whole conversation and
proposes the test cases actually worth building — the turns where the agent decided something,
not every turn — as GREEN promotions (lock in what it did) or RED corrections (assert what it
should have done), each with its reasoning. Candidates are reviewed beside the transcript, expected
output is editable, and a plain-language instruction bar refines the round. Turns it passed over
are listed with the reason, so its judgement is auditable rather than opaque.
This replaces the single-click Add test promote dialog, which did a strict subset of the same
job. Traces can still be added to a suite without the agent via Add from traces on the Test
Suites page — that route stays un-gated. Generation itself is Enterprise (
TestCaseSynthesis).What's in it
The unpassable-correction trap. A correction built on the last call of a tool loop can never
pass: that call's input already contains the tool calls and their results, so the only thing
left to grade is the closing summary. Such proposals are flagged, left unchecked, and the agent is
instructed to target the call whose own response holds the decision. Tracey's test-driven
improvement playbook now routes through
propose_test_casesfor exactly this reason — it decideswhether the resulting regression test can ever go green — and a prompt-lab scenario probes the live
model to confirm the wording holds.
Evaluator suggestion. When the destination suite's evaluators cannot score what was proposed,
the panel offers an agentic judge: added to that suite (blast radius stated up front, since a
suite's evaluators score every case in it) or carried by a new suite instead. Each of the three
answers names its consequence.
Exact Match now compares tool calls. An expected output that is a tool call carries no text
content, so the previous content-only comparison scored any tool-free response as a pass — a
tool-call expectation asserted nothing. Calls are now compared as an unordered multiset (parallel
calls have no meaningful order), ignoring the provider-generated call id and comparing arguments as
canonical JSON so
{"amount": 40}matches{"amount": 40.0}. Existing cases with tool-callexpectations are judged for real from this release, so some will correctly start failing. A/B
validation is unaffected; historical pass rates are not recomputed, so trend lines may show a step.
Sampling parameters reach the provider. Reasoning effort was mapped onto the outgoing request
and then silently dropped before it left the process — the playground's control did nothing, with
no error. Now sent for real. (
nis still dropped; tracked separately.)~3x faster generation. The panel spent its wait on hidden reasoning it did not need — several
thousand thinking tokens for a few hundred tokens of answer, 25–44s for a four-call conversation.
It now asks the model not to reason (8–13s for the same proposals) and retries without the setting
on models that lack it, so the worst case is the old speed rather than an error. The candidate
column names what it is doing and shows a running clock while a round runs.
Layers touched
ToolRequestMatch,ExactMatchEvaluatorTestCase/synthesis service, budgetedConversationTranscript,ProposalValidator(proposals validated against the real conversation)POST /api/agent-calls/{id}/test-case-proposalsModelClient/ChatClientExtensionssampling parametersTestCaseSynthesis(Enterprise)trace-detail/synthesis/*,SynthesizeTestsModal, Traceypropose_test_casestool +
ProposedCasesToolUI, Toast/Radio primitivesDocs
docs/optimization-loop.md,docs/licensing.md,docs/architecture.md,docs/i18n.md,docs/commands.md,frontend/docs/BEST_PRACTICES.md,frontend/docs/TRACEY.md; manual pages fortest suites, evaluators, traces and Tracey, with two new kiosk screenshots of the panel (including
the unpassable warning). CHANGELOG
[Unreleased]covers all of the above.Also here:
i18n:translatenow falls back to the repo-rootKIOSK_LLM_*config, and the fivecatalogs are translated (148 strings; French "outil" hand-corrected to the catalog's existing
"appel de Tool").
Tests
Backend unit + API tests for synthesis, transcript budgeting, proposal validation, tool-request
matching and the sampling parameters; frontend specs for the modal, selection rules and Toast; an
e2e spec generating test cases from a trace and adding them to a suite, plus a licensing-gate spec.
🤖 Generated with Claude Code
https://claude.ai/code/session_01SivSrf9XfAP6Ds1jMCMukB