返回 DeepSeek-Reasonix
goal_recovery_tool_test.go
根目录 / internal / control / goal_recovery_tool_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/event"
14 goaldomain "reasonix/internal/goal"
15 "reasonix/internal/session"
16 "reasonix/internal/tool"
17 )
18
19 type restoredGoalRunner struct {
20 mu sync.Mutex
21 sources []tool.GoalSource
22 done chan struct{}
23 }
24
25 func (r *restoredGoalRunner) Run(ctx context.Context, _ string) error {
26 binding, ok := tool.GoalLifecycleFromContext(ctx)
27 if !ok {
28 return context.Canceled
29 }
30 r.mu.Lock()
31 r.sources = append(r.sources, binding.Authority.Source)
32 call := len(r.sources)
33 r.mu.Unlock()
34 view, err := binding.Owner.GetGoal(ctx)
35 if err != nil || view == nil {
36 return err
37 }
38 switch call {
39 case 1:
40 if view.Phase != goaldomain.PhaseActive || view.Activation != goaldomain.ActivationDisarmed {
41 return errors.New("restored goal was not active/disarmed")
42 }
43 // Exercise the model-facing schema and decoder with the redundant fields
44 // models commonly echo after get_goal during cold-session recovery.
45 resumeTool, ok := tool.LookupBuiltin("update_goal")
46 if !ok {
47 return errors.New("update_goal is not registered")
48 }
49 args, err := json.Marshal(map[string]any{
50 "goal_id": view.ID, "revision": view.Revision, "action": "resume",
51 "objective": "echoed objective must not replace the target", "max_goal_rounds": nil, "blocked_reason": "",
52 })
53 if err != nil {
54 return err
55 }
56 validation := tool.ValidateArguments(resumeTool, args)
57 if validation.CompileErr != nil || len(validation.Violations) != 0 {
58 return errors.New("resume arguments failed host validation")
59 }
60 result, err := resumeTool.Execute(ctx, args)
61 if err == nil && !strings.Contains(result, "Fields not applied: objective, max_goal_rounds, blocked_reason.") {
62 return errors.New("resume result did not disclose ignored fields")
63 }
64 return err
65 case 2:
66 return nil
67 case 3:
68 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
69 close(r.done)
70 return err
71 default:
72 return errors.New("unexpected extra goal round")
73 }
74 }
75
76 func TestColdRestoredGoalCanResumeFromNaturalUserRequestAndContinue(t *testing.T) {
77 root := t.TempDir()
78 persistence := session.NewFilesystemPersistence(root)
79 seedService, err := session.NewService("desktop", persistence)
80 if err != nil {
81 t.Fatal(err)
82 }
83 t.Cleanup(func() { _ = seedService.CloseAll(context.Background()) })
84 seedRuntime, err := seedService.Create(t.Context(), session.CreateOptions{SessionID: "restored-goal"})
85 if err != nil {
86 t.Fatal(err)
87 }
88 machine := goaldomain.NewMachine(nil, func() string { return "restored-goal-id" })
89 limit := uint64(10)
90 if _, err := machine.Create(goaldomain.CreateRequest{Objective: "finish the restored target", MaxGoalRounds: &limit}); err != nil {
91 t.Fatal(err)
92 }
93 payload, err := machine.Encode()
94 if err != nil {
95 t.Fatal(err)
96 }
97 if _, err := seedRuntime.Session().Append(t.Context(), session.Batch{OperationID: "seed-goal", Events: []session.Event{{Kind: "goal/state", Payload: payload}}}); err != nil {
98 t.Fatal(err)
99 }
100 if err := seedService.Close(t.Context(), seedRuntime.Ref()); err != nil {
101 t.Fatal(err)
102 }
103
104 service, err := session.NewService("desktop", persistence)
105 if err != nil {
106 t.Fatal(err)
107 }
108 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
109 binding, err := service.Open(t.Context(), session.SessionRef{HostID: "desktop", SessionID: "restored-goal"})
110 if err != nil {
111 t.Fatal(err)
112 }
113 runtime := binding.Runtime()
114 runner := &restoredGoalRunner{done: make(chan struct{})}
115 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
116 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
117 cleanupGoalDriverController(t, c)
118 t.Cleanup(func() { _ = binding.Release(context.Background()) })
119 c.Send("继续把这个目标做完")
120 select {
121 case <-runner.done:
122 case <-time.After(5 * time.Second):
123 t.Fatal("restored goal did not resume and continue")
124 }
125 view, err := c.goalLifecycleView()
126 if err != nil || view == nil || view.Phase != goaldomain.PhaseComplete || view.RoundsStarted != 2 {
127 t.Fatalf("restored goal view = %+v, err = %v", view, err)
128 }
129 if view.Objective != "finish the restored target" || view.MaxGoalRounds == nil || *view.MaxGoalRounds != limit {
130 t.Fatalf("resume changed the restored goal definition: %+v", view)
131 }
132 runner.mu.Lock()
133 sources := append([]tool.GoalSource(nil), runner.sources...)
134 runner.mu.Unlock()
135 if len(sources) != 3 || sources[0] != tool.GoalSourceDirectHuman || sources[1] != tool.GoalSourceGoalRound || sources[2] != tool.GoalSourceGoalRound {
136 t.Fatalf("restored goal authorities = %v", sources)
137 }
138 }
139
139 lines GO