返回 DeepSeek-Reasonix
write_path_test.go
根目录 / internal / sandbox / write_path_test.go
1 package sandbox
2
3 import (
4 "os"
5 "path/filepath"
6 "runtime"
7 "testing"
8 )
9
10 func TestNormalizeWriteDirHomeAndRelative(t *testing.T) {
11 home := t.TempDir()
12 work := filepath.Join(home, "proj")
13 if err := os.MkdirAll(work, 0o755); err != nil {
14 t.Fatal(err)
15 }
16 abs, display, err := NormalizeWriteDir("~/.local", work, home)
17 if err != nil {
18 t.Fatal(err)
19 }
20 wantLocal, err := ResolveAbsPath(filepath.Join(home, ".local"))
21 if err != nil {
22 t.Fatal(err)
23 }
24 if abs != wantLocal {
25 t.Fatalf("abs = %q, want %q", abs, wantLocal)
26 }
27 if display != "~/.local" {
28 t.Fatalf("display = %q, want ~/.local", display)
29 }
30 abs, display, err = NormalizeWriteDir("${HOME}/.cache", work, home)
31 if err != nil {
32 t.Fatal(err)
33 }
34 wantCache, err := ResolveAbsPath(filepath.Join(home, ".cache"))
35 if err != nil {
36 t.Fatal(err)
37 }
38 if abs != wantCache || display != "~/.cache" {
39 t.Fatalf("HOME expand abs=%q display=%q", abs, display)
40 }
41 abs, _, err = NormalizeWriteDir("out", work, home)
42 if err != nil {
43 t.Fatal(err)
44 }
45 wantOut, err := ResolveAbsPath(filepath.Join(work, "out"))
46 if err != nil {
47 t.Fatal(err)
48 }
49 if abs != wantOut {
50 t.Fatalf("relative = %q, want %q", abs, wantOut)
51 }
52 }
53
54 func TestNormalizeWriteDirRejectsGlob(t *testing.T) {
55 if _, _, err := NormalizeWriteDir("~/.local/*", t.TempDir(), t.TempDir()); err == nil {
56 t.Fatal("glob should be rejected")
57 }
58 }
59
60 func TestNormalizeWriteDirResolvesSymlinkParent(t *testing.T) {
61 root := t.TempDir()
62 realDir := filepath.Join(root, "real")
63 if err := os.Mkdir(realDir, 0o755); err != nil {
64 t.Fatal(err)
65 }
66 link := filepath.Join(root, "link")
67 if err := os.Symlink(realDir, link); err != nil {
68 t.Fatal(err)
69 }
70 abs, _, err := NormalizeWriteDir(filepath.Join(link, "nested"), root, root)
71 if err != nil {
72 t.Fatal(err)
73 }
74 want, err := ResolveAbsPath(filepath.Join(realDir, "nested"))
75 if err != nil {
76 t.Fatal(err)
77 }
78 if abs != want {
79 t.Fatalf("symlink parent = %q, want %q", abs, want)
80 }
81 }
82
83 func TestCollapseWriteRootsDropsChildren(t *testing.T) {
84 parent := filepath.Join(t.TempDir(), "a")
85 child := filepath.Join(parent, "b")
86 got := CollapseWriteRoots([]string{child, parent, parent})
87 if len(got) != 1 || got[0] != parent {
88 t.Fatalf("CollapseWriteRoots = %v, want [%s]", got, parent)
89 }
90 }
91
92 func TestIsFilesystemRoot(t *testing.T) {
93 if !IsFilesystemRoot(string(filepath.Separator)) {
94 t.Fatal("separator root should be rejected")
95 }
96 if runtime.GOOS != "windows" && !IsFilesystemRoot("/") {
97 t.Fatal("/ should be a filesystem root")
98 }
99 if IsFilesystemRoot(t.TempDir()) {
100 t.Fatal("temp dir is not a filesystem root")
101 }
102 if runtime.GOOS == "windows" {
103 if !IsFilesystemRoot(`C:\`) {
104 t.Fatal(`C:\ should be a filesystem root`)
105 }
106 }
107 }
108
109 func TestIsHomeDir(t *testing.T) {
110 home := t.TempDir()
111 if !IsHomeDir(home, home) {
112 t.Fatal("home should match itself")
113 }
114 if IsHomeDir(filepath.Join(home, "x"), home) {
115 t.Fatal("child of home is not the home directory")
116 }
117 }
118
119 func TestProtectedWritePath(t *testing.T) {
120 state := t.TempDir()
121 if !IsProtectedWritePath(state, state) {
122 t.Fatal("state root must be protected")
123 }
124 if !IsProtectedWritePath(filepath.Join(state, "sessions", "a.json"), state) {
125 t.Fatal("sessions store must be protected")
126 }
127 if !IsProtectedWritePath(filepath.Join(state, "projects", "slug", "sessions", "a.json"), state) {
128 t.Fatal("project sessions must be protected")
129 }
130 if !IsProtectedWritePath(filepath.Join(state, "projects", "slug"), state) {
131 t.Fatal("project state must not be dynamically writable")
132 }
133 if !IsProtectedWritePath(filepath.Join(state, "settings.json"), state) {
134 t.Fatal("settings.json must be protected")
135 }
136 if IsProtectedWritePath(filepath.Join(state, "skills", "x.md"), state) {
137 t.Fatal("ordinary state files are not protected")
138 }
139 if err := ValidateWriteDir(filepath.Join(state, "sessions"), state); err == nil {
140 t.Fatal("requesting sessions should be rejected")
141 }
142 if err := ValidateWriteDir(string(filepath.Separator), state); err == nil {
143 t.Fatal("filesystem root should be rejected")
144 }
145 if err := ValidateWriteDir(filepath.Join(state, "skills"), state); err != nil {
146 t.Fatalf("skills dir should be allowed: %v", err)
147 }
148 }
149
150 func TestProtectedWriteRootsProtectsFutureState(t *testing.T) {
151 state := t.TempDir()
152 want, err := ResolveAbsPath(state)
153 if err != nil {
154 t.Fatal(err)
155 }
156 got := ProtectedWriteRoots(state)
157 if len(got) != 1 || got[0] != want {
158 t.Fatalf("ProtectedWriteRoots = %v, want state boundary %q", got, want)
159 }
160 if !PathWithin(got[0], filepath.Join(want, "desktop-future.json")) {
161 t.Fatal("state boundary should cover files created after sandbox startup")
162 }
163 }
164
165 func TestFormatConfigWritePath(t *testing.T) {
166 home := filepath.Join(string(filepath.Separator), "Users", "x")
167 got := FormatConfigWritePath(filepath.Join(home, ".local"), home)
168 if got != "${HOME}/.local" {
169 t.Fatalf("FormatConfigWritePath = %q", got)
170 }
171 }
172
173 func TestNormalizeWriteDirsCollapsesDisplay(t *testing.T) {
174 home := t.TempDir()
175 parent := filepath.Join(home, ".local")
176 child := filepath.Join(parent, "bin")
177 abs, display, broad, err := NormalizeWriteDirs([]string{child, parent}, home, home, filepath.Join(home, "state"))
178 if err != nil {
179 t.Fatal(err)
180 }
181 if broad {
182 t.Fatal("child of home is not a home grant")
183 }
184 if len(abs) != 1 || len(display) != 1 || display[0] != "~/.local" {
185 t.Fatalf("abs=%v display=%v", abs, display)
186 }
187 }
188
189 func TestEnsureWriteDirCreatesAndKeepsExisting(t *testing.T) {
190 dir := filepath.Join(t.TempDir(), "a", "b")
191 var err error
192 dir, err = ResolveAbsPath(dir)
193 if err != nil {
194 t.Fatal(err)
195 }
196 state := filepath.Join(t.TempDir(), "state")
197 if got, err := EnsureWriteDir(dir, state); err != nil || got != dir {
198 t.Fatal(err)
199 }
200 info, err := os.Stat(dir)
201 if err != nil || !info.IsDir() {
202 t.Fatalf("created dir missing: %v", err)
203 }
204 if _, err := EnsureWriteDir(dir, state); err != nil {
205 t.Fatal(err)
206 }
207 file := filepath.Join(t.TempDir(), "file")
208 if err := os.WriteFile(file, []byte("x"), 0o644); err != nil {
209 t.Fatal(err)
210 }
211 if _, err := EnsureWriteDir(file, state); err == nil {
212 t.Fatal("file should not be accepted as a write directory")
213 }
214 }
215
216 func TestEnsureWriteDirRejectsApprovalIdentityChange(t *testing.T) {
217 root := t.TempDir()
218 approvedParent := filepath.Join(root, "approved")
219 if err := os.Mkdir(approvedParent, 0o755); err != nil {
220 t.Fatal(err)
221 }
222 approved, _, err := NormalizeWriteDir(filepath.Join(approvedParent, "nested"), root, root)
223 if err != nil {
224 t.Fatal(err)
225 }
226 state := filepath.Join(root, "state")
227 projects := filepath.Join(state, "projects")
228 if err := os.MkdirAll(projects, 0o755); err != nil {
229 t.Fatal(err)
230 }
231 if err := os.Remove(approvedParent); err != nil {
232 t.Fatal(err)
233 }
234 if err := os.Symlink(projects, approvedParent); err != nil {
235 t.Fatal(err)
236 }
237 if _, err := EnsureWriteDir(approved, state); err == nil {
238 t.Fatal("retargeting an approved ancestor into protected state must fail")
239 }
240 }
241
242 func TestSameWritePathRequiresExactIdentity(t *testing.T) {
243 approved := filepath.Join(t.TempDir(), "Approved")
244 if !sameWritePath(approved, approved) {
245 t.Fatal("identical approved paths should match")
246 }
247 if sameWritePath(approved, filepath.Join(filepath.Dir(approved), "approved")) {
248 t.Fatal("case-only path changes must not reuse an existing approval")
249 }
250 }
251
251 lines GO