返回 DeepSeek-Reasonix
resolver_test.go
根目录 / internal / instruction / resolver_test.go
1 package instruction
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 )
9
10 func TestResolveKeepsMostSpecificSourceForExactDuplicate(t *testing.T) {
11 root := t.TempDir()
12 user := t.TempDir()
13 mustWriteInstruction(t, filepath.Join(user, "AGENTS.md"), "Always run tests.")
14 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "Always run tests.")
15
16 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root, UserDir: user})
17 if len(got.Documents) != 1 {
18 t.Fatalf("documents = %+v, want one exact instruction body", got.Documents)
19 }
20 if got.Documents[0].Scope != ScopeProject || got.Documents[0].Path != filepath.Join(root, "AGENTS.md") {
21 t.Fatalf("duplicate winner = %+v, want project source", got.Documents[0])
22 }
23 }
24
25 func TestResolveDuplicateReplacementPreservesPrecedenceOrder(t *testing.T) {
26 root := t.TempDir()
27 user := t.TempDir()
28 mustWriteInstruction(t, filepath.Join(user, "REASONIX.md"), "duplicate")
29 mustWriteInstruction(t, filepath.Join(user, "AGENTS.md"), "unique global")
30 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "duplicate")
31 mustWriteInstruction(t, filepath.Join(root, "CLAUDE.md"), "unique project")
32
33 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root, UserDir: user})
34 if len(got.Documents) != 3 {
35 t.Fatalf("documents = %+v, want three unique bodies", got.Documents)
36 }
37 if got.Documents[0].Body != "unique global" || got.Documents[1].Body != "duplicate" || got.Documents[2].Body != "unique project" {
38 t.Fatalf("precedence order = %+v", got.Documents)
39 }
40 for i := range got.Documents {
41 if got.Documents[i].Order != i {
42 t.Fatalf("document order metadata = %+v", got.Documents)
43 }
44 }
45 }
46
47 func TestResolveKeepsDistinctConventionFilesInDeterministicOrder(t *testing.T) {
48 root := t.TempDir()
49 mustWriteInstruction(t, filepath.Join(root, "REASONIX.md"), "Reasonix rule")
50 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "Portable rule")
51 mustWriteInstruction(t, filepath.Join(root, "CLAUDE.md"), "Claude-compatible rule")
52
53 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root})
54 if len(got.Documents) != 3 {
55 t.Fatalf("documents = %+v, want all three distinct sources", got.Documents)
56 }
57 for i, name := range []string{"REASONIX.md", "AGENTS.md", "CLAUDE.md"} {
58 if filepath.Base(got.Documents[i].Path) != name || got.Documents[i].Order != i {
59 t.Fatalf("document %d = %+v, want %s with stable order", i, got.Documents[i], name)
60 }
61 }
62 }
63
64 func TestResolveAppliesOnlyWorkspaceToTargetAncestorChain(t *testing.T) {
65 parent := t.TempDir()
66 root := filepath.Join(parent, "repo")
67 target := filepath.Join(root, "services", "api")
68 sibling := filepath.Join(root, "services", "web")
69 for _, dir := range []string{root, target, sibling} {
70 if err := os.MkdirAll(dir, 0o755); err != nil {
71 t.Fatal(err)
72 }
73 }
74 mustWriteInstruction(t, filepath.Join(parent, "AGENTS.md"), "outside workspace")
75 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "root rule")
76 mustWriteInstruction(t, filepath.Join(root, "services", "AGENTS.md"), "services rule")
77 mustWriteInstruction(t, filepath.Join(target, "AGENTS.md"), "api rule")
78 mustWriteInstruction(t, filepath.Join(sibling, "AGENTS.md"), "web rule")
79
80 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: target})
81 joined := documentBodies(got.Documents)
82 for _, want := range []string{"root rule", "services rule", "api rule"} {
83 if !strings.Contains(joined, want) {
84 t.Fatalf("resolved instructions missing %q: %+v", want, got.Documents)
85 }
86 }
87 for _, unwanted := range []string{"outside workspace", "web rule"} {
88 if strings.Contains(joined, unwanted) {
89 t.Fatalf("resolved instructions included %q outside target chain: %+v", unwanted, got.Documents)
90 }
91 }
92 if got.Documents[0].Scope != ScopeProject || got.Documents[1].Scope != ScopeAncestor || got.Documents[2].Scope != ScopeAncestor {
93 t.Fatalf("nested scopes = %+v", got.Documents)
94 }
95 }
96
97 func TestResolveImportsAreProvenancedDeduplicatedAndConfined(t *testing.T) {
98 root := t.TempDir()
99 outside := t.TempDir()
100 mustWriteInstruction(t, filepath.Join(root, "shared.md"), "SHARED RULE")
101 mustWriteInstruction(t, filepath.Join(root, "a.md"), "A\n@shared.md")
102 mustWriteInstruction(t, filepath.Join(root, "b.md"), "B\n@shared.md")
103 mustWriteInstruction(t, filepath.Join(outside, "secret.md"), "SECRET")
104 if err := os.Symlink(filepath.Join(outside, "secret.md"), filepath.Join(root, "linked.md")); err != nil {
105 t.Skipf("symlink unsupported: %v", err)
106 }
107 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "@a.md\n@b.md\n@../secret.md\n@linked.md")
108
109 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root})
110 if len(got.Documents) != 1 {
111 t.Fatalf("documents = %+v, want AGENTS.md", got.Documents)
112 }
113 body := got.Documents[0].Body
114 if strings.Count(body, "SHARED RULE") != 1 {
115 t.Fatalf("diamond import was not exactly deduplicated:\n%s", body)
116 }
117 for _, want := range []string{"instruction-import", "a.md", "b.md"} {
118 if !strings.Contains(body, want) {
119 t.Fatalf("resolved import missing provenance %q:\n%s", want, body)
120 }
121 }
122 if strings.Contains(body, "SECRET") {
123 t.Fatalf("external import escaped source directory:\n%s", body)
124 }
125 if len(got.Diagnostics) != 2 {
126 t.Fatalf("diagnostics = %+v, want traversal and symlink rejections", got.Diagnostics)
127 }
128 }
129
130 func TestResolveUserInstructionsImportTrustedConventionRoots(t *testing.T) {
131 home := t.TempDir()
132 t.Setenv("HOME", home)
133 t.Setenv("USERPROFILE", home) // os.UserHomeDir reads HOME on Unix and USERPROFILE on Windows.
134 userDir := filepath.Join(home, ".reasonix")
135 agentsDir := filepath.Join(home, ".agents")
136 root := filepath.Join(home, "repo")
137 mustWriteInstruction(t, filepath.Join(agentsDir, "AGENTS.md"), "SHARED USER RULE")
138 mustWriteInstruction(t, filepath.Join(userDir, "REASONIX.md"), "@~/.agents/AGENTS.md")
139 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "PROJECT RULE")
140
141 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root, UserDir: userDir})
142 body := documentBodies(got.Documents)
143 if !strings.Contains(body, "SHARED USER RULE") {
144 t.Fatalf("trusted user convention import missing: %+v", got)
145 }
146 if strings.Contains(body, home) {
147 t.Fatalf("provider-visible import provenance leaked home path:\n%s", body)
148 }
149 if len(got.Diagnostics) != 0 {
150 t.Fatalf("trusted user convention import diagnostics = %+v", got.Diagnostics)
151 }
152 }
153
154 func TestResolveProjectInstructionsCannotImportUserConventionRoots(t *testing.T) {
155 home := t.TempDir()
156 t.Setenv("HOME", home)
157 t.Setenv("USERPROFILE", home) // os.UserHomeDir reads HOME on Unix and USERPROFILE on Windows.
158 root := filepath.Join(home, "repo")
159 mustWriteInstruction(t, filepath.Join(home, ".agents", "AGENTS.md"), "PRIVATE USER RULE")
160 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "@~/.agents/AGENTS.md")
161
162 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root})
163 if strings.Contains(documentBodies(got.Documents), "PRIVATE USER RULE") {
164 t.Fatalf("project instruction escaped into user convention root: %+v", got)
165 }
166 if len(got.Diagnostics) != 1 || got.Diagnostics[0].Code != "import_outside_source" {
167 t.Fatalf("project external import diagnostics = %+v", got.Diagnostics)
168 }
169 }
170
171 func TestResolveUserInstructionsRejectArbitraryHomeAndConventionSymlinkEscape(t *testing.T) {
172 home := t.TempDir()
173 t.Setenv("HOME", home)
174 t.Setenv("USERPROFILE", home) // os.UserHomeDir reads HOME on Unix and USERPROFILE on Windows.
175 userDir := filepath.Join(home, ".reasonix")
176 agentsDir := filepath.Join(home, ".agents")
177 mustWriteInstruction(t, filepath.Join(home, "secret.md"), "HOME SECRET")
178 if err := os.MkdirAll(agentsDir, 0o755); err != nil {
179 t.Fatal(err)
180 }
181 if err := os.Symlink(filepath.Join(home, "secret.md"), filepath.Join(agentsDir, "linked.md")); err != nil {
182 t.Skipf("symlink unsupported: %v", err)
183 }
184 mustWriteInstruction(t, filepath.Join(userDir, "REASONIX.md"), "@~/secret.md\n@~/.agents/linked.md")
185
186 got := Resolve(ResolveOptions{WorkspaceRoot: t.TempDir(), TargetDir: t.TempDir(), UserDir: userDir})
187 if strings.Contains(documentBodies(got.Documents), "HOME SECRET") {
188 t.Fatalf("arbitrary home content entered instructions: %+v", got)
189 }
190 codes := map[string]bool{}
191 for _, diagnostic := range got.Diagnostics {
192 codes[diagnostic.Code] = true
193 }
194 if !codes["import_outside_source"] || !codes["import_symlink_escape"] {
195 t.Fatalf("user import rejection diagnostics = %+v", got.Diagnostics)
196 }
197 }
198
199 func TestResolveRejectsDirectInstructionSymlinkOutsideBoundary(t *testing.T) {
200 root := t.TempDir()
201 outside := t.TempDir()
202 mustWriteInstruction(t, filepath.Join(outside, "private.md"), "MACHINE-LOCAL SECRET")
203 if err := os.Symlink(filepath.Join(outside, "private.md"), filepath.Join(root, "AGENTS.md")); err != nil {
204 t.Skipf("symlink unsupported: %v", err)
205 }
206
207 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root})
208 if len(got.Documents) != 0 {
209 t.Fatalf("documents = %+v, want external symlink excluded", got.Documents)
210 }
211 if len(got.Diagnostics) != 1 || got.Diagnostics[0].Code != "document_symlink_escape" {
212 t.Fatalf("diagnostics = %+v, want document_symlink_escape", got.Diagnostics)
213 }
214 if strings.Contains(documentBodies(got.Documents), "MACHINE-LOCAL SECRET") {
215 t.Fatal("external symlink content entered provider-visible instructions")
216 }
217 }
218
219 func TestResolveAllowsDirectInstructionSymlinkWithinBoundary(t *testing.T) {
220 root := t.TempDir()
221 target := filepath.Join(root, "docs", "agent-rules.md")
222 mustWriteInstruction(t, target, "Run the focused tests.")
223 if err := os.Symlink(target, filepath.Join(root, "AGENTS.md")); err != nil {
224 t.Skipf("symlink unsupported: %v", err)
225 }
226
227 got := Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: root})
228 if len(got.Documents) != 1 || got.Documents[0].Body != "Run the focused tests." {
229 t.Fatalf("documents = %+v, want in-boundary symlink loaded", got.Documents)
230 }
231 if len(got.Diagnostics) != 0 {
232 t.Fatalf("diagnostics = %+v, want none", got.Diagnostics)
233 }
234 }
235
236 func TestInstructionBlockIsStableAcrossWorkspaceRoots(t *testing.T) {
237 resolve := func(base string) string {
238 root := filepath.Join(base, "repo")
239 target := filepath.Join(root, "services", "api")
240 user := filepath.Join(base, "reasonix-home")
241 mustWriteInstruction(t, filepath.Join(user, "AGENTS.md"), "Use concise replies.")
242 mustWriteInstruction(t, filepath.Join(root, "AGENTS.md"), "Run all tests.")
243 mustWriteInstruction(t, filepath.Join(target, "AGENTS.local.md"), "Run API tests first.")
244 return Block(Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: target, UserDir: user}).Documents)
245 }
246
247 firstRoot := t.TempDir()
248 secondRoot := t.TempDir()
249 first := resolve(firstRoot)
250 second := resolve(secondRoot)
251 if first != second {
252 t.Fatalf("provider instruction bytes changed across roots:\nfirst:\n%s\nsecond:\n%s", first, second)
253 }
254 for _, privateRoot := range []string{firstRoot, secondRoot} {
255 if strings.Contains(first, privateRoot) || strings.Contains(second, privateRoot) {
256 t.Fatalf("provider instructions exposed machine-local root %q", privateRoot)
257 }
258 }
259 for _, want := range []string{"user/AGENTS.md", "workspace/AGENTS.md", "workspace/services/api/AGENTS.local.md", "applies to workspace/services/api"} {
260 if !strings.Contains(first, want) {
261 t.Fatalf("provider instructions missing stable label %q:\n%s", want, first)
262 }
263 }
264 }
265
266 func TestInstructionBlockDerivesWorkspaceRootFromNestedDocument(t *testing.T) {
267 root := t.TempDir()
268 target := filepath.Join(root, "services", "api")
269 mustWriteInstruction(t, filepath.Join(target, "AGENTS.md"), "Run API tests first.")
270
271 block := Block(Resolve(ResolveOptions{WorkspaceRoot: root, TargetDir: target}).Documents)
272 for _, want := range []string{"workspace/services/api/AGENTS.md", "applies to workspace/services/api"} {
273 if !strings.Contains(block, want) {
274 t.Fatalf("provider instructions missing nested label %q:\n%s", want, block)
275 }
276 }
277 if strings.Contains(block, root) {
278 t.Fatalf("provider instructions exposed machine-local root %q:\n%s", root, block)
279 }
280 }
281
282 func TestImportTargetClassification(t *testing.T) {
283 for _, tc := range []struct {
284 line string
285 want bool
286 }{
287 {"@docs/setup.md", true}, {"@./notes.txt", true}, {"@/abs/path.md", true},
288 {"@mention", false}, {"@", false}, {"@a/b and more", false}, {"plain text", false},
289 } {
290 if _, got := parseImportTarget(tc.line); got != tc.want {
291 t.Errorf("parseImportTarget(%q) = %v, want %v", tc.line, got, tc.want)
292 }
293 }
294 }
295
296 func documentBodies(docs []Document) string {
297 var bodies []string
298 for _, doc := range docs {
299 bodies = append(bodies, doc.Body)
300 }
301 return strings.Join(bodies, "\n")
302 }
303
304 func mustWriteInstruction(t *testing.T, path, body string) {
305 t.Helper()
306 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
307 t.Fatal(err)
308 }
309 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
310 t.Fatal(err)
311 }
312 }
313
313 lines GO