返回 DeepSeek-Reasonix
goal_recorder_isolation_test.go
根目录 / internal / agent / goal_recorder_isolation_test.go
1 package agent
2
3 import (
4 "context"
5 "slices"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 goaldomain "reasonix/internal/goal"
11 "reasonix/internal/provider"
12 "reasonix/internal/tool"
13 )
14
15 type childIsolationGoalOwner struct{ updates int }
16
17 func (*childIsolationGoalOwner) GetGoal(context.Context) (*goaldomain.View, error) { return nil, nil }
18 func (*childIsolationGoalOwner) CreateGoal(context.Context, goaldomain.CreateRequest, tool.GoalAuthority) (goaldomain.View, error) {
19 return goaldomain.View{}, nil
20 }
21 func (o *childIsolationGoalOwner) UpdateGoal(context.Context, tool.GoalUpdateRequest, tool.GoalAuthority) (goaldomain.View, error) {
22 o.updates++
23 return goaldomain.View{}, nil
24 }
25
26 func TestSubAgentDoesNotInheritParentGoalRecorder(t *testing.T) {
27 goalTool, ok := tool.LookupBuiltin("update_goal")
28 if !ok {
29 t.Fatal("update_goal builtin not registered")
30 }
31 reg := tool.NewRegistry()
32 reg.Add(goalTool)
33 prov := &scriptedProvider{name: "goal-child", turns: [][]provider.Chunk{
34 {toolCallChunk("goal", "update_goal", `{"goal_id":"goal-1","revision":1,"action":"complete"}`), {Type: provider.ChunkDone}},
35 {{Type: provider.ChunkText, Text: "Child result."}, {Type: provider.ChunkDone}},
36 }}
37 owner := &childIsolationGoalOwner{}
38 ctx := tool.WithGoalLifecycle(context.Background(), owner, tool.GoalAuthority{Source: tool.GoalSourceGoalRound})
39 sess := NewSession("child system")
40 answer, err := RunSubAgentWithSession(ctx, prov, reg, sess, "inspect the task", Options{}, event.Discard)
41 if err != nil {
42 t.Fatalf("Goal child: %v", err)
43 }
44 if answer != "Child result." {
45 t.Fatalf("Goal child answer = %q", answer)
46 }
47 for i, req := range prov.requests {
48 if !slices.Contains(toolSchemaNames(req.Tools), "update_goal") {
49 t.Fatalf("child request %d changed the static tool surface: %v", i+1, toolSchemaNames(req.Tools))
50 }
51 }
52 if owner.updates != 0 {
53 t.Fatalf("child mutated parent Goal: %d updates", owner.updates)
54 }
55 if got := lastToolResult(sess, "update_goal"); !strings.Contains(got, "top-level host-attested goal context") {
56 t.Fatalf("child update_goal result = %q", got)
57 }
58 }
59
60 func TestCoordinatorPlannerCannotReportExecutorGoalDisposition(t *testing.T) {
61 goalTool, ok := tool.LookupBuiltin("update_goal")
62 if !ok {
63 t.Fatal("update_goal builtin not registered")
64 }
65 reg := tool.NewRegistry()
66 reg.Add(goalTool)
67 planner := &mockProvider{name: "planner", streams: [][]provider.Chunk{
68 {toolCallChunk("planner-goal", "update_goal", `{"goal_id":"goal-1","revision":1,"action":"complete"}`), {Type: provider.ChunkDone}},
69 {toolCallChunk("plan-1", "submit_plan", `{"objective":"inspect the implementation","steps":[{"title":"apply and verify the fix"}]}`), {Type: provider.ChunkDone}},
70 }}
71 exec := &mockProvider{name: "executor", chunks: []provider.Chunk{{Type: provider.ChunkText, Text: "Implemented and verified."}, {Type: provider.ChunkDone}}}
72 plannerSess := NewSession("planner-sys")
73 executor := New(exec, reg, NewSession("exec-sys"), Options{}, event.Discard)
74 customPlannerReg := tool.NewRegistry()
75 customPlannerReg.Add(goalTool)
76 coord := NewCoordinator(planner, plannerSess, nil, PlannerToolRegistry(customPlannerReg), Options{}, executor, 0, event.Discard, nil)
77 owner := &childIsolationGoalOwner{}
78 ctx := withNoClosedLoop(tool.WithGoalLifecycle(context.Background(), owner, tool.GoalAuthority{Source: tool.GoalSourceDirectHuman}))
79 if err := coord.Run(ctx, "fix the goal bug"); err != nil {
80 t.Fatalf("Run: %v", err)
81 }
82 for i, req := range planner.requests {
83 if got := toolSchemaNames(req.Tools); slices.Contains(got, "update_goal") || !slices.Equal(got, []string{"submit_plan"}) {
84 t.Fatalf("planner request %d exposed goal mutation outside the top-level host: %v", i+1, got)
85 }
86 }
87 if got := lastToolResult(plannerSess, "update_goal"); !strings.Contains(got, "unknown tool") {
88 t.Fatalf("planner goal mutation did not fail closed: %q", got)
89 }
90 for i, req := range exec.requests {
91 if !slices.Contains(toolSchemaNames(req.Tools), "update_goal") {
92 t.Fatalf("executor request %d lost update_goal: %v", i+1, toolSchemaNames(req.Tools))
93 }
94 }
95 if owner.updates != 0 {
96 t.Fatalf("planner mutated executor Goal: %d updates", owner.updates)
97 }
98 }
99
99 lines GO