mirror of
https://github.com/lukaszraczylo/llm-testbench.git
synced 2026-10-12 00:45:19 +00:00
Bring agents/tool-routing and agents/planning up to 10 tests each. Add a new agents/delegation subcategory with 10 tests. Split agents.go into agents_toolrouting.go, agents_planning.go, and agents_delegation.go, each with a sibling _test.go file, and reduce agents.go to a thin registration aggregator. Every new prompt is self-contained and forces a JSON or number-only answer. Set-valued answers use JSONStringSet or a custom stage-set evaluator so no arbitrary order is required. Dependency-graph fixtures that force a single valid order are verified unique in the unit tests by brute-force enumeration over every permutation. Numeric ground truth is derived by running the same algorithm the fixture describes, then cross-checked against an independently hand-derived value. MaxTokens is omitted on all 28 new tests per the expansion rules; the 2 existing tests keep their prior value unchanged.
426 lines
17 KiB
Go
426 lines
17 KiB
Go
package tests
|
|
|
|
import (
|
|
"github.com/lukaszraczylo/llm-testbench/internal/eval"
|
|
"github.com/lukaszraczylo/llm-testbench/internal/testkit"
|
|
)
|
|
|
|
func registerAgentDelegationTests(r *testkit.Registry) {
|
|
r.Register(delegTaskToSpecialistTest())
|
|
r.Register(delegBuildThenVerifyTest())
|
|
r.Register(delegHandoffContextTest())
|
|
r.Register(delegVerifyVsTrustTest())
|
|
r.Register(delegBatchVsSeparateTest())
|
|
r.Register(delegMainThreadVsDelegateTest())
|
|
r.Register(delegReviewerIndependenceTest())
|
|
r.Register(delegEscalationToHumanTest())
|
|
r.Register(delegParallelDispatchSafetyTest())
|
|
r.Register(delegMinimalPrivilegeTest())
|
|
}
|
|
|
|
// delegRoster is the inline specialist-agent roster shared by every
|
|
// delegation test. It mirrors real specialist agents used in this kind of
|
|
// workflow: a builder, an independent reviewer, a test author, a security
|
|
// auditor, an infra debugger, a docs author, a performance specialist, a
|
|
// release specialist, an external-only researcher, and a coordinator.
|
|
const delegRoster = `- code-writer: Implements new features or bug fixes by writing and editing
|
|
source code directly.
|
|
- code-reviewer: Reviews a diff or PR for correctness, style, and risk.
|
|
Does not write code itself.
|
|
- test-writer: Writes new automated tests (unit or integration) for
|
|
existing or new code.
|
|
- security-reviewer: Audits code or config for security vulnerabilities
|
|
(injection, auth, secrets handling).
|
|
- devops-debugger: Diagnoses failing deployments, CI pipelines, and
|
|
infrastructure incidents.
|
|
- docs-writer: Writes and updates prose documentation (READMEs, guides,
|
|
design docs).
|
|
- performance-engineer: Profiles and optimizes runtime performance and
|
|
resource usage.
|
|
- release-manager: Manages version tagging, changelog generation, and
|
|
release publishing. Does not write or modify application code.
|
|
- web-researcher: Searches the public web and summarizes external
|
|
information. Does not read or write anything in the repository.
|
|
- orchestrator: Plans and sequences a multi-agent workflow, dispatching to
|
|
the other specialists. Does not do specialist work itself.`
|
|
|
|
// delegTaskToSpecialistTest: map 4 tasks to the correct specialist from the
|
|
// roster.
|
|
//
|
|
// ground truth: diagnosing a stuck Kubernetes deploy is an infrastructure
|
|
// incident (devops-debugger, not code-writer, since nothing is known yet to
|
|
// be a code bug). Writing new unit tests for an evaluator is exactly
|
|
// test-writer's job, not code-writer's. Checking password hashing is a
|
|
// security audit (security-reviewer), not code-reviewer's general
|
|
// correctness/style/risk pass. Publishing a version with a generated
|
|
// changelog is release-manager's job specifically because it excludes
|
|
// writing code.
|
|
func delegTaskToSpecialistTest() testkit.Test {
|
|
prompt := `You can delegate to this roster of specialist agents:
|
|
|
|
` + delegRoster + `
|
|
|
|
For each of these 4 tasks, decide which single specialist is the correct
|
|
one to dispatch to:
|
|
|
|
task1: "Investigate why the Kubernetes deployment of graphann-web is stuck
|
|
in ImagePullBackOff."
|
|
task2: "Write unit tests covering the new cosine-similarity evaluator's
|
|
edge cases."
|
|
task3: "Check whether the new /admin login endpoint properly hashes
|
|
passwords with bcrypt before storing them."
|
|
task4: "Publish v1.4.0 with a changelog generated from the merged commits
|
|
since v1.3.0."
|
|
|
|
Respond with only a JSON object mapping each task id to the specialist
|
|
name: {"task1":"...","task2":"...","task3":"...","task4":"..."}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("task1", "devops-debugger"),
|
|
eval.JSONField("task2", "test-writer"),
|
|
eval.JSONField("task3", "security-reviewer"),
|
|
eval.JSONField("task4", "release-manager"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-task-to-specialist",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Map 4 tasks to the correct specialist agent from a 10-agent roster.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegBuildThenVerifyTest: pick the two specialists, in order, for a
|
|
// build-then-independently-verify workflow.
|
|
//
|
|
// ground truth: "implemented" is code-writer's job. "independently checked
|
|
// for correctness and risk before merge" is code-reviewer's roster
|
|
// description verbatim - not test-writer, which writes tests rather than
|
|
// reviewing the implementation, and not the same code-writer again, which
|
|
// would not be independent.
|
|
func delegBuildThenVerifyTest() testkit.Test {
|
|
prompt := `You can delegate to this roster of specialist agents:
|
|
|
|
` + delegRoster + `
|
|
|
|
Task: "A new caching layer needs to be implemented, and then someone
|
|
independent needs to check it is correct and low-risk before it merges."
|
|
|
|
Which TWO specialists should handle this, in order? Respond with only a
|
|
JSON array of exactly 2 specialist names, e.g. ["a","b"].`
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-build-then-verify",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Pick the two specialists, in order, for a build-then-independently-verify workflow.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringArrayEquals([]string{"code-writer", "code-reviewer"}),
|
|
}
|
|
}
|
|
|
|
// delegHandoffContextCandidates is the enumerated list of possible context
|
|
// items for delegHandoffContextTest, mixing genuinely essential items with
|
|
// plausible-sounding noise.
|
|
const delegHandoffContextCandidates = `- file_paths_touched
|
|
- failing_test_output
|
|
- reproduction_steps
|
|
- acceptance_criteria
|
|
- unrelated_agent_chat_history
|
|
- full_git_log_since_project_start`
|
|
|
|
// delegHandoffContextWant is the subset of candidates essential to a
|
|
// failing-test-bug handoff.
|
|
//
|
|
// ground truth: code-writer needs to know exactly which files are involved
|
|
// (file_paths_touched), what actually failed (failing_test_output), how to
|
|
// reproduce it (reproduction_steps), and what "fixed" means
|
|
// (acceptance_criteria). Chat history from unrelated work and the entire
|
|
// project's git log are not about this bug and would only add noise the
|
|
// specialist has to filter back out.
|
|
var delegHandoffContextWant = []string{
|
|
"file_paths_touched",
|
|
"failing_test_output",
|
|
"reproduction_steps",
|
|
"acceptance_criteria",
|
|
}
|
|
|
|
func delegHandoffContextTest() testkit.Test {
|
|
prompt := `You are handing off a failing-test bug to code-writer to fix.
|
|
Here are candidate items you could include in the handoff:
|
|
|
|
` + delegHandoffContextCandidates + `
|
|
|
|
Which of these candidates are essential to include in the handoff? Exclude
|
|
anything that is unrelated noise. Respond with only a JSON array of the
|
|
essential candidate names.`
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-handoff-context",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Select the essential context items for a bug handoff from an enumerated list mixing essentials and noise.",
|
|
Prompt: prompt,
|
|
Eval: eval.JSONStringSet(delegHandoffContextWant),
|
|
}
|
|
}
|
|
|
|
// delegVerifyVsTrustTest: decide, for two changes of very different risk,
|
|
// whether the result should be independently verified or simply trusted.
|
|
//
|
|
// ground truth: a one-line comment typo has no behavioral effect and is
|
|
// trivially reversible, so trusting it without a separate verification pass
|
|
// is reasonable. A change to authentication token validation logic is
|
|
// security-sensitive and easy to get subtly wrong, so it must be
|
|
// independently verified before being trusted.
|
|
func delegVerifyVsTrustTest() testkit.Test {
|
|
prompt := `For each of these two completed changes, should the result be
|
|
independently verified before trusting it, or is it reasonable to trust it
|
|
as-is?
|
|
|
|
scenario_a: "A one-line typo fix inside a code comment. No executable code
|
|
changed."
|
|
scenario_b: "A change to the authentication token validation logic."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"trust"|"verify","scenario_b":"trust"|"verify"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "trust"),
|
|
eval.JSONField("scenario_b", "verify"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-verify-vs-trust",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Decide verify-vs-trust for a trivial comment typo versus a security-sensitive auth logic change.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegBatchVsSeparateTest: decide whether related failures should be
|
|
// batched into one dispatch or split into separate ones.
|
|
//
|
|
// ground truth: three failures sharing one root cause in one function are
|
|
// fixed by one change, so one batched dispatch is strictly better - three
|
|
// separate dispatches would triplicate the same diagnosis. A flaky network
|
|
// timeout and an unrelated real logic bug in a different module have
|
|
// different root causes and different fixes in different files, so they
|
|
// should be separate dispatches rather than conflated into one.
|
|
func delegBatchVsSeparateTest() testkit.Test {
|
|
prompt := `For each of these two situations, should the fixes be
|
|
batched into one dispatch, or split into separate dispatches?
|
|
|
|
scenario_a: "Three failing tests, all caused by the exact same off-by-one
|
|
bug in the same function."
|
|
scenario_b: "Two failing tests: one is a flaky network timeout in the
|
|
integration suite, the other is a real logic bug in a completely unrelated
|
|
module."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"batch"|"separate","scenario_b":"batch"|"separate"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "batch"),
|
|
eval.JSONField("scenario_b", "separate"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-batch-vs-separate",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Decide batch-vs-separate dispatch for a single-root-cause failure set versus two unrelated failures.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegMainThreadVsDelegateTest: decide whether a quick edit should be
|
|
// handled on the main thread or delegated to a subagent.
|
|
//
|
|
// ground truth: a single-character typo fix in a file already open needs
|
|
// no extra context-gathering or specialist skill, so handling it directly
|
|
// is faster and simpler than the overhead of dispatching a subagent. A
|
|
// 6-file refactor plus new tests is large enough to benefit from a
|
|
// dedicated agent's focused context and is exactly the kind of work
|
|
// delegation exists for.
|
|
func delegMainThreadVsDelegateTest() testkit.Test {
|
|
prompt := `For each of these two tasks, should you handle it directly on
|
|
the main thread, or delegate it to a subagent?
|
|
|
|
scenario_a: "Fix a single-character typo in a log message string, in the
|
|
file you already have open."
|
|
scenario_b: "Refactor the authentication module across 6 files and add
|
|
corresponding tests."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"main-thread"|"delegate","scenario_b":"main-thread"|"delegate"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "main-thread"),
|
|
eval.JSONField("scenario_b", "delegate"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-main-thread-vs-delegate",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Decide main-thread-vs-delegate for a trivial one-file typo fix versus a 6-file refactor plus tests.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegReviewerIndependenceTest: the same agent that made a change must not
|
|
// be the one that signs off on it.
|
|
//
|
|
// ground truth: code-writer just implemented the caching layer, so
|
|
// code-writer reviewing its own work is not independent; code-reviewer is
|
|
// the roster's dedicated independent-review specialist. In the second
|
|
// case, security-reviewer directly fixed the issue it found, which makes
|
|
// security-reviewer the author of that fix for this purpose - so a
|
|
// separate independent pass (code-reviewer) must confirm it, not
|
|
// security-reviewer confirming its own fix.
|
|
func delegReviewerIndependenceTest() testkit.Test {
|
|
prompt := `You can delegate to this roster of specialist agents:
|
|
|
|
` + delegRoster + `
|
|
|
|
case1: "code-writer just implemented a new caching layer. Who should review
|
|
it before merge?"
|
|
case2: "security-reviewer found a vulnerability during an audit and fixed
|
|
it directly. Who should confirm the fix is correct before merge?"
|
|
|
|
In both cases, the same agent that made the change must not be the one that
|
|
signs off on it. Respond with only a JSON object:
|
|
{"case1":"...","case2":"..."}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("case1", "code-reviewer"),
|
|
eval.JSONField("case2", "code-reviewer"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-reviewer-independence",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Require an independent reviewer rather than letting an agent sign off on its own change, in two framings.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegEscalationToHumanTest: decide whether a change should escalate to a
|
|
// human before proceeding, or can proceed autonomously.
|
|
//
|
|
// ground truth: deleting a production database table with no verified
|
|
// backup is irreversible and high-risk with no safe rollback path, which is
|
|
// exactly when a human must be asked first. Adding a new optional CLI flag
|
|
// with a sensible default is reversible, low-risk, and unambiguous, so it
|
|
// can proceed without waiting on a human.
|
|
func delegEscalationToHumanTest() testkit.Test {
|
|
prompt := `For each of these two requested changes, should you escalate
|
|
to a human before proceeding, or proceed without asking?
|
|
|
|
scenario_a: "Delete a production database table. No backup has been
|
|
verified to exist."
|
|
scenario_b: "Add a new optional CLI flag with a sensible default value."
|
|
|
|
Respond with only a JSON object:
|
|
{"scenario_a":"escalate"|"proceed","scenario_b":"escalate"|"proceed"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "escalate"),
|
|
eval.JSONField("scenario_b", "proceed"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-escalation-to-human",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Decide escalate-vs-proceed for an irreversible high-risk database deletion versus a low-risk CLI flag addition.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegParallelDispatchSafetyTest: spot which of two planned parallel
|
|
// dispatch pairs would conflict by touching the same file.
|
|
//
|
|
// ground truth: pair_1's two dispatches both list b.go, so running them in
|
|
// parallel risks a write conflict on that shared file - unsafe. pair_2's
|
|
// two dispatches touch entirely disjoint files (x.go and y.go), so nothing
|
|
// they do can collide - safe.
|
|
func delegParallelDispatchSafetyTest() testkit.Test {
|
|
prompt := `You are planning to dispatch two subagents in parallel.
|
|
|
|
pair_1: dispatch_1 will edit files [a.go, b.go]; dispatch_2 will edit files
|
|
[b.go, c.go].
|
|
pair_2: dispatch_3 will edit files [x.go]; dispatch_4 will edit files
|
|
[y.go].
|
|
|
|
For each pair, is it safe to run the two dispatches in parallel, or do they
|
|
conflict over a shared file? Respond with only a JSON object:
|
|
{"pair_1":"safe"|"conflict","pair_2":"safe"|"conflict"}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("pair_1", "conflict"),
|
|
eval.JSONField("pair_2", "safe"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-parallel-dispatch-safety",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Spot a shared-file conflict between one pair of planned parallel dispatches, versus a disjoint, safe pair.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|
|
|
|
// delegMinimalPrivilegeTest: pick the specialist with the narrowest access
|
|
// that is still adequate, rather than a broader one that could also do the
|
|
// job.
|
|
//
|
|
// ground truth: summarizing a competitor's public pricing page needs no
|
|
// repository access at all - web-researcher is the roster's only agent
|
|
// explicitly scoped to external information with no repo access, which is
|
|
// the minimal-privilege fit; orchestrator or code-writer would carry
|
|
// unnecessary repo-write privilege for a task that never touches the repo.
|
|
// Tagging a release from already-merged commits, without writing or
|
|
// modifying application code, is exactly release-manager's scope, which
|
|
// explicitly excludes code changes - code-writer would carry unneeded
|
|
// code-editing privilege for a task that must not touch code.
|
|
func delegMinimalPrivilegeTest() testkit.Test {
|
|
prompt := `You can delegate to this roster of specialist agents:
|
|
|
|
` + delegRoster + `
|
|
|
|
scenario_a: "Look up and summarize a competitor's public pricing page.
|
|
Nothing in the repository needs to change."
|
|
scenario_b: "Generate a changelog and tag a release version from commits
|
|
that are already merged. Do not write or modify any application code."
|
|
|
|
For each scenario, pick the specialist with the narrowest access that is
|
|
still adequate for the task. Respond with only a JSON object:
|
|
{"scenario_a":"...","scenario_b":"..."}`
|
|
|
|
evaluator := eval.Mean(
|
|
eval.JSONField("scenario_a", "web-researcher"),
|
|
eval.JSONField("scenario_b", "release-manager"),
|
|
)
|
|
|
|
return testkit.Test{
|
|
ID: "agent-deleg-minimal-privilege",
|
|
Category: "agents",
|
|
Subcategory: "delegation",
|
|
Description: "Pick the narrowest-access adequate specialist for a repo-free lookup and a code-free release task.",
|
|
Prompt: prompt,
|
|
Eval: evaluator,
|
|
}
|
|
}
|