| 1 | package evidence |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | func grepReceipt(pattern, path string) Receipt { |
| 10 | args := json.RawMessage(fmt.Sprintf(`{"pattern":%q,"path":%q}`, pattern, path)) |
| 11 | r := ReceiptFromToolCall("grep", args, true, true) |
| 12 | r.OutputBytes = 64 |
| 13 | return r |
| 14 | } |
| 15 | |
| 16 | func windowReceipt(path string, offset int) Receipt { |
| 17 | args := json.RawMessage(fmt.Sprintf(`{"path":%q,"offset":%d,"limit":300}`, path, offset)) |
| 18 | r := ReceiptFromToolCall("read_file", args, true, true) |
| 19 | r.OutputBytes = 64 |
| 20 | return r |
| 21 | } |
| 22 | |
| 23 | // Read novelty used to be keyed on the path alone, so the two most common |
| 24 | // investigation moves — grepping one package for a second symbol, paging |
| 25 | // through a long file — scored as repeats from the second round on, and a turn |
| 26 | // doing exactly that was told it had produced no new evidence. |
| 27 | func TestProgressTrackerScoresTheQuestionNotJustThePath(t *testing.T) { |
| 28 | tr := NewProgressTracker() |
| 29 | |
| 30 | if got := tr.ScoreRound([]Receipt{grepReceipt("Compose", "internal/agent")}); got != gainNewRead { |
| 31 | t.Fatalf("first grep = %d, want %d", got, gainNewRead) |
| 32 | } |
| 33 | if got := tr.ScoreRound([]Receipt{grepReceipt("Receipt", "internal/agent")}); got != gainNewRead { |
| 34 | t.Fatalf("new pattern over a read path = %d, want %d", got, gainNewRead) |
| 35 | } |
| 36 | if got := tr.ScoreRound([]Receipt{grepReceipt("Receipt", "internal/agent")}); got != 0 { |
| 37 | t.Fatalf("identical grep = %d, want 0", got) |
| 38 | } |
| 39 | |
| 40 | if got := tr.ScoreRound([]Receipt{windowReceipt("agent.go", 0)}); got != gainNewRead { |
| 41 | t.Fatalf("first window = %d, want %d", got, gainNewRead) |
| 42 | } |
| 43 | if got := tr.ScoreRound([]Receipt{windowReceipt("agent.go", 300)}); got != gainNewRead { |
| 44 | t.Fatalf("next window of the same file = %d, want %d", got, gainNewRead) |
| 45 | } |
| 46 | if got := tr.ScoreRound([]Receipt{windowReceipt("agent.go", 300)}); got != 0 { |
| 47 | t.Fatalf("identical window = %d, want 0", got) |
| 48 | } |
| 49 | |
| 50 | // A first read of a new path still counts once, not twice. It needs a fresh |
| 51 | // tracker: past explorationRunLimit look-only rounds the run stops counting |
| 52 | // whatever it finds, which is the separate cliff this change does not touch. |
| 53 | fresh := NewProgressTracker() |
| 54 | first := readReceipt("new.go") |
| 55 | first.OutputBytes = 64 |
| 56 | if got := fresh.ScoreRound([]Receipt{first}); got != gainNewRead { |
| 57 | t.Fatalf("new path = %d, want %d", got, gainNewRead) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // The shadow scorer keys novelty the same way, so the two never disagree about |
| 62 | // what counts as a repeat. |
| 63 | func TestOutcomeTrackerScoresTheQuestionNotJustThePath(t *testing.T) { |
| 64 | tr := NewOutcomeTracker() |
| 65 | |
| 66 | if s := tr.ScoreRound([]Receipt{grepReceipt("Compose", "internal/agent")}); s.Exploration != 1 { |
| 67 | t.Fatalf("first grep = %+v, want exploration 1", s) |
| 68 | } |
| 69 | if s := tr.ScoreRound([]Receipt{grepReceipt("Receipt", "internal/agent")}); s.Exploration != 1 { |
| 70 | t.Fatalf("new pattern over a read path = %+v, want exploration 1", s) |
| 71 | } |
| 72 | if s := tr.ScoreRound([]Receipt{grepReceipt("Receipt", "internal/agent")}); s.Exploration != 0 { |
| 73 | t.Fatalf("identical grep = %+v, want exploration 0", s) |
| 74 | } |
| 75 | if s := tr.ScoreRound([]Receipt{windowReceipt("agent.go", 0)}); s.Exploration != 1 { |
| 76 | t.Fatalf("first window = %+v, want exploration 1", s) |
| 77 | } |
| 78 | if s := tr.ScoreRound([]Receipt{windowReceipt("agent.go", 300)}); s.Exploration != 1 { |
| 79 | t.Fatalf("next window of the same file = %+v, want exploration 1", s) |
| 80 | } |
| 81 | } |
| 82 |