Files
boshu2__agentops/cli/cmd/ao/eval_composition_test.go
Bo 81e6380510 feat(cli): wire ao eval — the Learn seat's measurement surface (#921)
Registers the fully-built-but-never-wired eval module into the CLI spine
and repairs its one real instrument bug. Three commits, RPI-shaped: wire
+ fix, drift guards + docs, fixture counts.

## What
- **`ao eval` joins the spine** (cmd/ao/eval_composition.go):
deterministic suite runs, run-record
compare/baseline/scorecard/coverage, locked Tasks, holdout scenarios,
suite A/B verdicts with power-derived n-required, and the holdout-safe
Outcomes projection. This is the operating contract's *Learn* seat — a
read/measure consumer of evidence. It reports numbers and owns no retry,
scheduling, promotion, or lifecycle authority. The retired Aliases/Bench
seats stay nil and their subcommands are omitted.
- **Instrument bug fixed**: both scenario-ab arms were corpus-denied at
runtime — the with-gold treatment was only a prompt injection through
the retired `ao lookup`, so the A/B silently guaranteed a zero delta.
Treatment is now environment-shaped: the with-gold arm runs with the
corpus readable, the control arm stays sandbox-confined (the fail-closed
empty-deny guard is untouched), and judges are always corpus-denied.
- **Schema drift guards**: a run record persisted by the production
writer must validate against eval-run.v1.schema.json (and the minimal
suite against eval-suite.v1.schema.json) — closing the same silent-fork
class the verdict.v2 golden corpus closed.
- **Hermetic test builds hardened**: keep the real HOME's Go caches
(TestMain isolation was forcing module re-downloads) and skip VCS
stamping.
- docs/architecture/go-cli.md gains the eval section; MIGRATION.md notes
`ao eval`'s return; COMMANDS.md, cli-surface, and surface counts
regenerated; new leaves carry honest allowlist reasons; new cmd/ao L2
tests drive eval run/compare/help through the full production wiring.

## Evidence
- Full cli suite 2,801 green; race green on cmd/ao + the eval island;
vet clean
- Full-tier gate: 65/66 on the penultimate commit; the single failure
(surface counts) is the final fixture commit, re-verified PASS
standalone — no other check reads those fixtures
- Fresh independent validation (Codex, frozen subject 86022a3c6, clean
tree at start and end): **OVERALL PASS**, 6/6 — including a live
end-to-end `ao eval run` producing a passing run record
- Rubric-relocation decision honored: true holdout rubrics live in the
external measurement register; the repo keeps only leak-guarded
projections
2026-07-18 18:33:46 -04:00

124 lines
3.9 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// minimalEvalSuite is a self-contained deterministic suite: one static
// artifact_contains case over a fixture file, no network, no subprocesses.
const minimalEvalSuite = `{
"schema_version": 1,
"id": "fixture.pass",
"name": "Fixture pass",
"domain": "cli",
"visibility": "public_canary",
"tier": "deterministic",
"scoring": {
"aggregate_threshold": 1,
"dimensions": [
{"name": "correctness", "weight": 1, "threshold": 1}
]
},
"baseline_policy": {"mode": "none"},
"cases": [
{
"id": "contains",
"title": "fixture contains needle",
"kind": "artifact_check",
"objective": "Verify static fixtures are scored offline.",
"expectations": [
{"type": "artifact_contains", "target": "fixture.txt", "value": "needle"}
]
}
]
}`
func writeMinimalSuite(t *testing.T) (dir, suitePath string) {
t.Helper()
dir = t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "fixture.txt"), []byte("alpha\nneedle\n"), 0o644); err != nil {
t.Fatal(err)
}
suitePath = filepath.Join(dir, "suite.json")
if err := os.WriteFile(suitePath, []byte(minimalEvalSuite), 0o644); err != nil {
t.Fatal(err)
}
return dir, suitePath
}
// TestEvalRun_DeterministicSuite exercises the full production wiring
// (composition -> module -> CoreService -> adapter Runtime -> engine) over a
// real on-disk suite and asserts the durable run record.
func TestEvalRun_DeterministicSuite(t *testing.T) {
dir, suitePath := writeMinimalSuite(t)
out := filepath.Join(dir, "run.json")
output, err := executeCommand("eval", "run", suitePath, "--run-id", "run-1", "--out", out)
if err != nil {
t.Fatalf("ao eval run failed: %v\n%s", err, output)
}
payload, err := os.ReadFile(out)
if err != nil {
t.Fatalf("run record not written: %v", err)
}
var record struct {
SchemaVersion int `json:"schema_version"`
RunID string `json:"run_id"`
Status string `json:"status"`
Verdict string `json:"verdict"`
AggregateScore float64 `json:"aggregate_score"`
}
if err := json.Unmarshal(payload, &record); err != nil {
t.Fatalf("run record is not JSON: %v", err)
}
if record.RunID != "run-1" || record.Status != "pass" || record.Verdict != "pass" {
t.Fatalf("run record = %+v, want run-1/pass/pass", record)
}
if record.AggregateScore != 1 {
t.Fatalf("aggregate_score = %v, want 1", record.AggregateScore)
}
}
// TestEvalCompare_IdenticalRunsPass compares a run record against itself:
// zero regression must yield a pass verdict.
func TestEvalCompare_IdenticalRunsPass(t *testing.T) {
dir, suitePath := writeMinimalSuite(t)
out := filepath.Join(dir, "run.json")
if output, err := executeCommand("eval", "run", suitePath, "--run-id", "run-1", "--out", out); err != nil {
t.Fatalf("seed run failed: %v\n%s", err, output)
}
output, err := executeCommand("eval", "compare", out, out)
if err != nil {
t.Fatalf("ao eval compare failed: %v\n%s", err, output)
}
if !strings.Contains(output, "pass") {
t.Fatalf("compare output does not report pass:\n%s", output)
}
}
// TestEvalHelp_PublishesMeasurementSurface pins the top-level subcommand set.
func TestEvalHelp_PublishesMeasurementSurface(t *testing.T) {
output, err := executeCommand("eval", "--help")
if err != nil {
t.Fatalf("ao eval --help failed: %v", err)
}
for _, subcommand := range []string{"run", "compare", "baseline", "scorecard", "coverage", "task", "suite", "outcomes", "scenario"} {
if !strings.Contains(output, subcommand) {
t.Errorf("ao eval --help missing subcommand %q", subcommand)
}
}
// The retired alias seats must not resurface: their production
// implementations were removed with the legacy knowledge stack.
for _, retired := range []string{"session-outcome", "chaos", "bench"} {
if strings.Contains(output, retired) {
t.Errorf("ao eval --help exposes retired alias %q", retired)
}
}
}