| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "reasonix/internal/event" |
| 7 | "reasonix/internal/provider" |
| 8 | ) |
| 9 | |
| 10 | func TestDelegationAdmissionVerdicts(t *testing.T) { |
| 11 | cases := []struct { |
| 12 | name, input, args, verdict, reason string |
| 13 | }{ |
| 14 | {"local fix, plain query", "fix the config serializer bug in parser.go", |
| 15 | `{"prompt":"how does the serializer format keys"}`, "allow", "model_decides"}, |
| 16 | {"user asked for research", "research the best TOML library and fix the loader", |
| 17 | `{"prompt":"compare toml libraries"}`, "allow", "model_decides"}, |
| 18 | {"external source cited", "fix the retry logic to match the upstream spec", |
| 19 | `{"prompt":"read https://example.com/spec and summarize backoff rules"}`, "allow", "model_decides"}, |
| 20 | {"advisory turn", "how does our retry budget compare to industry practice?", |
| 21 | `{"prompt":"survey retry budget conventions"}`, "allow", "model_decides"}, |
| 22 | } |
| 23 | for _, c := range cases { |
| 24 | verdict, reason, _ := delegationAdmission(c.input, c.args) |
| 25 | if verdict != c.verdict || reason != c.reason { |
| 26 | t.Errorf("%s: got %s/%s, want %s/%s", c.name, verdict, reason, c.verdict, c.reason) |
| 27 | } |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | type admissionSink struct { |
| 32 | audits []event.DelegationAdmissionAudit |
| 33 | } |
| 34 | |
| 35 | func (s *admissionSink) Emit(event.Event) {} |
| 36 | func (s *admissionSink) RecordDelegationAdmission(a event.DelegationAdmissionAudit) { |
| 37 | s.audits = append(s.audits, a) |
| 38 | } |
| 39 | |
| 40 | func TestObserveDelegationAdmissionRecordsOnlyGatedTools(t *testing.T) { |
| 41 | sink := &admissionSink{} |
| 42 | a := &Agent{svc: agentServices{sink: sink}} |
| 43 | a.turn.recoveryTaskSummary = "fix the failing date parser" |
| 44 | a.observeDelegationAdmission([]provider.ToolCall{ |
| 45 | {Name: "read_file", Arguments: `{"path":"a.go"}`}, |
| 46 | {Name: "research", Arguments: `{"prompt":"date formats"}`}, |
| 47 | {Name: "task", Arguments: `{"prompt":"sub work"}`}, |
| 48 | }) |
| 49 | // read_file is not a delegation and must never be audited; research and task |
| 50 | // both are, since a paired measurement priced task/fleet delegation at 2-4x |
| 51 | // the cost of doing the same work directly. |
| 52 | if len(sink.audits) != 2 { |
| 53 | t.Fatalf("got %d audits, want research + task", len(sink.audits)) |
| 54 | } |
| 55 | for _, got := range sink.audits { |
| 56 | if got.Tool == "read_file" { |
| 57 | t.Fatalf("a non-delegation tool was audited: %+v", got) |
| 58 | } |
| 59 | if got.Verdict != "allow" || got.Reason != "model_decides" { |
| 60 | t.Fatalf("audit = %+v, want allow/model_decides", got) |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 |