| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/event" |
| 9 | "reasonix/internal/evidence" |
| 10 | "reasonix/internal/provider" |
| 11 | "reasonix/internal/tool" |
| 12 | ) |
| 13 | |
| 14 | type originProbe struct { |
| 15 | event.Sink |
| 16 | got []evidence.DelegationAudit |
| 17 | } |
| 18 | |
| 19 | func (p *originProbe) RecordDelegationAudit(a evidence.DelegationAudit) { p.got = append(p.got, a) } |
| 20 | |
| 21 | // runDelegationForOrigin spawns one real child through TaskTool that reads |
| 22 | // src/parser.go and src/scanner.go, and returns the audit the sink received. |
| 23 | func runDelegationForOrigin(t *testing.T, prompt string) evidence.DelegationAudit { |
| 24 | t.Helper() |
| 25 | probe := &originProbe{Sink: event.Discard} |
| 26 | reg := tool.NewRegistry() |
| 27 | reg.Add(fakeReadFileTool{}) |
| 28 | prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ |
| 29 | {toolCallChunk("1", "read_file", `{"path":"src/parser.go"}`), {Type: provider.ChunkDone}}, |
| 30 | {toolCallChunk("2", "read_file", `{"path":"src/scanner.go"}`), {Type: provider.ChunkDone}}, |
| 31 | {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}, |
| 32 | }} |
| 33 | task := NewTaskTool(prov, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil). |
| 34 | WithTranscripts(mustSubagentStore(t), t.TempDir(), "base", "high"). |
| 35 | WithScheduler(NewSubagentScheduler(4, 4)) |
| 36 | ctx := withCallContext(context.Background(), "call-1", probe, nil, false) |
| 37 | args, err := json.Marshal(map[string]string{"prompt": prompt}) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | if _, err := task.Execute(ctx, args); err != nil { |
| 42 | t.Fatalf("task: %v", err) |
| 43 | } |
| 44 | if len(probe.got) != 1 { |
| 45 | t.Fatalf("got %d audits through the TaskTool path, want 1", len(probe.got)) |
| 46 | } |
| 47 | return probe.got[0] |
| 48 | } |
| 49 | |
| 50 | // A delegation that hands over no location leaves the child to find every file |
| 51 | // it reads. This is the number blind delegation would have to hold at zero. |
| 52 | func TestDelegationAuditScoresBlindDelegationAsFullyDiscovered(t *testing.T) { |
| 53 | audit := runDelegationForOrigin(t, "Determine why the failing test fails.") |
| 54 | if audit.ParentNamedFiles != 0 { |
| 55 | t.Errorf("ParentNamedFiles = %d, want 0", audit.ParentNamedFiles) |
| 56 | } |
| 57 | if audit.EvidencePaths != 2 || audit.DiscoveredPaths != 2 { |
| 58 | t.Errorf("discovered %d/%d, want 2/2", audit.DiscoveredPaths, audit.EvidencePaths) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // The parent's own text is the only channel that can leak a location, so a |
| 63 | // prompt naming one file must show up as a hint and cost the child credit for |
| 64 | // the evidence it did not have to find. |
| 65 | func TestDelegationAuditCreditsOnlyWhatTheChildFoundItself(t *testing.T) { |
| 66 | audit := runDelegationForOrigin(t, "The bug is in src/parser.go — confirm it.") |
| 67 | if audit.ParentNamedFiles != 1 { |
| 68 | t.Errorf("ParentNamedFiles = %d, want 1", audit.ParentNamedFiles) |
| 69 | } |
| 70 | if audit.EvidencePaths != 2 || audit.DiscoveredPaths != 1 { |
| 71 | t.Errorf("discovered %d/%d, want 1/2", audit.DiscoveredPaths, audit.EvidencePaths) |
| 72 | } |
| 73 | } |
| 74 |