返回 DeepSeek-Reasonix
capability_gate_test.go
根目录 / internal / agent / capability_gate_test.go
1 package agent
2
3 import (
4 "encoding/json"
5 "strings"
6 "testing"
7
8 "reasonix/internal/evidence"
9 "reasonix/internal/tool"
10 )
11
12 func TestDeliveryReviewGateExplainsOpaqueMutationRecovery(t *testing.T) {
13 ledger := evidence.NewLedger()
14 ledger.Record(evidence.Receipt{
15 ToolName: "bash",
16 Success: true,
17 Mutation: true,
18 Command: "opaque-writer",
19 })
20
21 reg := tool.NewRegistry()
22 reg.Add(fakeTool{name: "review", readOnly: true})
23 reg.Add(fakeTool{name: "security_review", readOnly: true})
24 a := &Agent{deliveryProfile: true, evidence: ledger, tools: reg}
25
26 got := a.deliveryReviewGateFailure()
27 for _, want := range []string{"high-risk", "git status --short", "git diff", "mutation did not report file paths"} {
28 if !strings.Contains(got, want) {
29 t.Fatalf("review gate = %q, want %q", got, want)
30 }
31 }
32 if strings.HasSuffix(got, "covering: ") {
33 t.Fatalf("review gate must not end with empty coverage: %q", got)
34 }
35
36 ledger.Record(evidence.Receipt{ToolName: "review_report", Success: true, Args: json.RawMessage(`{
37 "kind":"review",
38 "verdict":"pass",
39 "reviewed_paths":["internal/agent/agent.go"],
40 "findings":[]
41 }`)})
42 got = a.deliveryReviewGateFailure()
43 if !strings.Contains(got, "security_review") || !strings.Contains(got, "mutation did not report file paths") {
44 t.Fatalf("security review gate = %q, want opaque-mutation recovery guidance", got)
45 }
46
47 ledger.Record(evidence.Receipt{ToolName: "review_report", Success: true, Args: json.RawMessage(`{
48 "kind":"security",
49 "verdict":"pass",
50 "reviewed_paths":["internal/agent/agent.go"],
51 "findings":[]
52 }`)})
53 if got := a.deliveryReviewGateFailure(); got != "" {
54 t.Fatalf("review gate = %q after both reports, want ready", got)
55 }
56 }
57
58 func TestNonDeliveryProfileNeverRequiresStructuredReview(t *testing.T) {
59 ledger := evidence.NewLedger()
60 ledger.Record(evidence.ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/permission/gate.go"}`), true, false))
61
62 reg := tool.NewRegistry()
63 reg.Add(fakeTool{name: "review", readOnly: true})
64 reg.Add(fakeTool{name: "security_review", readOnly: true})
65 a := &Agent{deliveryProfile: false, evidence: ledger, tools: reg}
66
67 if got := a.deliveryReviewGateFailure(); got != "" {
68 t.Fatalf("non-Delivery review gate = %q, want disabled", got)
69 }
70 }
71
72 func TestDeliveryReviewGateHighRiskStillRequiresSecurityReview(t *testing.T) {
73 ledger := evidence.NewLedger()
74 ledger.Record(evidence.ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/permission/gate.go"}`), true, false))
75
76 reg := tool.NewRegistry()
77 reg.Add(fakeTool{name: "review", readOnly: true})
78 reg.Add(fakeTool{name: "security_review", readOnly: true})
79 a := &Agent{deliveryProfile: true, evidence: ledger, tools: reg}
80
81 if got := a.deliveryReviewGateFailure(); !strings.Contains(got, "high-risk") {
82 t.Fatalf("review gate = %q, want high-risk review demand", got)
83 }
84
85 ledger.Record(evidence.Receipt{ToolName: "review_report", Success: true, Args: json.RawMessage(`{
86 "kind":"review",
87 "verdict":"pass",
88 "reviewed_paths":["internal/permission/gate.go"],
89 "findings":[]
90 }`)})
91 if got := a.deliveryReviewGateFailure(); !strings.Contains(got, "security_review") {
92 t.Fatalf("security review gate = %q, want security_review demand", got)
93 }
94
95 ledger.Record(evidence.Receipt{ToolName: "review_report", Success: true, Args: json.RawMessage(`{
96 "kind":"security",
97 "verdict":"pass",
98 "reviewed_paths":["internal/permission/gate.go"],
99 "findings":[]
100 }`)})
101 if got := a.deliveryReviewGateFailure(); got != "" {
102 t.Fatalf("review gate = %q after both reports, want ready", got)
103 }
104 }
105
106 func TestDeliveryReviewGateMediumAcceptsHostProvenVerificationAndCoverage(t *testing.T) {
107 ledger := evidence.NewLedger()
108 ledger.Record(evidence.ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/agent/parser.go"}`), true, false))
109 ledger.Record(evidence.ReceiptFromToolCall("bash", json.RawMessage(`{"command":"go test ./..."}`), true, true))
110 ledger.Record(evidence.ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/agent/parser.go"}`), true, true))
111 ledger.Record(evidence.Receipt{ToolName: "complete_step", Success: true, Args: json.RawMessage(`{
112 "step":"fix parser",
113 "evidence":[{"kind":"verification","command":"go test ./..."}]
114 }`)})
115
116 reg := tool.NewRegistry()
117 reg.Add(fakeTool{name: "review", readOnly: true})
118 a := &Agent{deliveryProfile: true, evidence: ledger, tools: reg}
119 if got := a.deliveryReviewGateFailure(); got != "" {
120 t.Fatalf("medium-risk host proof was rejected: %q", got)
121 }
122
123 missingVerification := evidence.NewLedger()
124 missingVerification.Record(evidence.ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/agent/parser.go"}`), true, false))
125 missingVerification.Record(evidence.ReceiptFromToolCall("read_file", json.RawMessage(`{"path":"internal/agent/parser.go"}`), true, true))
126 a.evidence = missingVerification
127 if got := a.deliveryReviewGateFailure(); !strings.Contains(got, "host-proven verification") {
128 t.Fatalf("medium-risk review without verification = %q, want host-proof guidance", got)
129 }
130 }
131
132 func TestDeliveryReviewGateDefersToParentInSubagents(t *testing.T) {
133 ledger := evidence.NewLedger()
134 ledger.Record(evidence.ReceiptFromToolCall("edit_file", json.RawMessage(`{"path":"internal/permission/gate.go"}`), true, false))
135
136 reg := tool.NewRegistry()
137 reg.Add(fakeTool{name: "review", readOnly: true})
138 reg.Add(fakeTool{name: "security_review", readOnly: true})
139 a := &Agent{deliveryProfile: true, evidence: ledger, tools: reg, subagentDepth: 1}
140
141 // Inside a sub-agent the structured-review contract belongs to the parent,
142 // which receives the child's mutation receipts via mergeChildEvidence. The
143 // child must not wedge against a review_report demand it may be unable to
144 // satisfy.
145 if got := a.deliveryReviewGateFailure(); got != "" {
146 t.Fatalf("subagent review gate = %q, want deferred to parent", got)
147 }
148 }
149
149 lines GO