返回 DeepSeek-Reasonix
spawn_boundary_test.go
根目录 / internal / agent / spawn_boundary_test.go
1 package agent
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 "reasonix/internal/tool"
13 )
14
15 // knownDirectChildRunners are the entry points that still construct a child
16 // outside TaskTool.RunProfileSpec. Each one re-resolves tools, depth, and
17 // permissions on its own, which is exactly how a boundary added in one place
18 // gets missed in another. The list may shrink; adding to it needs a reason in
19 // the pull request.
20 var knownDirectChildRunners = map[string]string{
21 "internal/agent/task.go": "defines the runners and is the unified path itself",
22 "internal/boot/boot.go": "skill try + run_skill runners",
23 "internal/cli/review.go": "reasonix review",
24 "desktop/subagents_app.go": "desktop profile preview",
25 }
26
27 // A new fork must be a deliberate, reviewed choice rather than something that
28 // appears because one more caller found the low-level runner convenient.
29 func TestChildConstructionForksStayEnumerated(t *testing.T) {
30 root, err := filepath.Abs("../..")
31 if err != nil {
32 t.Fatal(err)
33 }
34 var found []string
35 err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
36 if err != nil || d.IsDir() {
37 if d != nil && d.IsDir() && (d.Name() == ".git" || d.Name() == "node_modules" || d.Name() == ".claude") {
38 return filepath.SkipDir
39 }
40 return nil
41 }
42 if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
43 return nil
44 }
45 body, readErr := os.ReadFile(path)
46 if readErr != nil {
47 return readErr
48 }
49 text := string(body)
50 if !strings.Contains(text, "RunSubAgentWithSession(") && !strings.Contains(text, "RunReadOnlySubAgentWithSession(") {
51 return nil
52 }
53 rel, relErr := filepath.Rel(root, path)
54 if relErr != nil {
55 return relErr
56 }
57 rel = filepath.ToSlash(rel)
58 if _, known := knownDirectChildRunners[rel]; !known {
59 found = append(found, rel)
60 }
61 return nil
62 })
63 if err != nil {
64 t.Fatal(err)
65 }
66 if len(found) > 0 {
67 t.Fatalf("these files construct a sub-agent outside the unified runner: %s\n\nCompile the call into a ProfileExecSpec and run it through TaskTool.RunProfileSpec so it inherits tool scoping, depth caps, write claims, scheduler slots, and the completion contract. If a direct runner is genuinely required, add the file to knownDirectChildRunners with a reason.",
68 strings.Join(found, ", "))
69 }
70 }
71
72 // Converging read_only_task onto the unified runner must not quietly give it
73 // durable side effects: its contract is that the call leaves nothing behind.
74 func TestReadOnlyTaskStaysEphemeralOnTheUnifiedRunner(t *testing.T) {
75 root := t.TempDir()
76 reg := tool.NewRegistry()
77 reg.Add(fakeReadFileTool{})
78 prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
79 {{Type: provider.ChunkText, Text: "research done"}, {Type: provider.ChunkDone}},
80 }}
81 task := NewTaskTool(prov, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
82 WithTranscripts(mustSubagentStore(t), root, "base", "high").
83 WithScheduler(NewSubagentScheduler(4, 4))
84
85 ctx := withCallContext(context.Background(), "call-1", event.Discard, nil, false)
86 ctx = WithParentSession(ctx, filepath.Join(root, "parent.jsonl"))
87 out, err := NewReadOnlyTaskTool(task).Execute(ctx, []byte(`{"prompt":"inspect the parser"}`))
88 if err != nil {
89 t.Fatalf("read_only_task: %v", err)
90 }
91 if !strings.Contains(out, "research done") {
92 t.Fatalf("answer = %q", out)
93 }
94 if strings.Contains(out, "Subagent reference") {
95 t.Fatalf("read_only_task must not persist a transcript even under a parent session:\n%s", out)
96 }
97 }
98
98 lines GO