返回 DeepSeek-Reasonix
review_test.go
根目录 / internal / cli / review_test.go
1 package cli
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strconv"
9 "strings"
10 "testing"
11
12 "reasonix/internal/config"
13 "reasonix/internal/skill"
14 )
15
16 func TestBuildReviewTask(t *testing.T) {
17 // Small diff.
18 diff := "diff --git a/foo.go b/foo.go\n+added line"
19 got := buildReviewTask(diff, "")
20 if !strings.Contains(got, "Review the following changes.") {
21 t.Error("missing review prompt prefix")
22 }
23 if !strings.Contains(got, diff) {
24 t.Errorf("diff content missing:\n%s", got)
25 }
26
27 // With extra instructions.
28 got = buildReviewTask(diff, "focus on error handling")
29 if !strings.Contains(got, "focus on error handling") {
30 t.Error("extra instructions missing")
31 }
32 if !strings.Contains(got, "The diff is:") {
33 t.Error("missing diff separator")
34 }
35
36 // Truncation.
37 hugeDiff := strings.Repeat("x", 20000)
38 got = buildReviewTask(hugeDiff, "")
39 if !strings.Contains(got, "truncated at 16000") {
40 t.Error("large diff should be truncated")
41 }
42 if len(got) > 16500 {
43 t.Errorf("truncated output too long: %d", len(got))
44 }
45 }
46
47 func TestBuildReviewSubagentRegistryUsesForegroundOnlyBash(t *testing.T) {
48 reg := buildReviewSubagentRegistry(skill.Skill{AllowedTools: []string{
49 "bash",
50 "wait",
51 "bash_output",
52 "kill_shell",
53 "task",
54 }}, config.Default(), t.TempDir())
55
56 for _, hidden := range []string{"wait", "bash_output", "kill_shell", "task"} {
57 if _, ok := reg.Get(hidden); ok {
58 t.Fatalf("review subagent registry should hide %q; got %v", hidden, reg.Names())
59 }
60 }
61 bash, ok := reg.Get("bash")
62 if !ok {
63 t.Fatalf("review subagent registry should keep bash; got %v", reg.Names())
64 }
65 if strings.Contains(string(bash.Schema()), "run_in_background") {
66 t.Fatalf("review subagent bash schema should not include run_in_background: %s", bash.Schema())
67 }
68 if _, err := bash.Execute(context.Background(), json.RawMessage(`{"command":"sleep 1","run_in_background":true}`)); err == nil || !strings.Contains(err.Error(), "background bash is unavailable in subagents") {
69 t.Fatalf("review subagent background bash should return a clear error, got %v", err)
70 }
71 }
72
73 // TestBuildReviewSubagentRegistryConfinesReaders pins the sandbox contract:
74 // the CLI review registry must honor the user's [sandbox] forbid_read config
75 // exactly like an in-session registry. The zero-value readers registered at
76 // init are unconfined, so before this the review subagent could read paths a
77 // normal session refuses.
78 func TestBuildReviewSubagentRegistryConfinesReaders(t *testing.T) {
79 root := t.TempDir()
80 secret := filepath.Join(root, "secrets")
81 if err := os.MkdirAll(secret, 0o755); err != nil {
82 t.Fatal(err)
83 }
84 if err := os.WriteFile(filepath.Join(secret, "token.txt"), []byte("hunter2"), 0o644); err != nil {
85 t.Fatal(err)
86 }
87 cfg := config.Default()
88 cfg.Sandbox.ForbidRead = []string{secret}
89
90 reg := buildReviewSubagentRegistry(skill.Skill{
91 ReadOnly: true,
92 AllowedTools: []string{"read_file"},
93 }, cfg, root)
94
95 rf, ok := reg.Get("read_file")
96 if !ok {
97 t.Fatalf("read_file missing; got %v", reg.Names())
98 }
99 out, err := rf.Execute(context.Background(), json.RawMessage(`{"path":`+strconv.Quote(filepath.Join(secret, "token.txt"))+`}`))
100 if err == nil && strings.Contains(out, "hunter2") {
101 t.Fatalf("forbid_read path was readable through the review registry: %s", out)
102 }
103 if err == nil {
104 t.Fatalf("expected a not-exist style refusal, got output: %s", out)
105 }
106 }
107
108 // TestBuildReviewSubagentRegistryEnforcesReadOnlySkill pins the CLI path of the
109 // review read-only contract: `reasonix review` runs the same builtin skill as
110 // the in-session review tool, so its bash must enforce the read-only
111 // policy instead of trusting the prompt's "stay read-only" promise.
112 func TestBuildReviewSubagentRegistryEnforcesReadOnlySkill(t *testing.T) {
113 reg := buildReviewSubagentRegistry(skill.Skill{
114 ReadOnly: true,
115 AllowedTools: []string{"bash", "read_file", "task"},
116 }, config.Default(), t.TempDir())
117
118 if _, ok := reg.Get("task"); ok {
119 t.Fatalf("read-only review registry should hide task; got %v", reg.Names())
120 }
121 bash, ok := reg.Get("bash")
122 if !ok {
123 t.Fatalf("read-only review registry should keep bash; got %v", reg.Names())
124 }
125 if !bash.ReadOnly() {
126 t.Fatal("read-only review bash wrapper must report ReadOnly")
127 }
128 out, err := bash.Execute(context.Background(), json.RawMessage(`{"command":"rm -rf tmp"}`))
129 if err != nil || !strings.HasPrefix(out, "blocked:") {
130 t.Fatalf("write-capable command should be blocked as tool output, got %q, %v", out, err)
131 }
132 }
133
133 lines GO