返回 DeepSeek-Reasonix
child_test.go
根目录 / internal / evidence / child_test.go
1 package evidence
2
3 import (
4 "encoding/json"
5 "path/filepath"
6 "testing"
7 )
8
9 func TestMetaToolsDoNotMutate(t *testing.T) {
10 for _, name := range []string{
11 "run_skill", "read_skill", "read_only_skill", "task", "read_only_task",
12 "parallel_tasks", "explore", "research", "review", "security_review", "use_capability",
13 } {
14 if ToolCallMutates(name, json.RawMessage(`{}`), false) {
15 t.Fatalf("%s must not count as mutation", name)
16 }
17 }
18 }
19
20 func TestMergeChildPropagatesRealWrites(t *testing.T) {
21 parent := NewLedger()
22 parent.Record(ReceiptFromToolCall("task", json.RawMessage(`{"prompt":"edit"}`), true, false))
23 if _, ok := parent.LatestSuccessfulMutationIndex(); ok {
24 t.Fatal("task alone must not create a mutation index")
25 }
26
27 child := NewLedger()
28 child.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/a.go"}`), true, false))
29 child.Record(ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/a.go"}`), true, true))
30 parent.MergeChild(child.Summary())
31
32 idx, ok := parent.LatestSuccessfulMutationIndex()
33 if !ok {
34 t.Fatal("expected merged child write to count as mutation")
35 }
36 paths := parent.PathsSince(idx)
37 wantPath := filepath.ToSlash("internal/a.go")
38 if len(paths) != 1 || filepath.ToSlash(paths[0]) != wantPath {
39 t.Fatalf("paths = %v, want %s", paths, wantPath)
40 }
41 if !parent.HasSuccessfulReviewAfter(idx) {
42 t.Fatal("child read of mutated path should satisfy review")
43 }
44 }
45
46 func TestClassifyMutationRisk(t *testing.T) {
47 low := []Receipt{
48 ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"docs/GUIDE.md"}`), true, false),
49 }
50 if got := ClassifyMutationRisk(low, 0); got != RiskLow {
51 t.Fatalf("docs risk = %s, want low", got)
52 }
53
54 med := []Receipt{
55 ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/agent/agent.go"}`), true, false),
56 }
57 if got := ClassifyMutationRisk(med, 0); got != RiskMedium {
58 t.Fatalf("prod risk = %s, want medium", got)
59 }
60
61 high := []Receipt{
62 ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/permission/gate.go"}`), true, false),
63 }
64 if got := ClassifyMutationRisk(high, 0); got != RiskHigh {
65 t.Fatalf("auth risk = %s, want high", got)
66 }
67
68 // A path-less bash write cannot be scored by path, so it remains high risk.
69 opaque := []Receipt{
70 {ToolName: "bash", Success: true, Mutation: true, Command: "some-unknown-writer"},
71 }
72 if got := ClassifyMutationRisk(opaque, 0); got != RiskHigh {
73 t.Fatalf("opaque risk = %s, want high", got)
74 }
75
76 // Privileged/opaque tools keep escalating to High even without paths.
77 opaquePrivileged := []Receipt{
78 {ToolName: "mcp__srv__write", Success: true, Mutation: true},
79 }
80 if got := ClassifyMutationRisk(opaquePrivileged, 0); got != RiskHigh {
81 t.Fatalf("privileged opaque risk = %s, want high", got)
82 }
83
84 // An opaque write alongside a security-sensitive path still classifies High.
85 opaqueHighPath := []Receipt{
86 {ToolName: "bash", Success: true, Mutation: true},
87 ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/permission/gate.go"}`), true, false),
88 }
89 if got := ClassifyMutationRisk(opaqueHighPath, 0); got != RiskHigh {
90 t.Fatalf("opaque+auth risk = %s, want high", got)
91 }
92 }
93
94 func TestStructuredReviewReportGate(t *testing.T) {
95 ledger := NewLedger()
96 ledger.Record(ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/a.go"}`), true, false))
97 mutation, ok := ledger.LatestSuccessfulMutationIndex()
98 if !ok {
99 t.Fatal("expected mutation")
100 }
101
102 raw := json.RawMessage(`{
103 "kind":"review",
104 "verdict":"pass",
105 "reviewed_paths":["internal/a.go"],
106 "findings":[]
107 }`)
108 ledger.Record(Receipt{ToolName: "review_report", Args: raw, Success: true})
109 if !ledger.HasSuccessfulStructuredReviewAfter(ReviewKindReview, mutation, []string{"internal/a.go"}) {
110 t.Fatal("expected structured review coverage")
111 }
112
113 block := json.RawMessage(`{
114 "kind":"security",
115 "verdict":"block",
116 "reviewed_paths":["internal/a.go"],
117 "findings":[{"severity":"critical","summary":"hardcoded secret","path":"internal/a.go","line":1}]
118 }`)
119 ledger.Record(Receipt{ToolName: "review_report", Args: block, Success: true})
120 ok, blocking, _ := ledger.HasStructuredReviewAfter(ReviewKindSecurity, mutation, []string{"internal/a.go"})
121 if !ok || !blocking {
122 t.Fatalf("security block: ok=%v blocking=%v", ok, blocking)
123 }
124 }
125
125 lines GO