返回 DeepSeek-Reasonix
child_test.go
根目录 / internal / evidence / child_test.go
1 package evidence
2
3 import (
4 "encoding/json"
5 "path/filepath"
6 "sync"
7 "testing"
8 )
9
10 func TestMetaToolsDoNotMutate(t *testing.T) {
11 for _, name := range []string{
12 "run_skill", "read_skill", "read_only_skill", "task", "read_only_task",
13 "parallel_tasks", "explore", "research", "review", "security_review", "use_capability",
14 } {
15 if ToolCallMutates(name, json.RawMessage(`{}`), false) {
16 t.Fatalf("%s must not count as mutation", name)
17 }
18 }
19 }
20
21 func TestMergeChildPropagatesRealWrites(t *testing.T) {
22 parent := NewLedger()
23 parent.Record(ReceiptFromToolCall("task", json.RawMessage(`{"prompt":"edit"}`), true, false))
24 if _, ok := parent.LatestSuccessfulMutationIndex(); ok {
25 t.Fatal("task alone must not create a mutation index")
26 }
27
28 child := NewLedger()
29 child.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/a.go"}`), true, false))
30 child.Record(ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/a.go"}`), true, true))
31 parent.MergeChild(child.Summary())
32
33 idx, ok := parent.LatestSuccessfulMutationIndex()
34 if !ok {
35 t.Fatal("expected merged child write to count as mutation")
36 }
37 paths := parent.Summary().MutationPaths()
38 wantPath := filepath.ToSlash("internal/a.go")
39 if len(paths) != 1 || filepath.ToSlash(paths[0]) != wantPath {
40 t.Fatalf("paths = %v, want %s", paths, wantPath)
41 }
42 if !parent.HasSuccessfulReviewAfter(idx) {
43 t.Fatal("child read of mutated path should satisfy review")
44 }
45 }
46
47 func TestConcurrentChildMergesKeepDistinctWrites(t *testing.T) {
48 parent := NewLedger()
49 start := make(chan struct{})
50 var wg sync.WaitGroup
51 wg.Add(2)
52 go func() {
53 defer wg.Done()
54 <-start
55 child := NewLedger()
56 child.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/a.go"}`), true, false))
57 parent.MergeChild(child.Summary())
58 }()
59 go func() {
60 defer wg.Done()
61 <-start
62 child := NewLedger()
63 child.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/b.go"}`), true, false))
64 parent.MergeChild(child.Summary())
65 }()
66 close(start)
67 wg.Wait()
68 if parent.Len() != 2 {
69 t.Fatalf("merged receipts = %d, want 2", parent.Len())
70 }
71 paths := parent.Summary().MutationPaths()
72 if len(paths) != 2 {
73 t.Fatalf("mutation paths = %v, want both child writes", paths)
74 }
75 }
76
77 func TestStructuredReviewReportGate(t *testing.T) {
78 ledger := NewLedger()
79 ledger.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/a.go"}`), true, false))
80 mutation, ok := ledger.LatestSuccessfulMutationIndex()
81 if !ok {
82 t.Fatal("expected mutation")
83 }
84
85 raw := json.RawMessage(`{
86 "kind":"review",
87 "verdict":"pass",
88 "reviewed_paths":["internal/a.go"],
89 "findings":[]
90 }`)
91 ledger.Record(Receipt{ToolName: "review_report", Args: raw, Success: true})
92 if !ledger.HasSuccessfulStructuredReviewAfter(ReviewKindReview, mutation, []string{"internal/a.go"}) {
93 t.Fatal("expected structured review coverage")
94 }
95
96 block := json.RawMessage(`{
97 "kind":"security",
98 "verdict":"block",
99 "reviewed_paths":["internal/a.go"],
100 "findings":[{"severity":"critical","summary":"hardcoded secret","path":"internal/a.go","line":1}]
101 }`)
102 ledger.Record(Receipt{ToolName: "review_report", Args: block, Success: true})
103 ok, blocking, _ := ledger.HasStructuredReviewAfter(ReviewKindSecurity, mutation, []string{"internal/a.go"})
104 if !ok || !blocking {
105 t.Fatalf("security block: ok=%v blocking=%v", ok, blocking)
106 }
107 }
108
108 lines GO