mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
81e6380510
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
124 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|