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