返回 DeepSeek-Reasonix
goal_driver_test.go
根目录 / internal / control / goal_driver_test.go
1 package control
2
3 import (
4 "context"
5 "errors"
6 "os"
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/provider"
16 "reasonix/internal/session"
17 "reasonix/internal/tool"
18 )
19
20 func cleanupGoalDriverController(t *testing.T, c *Controller) {
21 t.Helper()
22 t.Cleanup(func() {
23 service, runtime, exclusive := c.v3Binding()
24 c.Close()
25 deadline := time.Now().Add(5 * time.Second)
26 for time.Now().Before(deadline) {
27 c.goalDriverMu.Lock()
28 settled := !c.goalDriverPending && c.goalDriverActive == nil
29 c.goalDriverMu.Unlock()
30 runtimeRetired := !exclusive || service == nil || runtime == nil || goalRuntimeRetired(c, service, runtime, settled)
31 if settled && !c.Running() && runtimeRetired {
32 return
33 }
34 time.Sleep(time.Millisecond)
35 }
36 t.Error("goal controller did not settle after close")
37 })
38 }
39
40 type lifecycleDriverRunner struct {
41 mu sync.Mutex
42 calls int
43 inputs []string
44 done chan struct{}
45 }
46
47 func (r *lifecycleDriverRunner) Run(ctx context.Context, input string) error {
48 r.mu.Lock()
49 r.calls++
50 call := r.calls
51 r.inputs = append(r.inputs, input)
52 r.mu.Unlock()
53 binding, ok := tool.GoalLifecycleFromContext(ctx)
54 if !ok {
55 return context.Canceled
56 }
57 switch call {
58 case 1:
59 if binding.Authority.Source != tool.GoalSourceDirectHuman {
60 return context.Canceled
61 }
62 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "finish the lifecycle"}, binding.Authority)
63 return err
64 case 2:
65 // A normal final response is not a lifecycle transition. Leaving the
66 // target active must cause another independently admitted top-level turn.
67 if binding.Authority.Source != tool.GoalSourceGoalRound {
68 return context.Canceled
69 }
70 return nil
71 case 3:
72 if binding.Authority.Source != tool.GoalSourceGoalRound {
73 return context.Canceled
74 }
75 view, err := binding.Owner.GetGoal(ctx)
76 if err != nil {
77 return err
78 }
79 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
80 close(r.done)
81 return err
82 default:
83 return context.Canceled
84 }
85 }
86
87 func TestGoalDriverContinuesAfterFinalAndCompletesThroughExactRoundAuthority(t *testing.T) {
88 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
89 if err != nil {
90 t.Fatal(err)
91 }
92 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
93 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-driver"})
94 if err != nil {
95 t.Fatal(err)
96 }
97 runner := &lifecycleDriverRunner{done: make(chan struct{})}
98 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
99 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
100 cleanupGoalDriverController(t, c)
101 c.Send("work until the whole target is finished")
102 select {
103 case <-runner.done:
104 case <-time.After(5 * time.Second):
105 t.Fatal("automatic goal round did not run")
106 }
107 deadline := time.Now().Add(5 * time.Second)
108 for (c.Running() || c.GoalStatus() != GoalStatusComplete) && time.Now().Before(deadline) {
109 time.Sleep(time.Millisecond)
110 }
111 if c.GoalStatus() != GoalStatusComplete {
112 t.Fatalf("goal status = %q", c.GoalStatus())
113 }
114 view, err := c.goalLifecycleView()
115 if err != nil || view == nil || view.RoundsStarted != 2 || view.Revision != 2 {
116 t.Fatalf("goal view = %+v, err = %v", view, err)
117 }
118 runner.mu.Lock()
119 defer runner.mu.Unlock()
120 if runner.calls != 3 || !strings.Contains(runner.inputs[1], `"round":1`) || !strings.Contains(runner.inputs[2], `"round":2`) || !strings.Contains(runner.inputs[1], "<goal-round>") {
121 t.Fatalf("calls/inputs = %d %#v", runner.calls, runner.inputs)
122 }
123 if _, err := runtime.Session().Flush(t.Context()); err != nil {
124 t.Fatal(err)
125 }
126 commits, err := runtime.Session().Handle().Read(t.Context(), 1, 100)
127 if err != nil {
128 t.Fatal(err)
129 }
130 foundAtomicAdmission := false
131 for _, commit := range commits.Commits {
132 hasStart, hasGoal := false, false
133 for _, item := range commit.Events {
134 hasStart = hasStart || item.Kind == "turn/start"
135 hasGoal = hasGoal || item.Kind == "goal/state"
136 }
137 foundAtomicAdmission = foundAtomicAdmission || hasStart && hasGoal
138 }
139 if !foundAtomicAdmission {
140 t.Fatal("automatic round did not atomically commit turn/start with goal/state")
141 }
142 }
143
144 type limitedGoalRunner struct {
145 mu sync.Mutex
146 calls int
147 }
148
149 func (r *limitedGoalRunner) Run(ctx context.Context, _ string) error {
150 r.mu.Lock()
151 r.calls++
152 call := r.calls
153 r.mu.Unlock()
154 binding, ok := tool.GoalLifecycleFromContext(ctx)
155 if !ok {
156 return context.Canceled
157 }
158 if call == 1 {
159 limit := uint64(1)
160 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "one automatic round", MaxGoalRounds: &limit}, binding.Authority)
161 return err
162 }
163 return nil
164 }
165
166 func TestGoalDriverTurnsExplicitRoundLimitIntoBlockedState(t *testing.T) {
167 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
168 if err != nil {
169 t.Fatal(err)
170 }
171 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
172 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-limit"})
173 if err != nil {
174 t.Fatal(err)
175 }
176 runner := &limitedGoalRunner{}
177 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
178 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
179 cleanupGoalDriverController(t, c)
180 c.Send("run exactly one automatic round")
181 deadline := time.Now().Add(5 * time.Second)
182 for time.Now().Before(deadline) {
183 view, viewErr := c.goalLifecycleView()
184 if viewErr != nil {
185 t.Fatal(viewErr)
186 }
187 if view != nil && view.Phase == goaldomain.PhaseBlocked {
188 if view.RoundsStarted != 1 || view.BlockedReason == nil || view.BlockedReason.Code != "round-limit" {
189 t.Fatalf("blocked view = %+v", view)
190 }
191 return
192 }
193 time.Sleep(time.Millisecond)
194 }
195 t.Fatal("round-limited goal did not enter blocked state")
196 }
197
198 func TestDuplicateGoalDriverKicksAdmitOnlyOneRound(t *testing.T) {
199 // Reservation identity is checked again after Flush and by guarded turn
200 // admission; this focused test exercises level-trigger collapse itself.
201 c := &Controller{}
202 c.closed = true
203 for range 20 {
204 c.kickGoalDriver()
205 }
206 c.goalDriverMu.Lock()
207 defer c.goalDriverMu.Unlock()
208 if c.goalDriverPending || c.goalDriverActive != nil {
209 t.Fatalf("closed driver accepted work: pending=%v active=%v", c.goalDriverPending, c.goalDriverActive)
210 }
211 }
212
213 type gatedGoalRunner struct {
214 mu sync.Mutex
215 calls int
216 started chan struct{}
217 release chan struct{}
218 }
219
220 func (r *gatedGoalRunner) Run(ctx context.Context, _ string) error {
221 r.mu.Lock()
222 r.calls++
223 call := r.calls
224 r.mu.Unlock()
225 binding, ok := tool.GoalLifecycleFromContext(ctx)
226 if !ok {
227 return context.Canceled
228 }
229 if call == 1 {
230 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "deduplicate idle notifications"}, binding.Authority)
231 return err
232 }
233 if call == 2 {
234 close(r.started)
235 select {
236 case <-r.release:
237 case <-ctx.Done():
238 return ctx.Err()
239 }
240 view, err := binding.Owner.GetGoal(ctx)
241 if err != nil {
242 return err
243 }
244 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
245 return err
246 }
247 return context.Canceled
248 }
249
250 func TestConcurrentIdleKicksCannotAdmitParallelGoalRounds(t *testing.T) {
251 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
252 if err != nil {
253 t.Fatal(err)
254 }
255 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
256 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-dedup"})
257 if err != nil {
258 t.Fatal(err)
259 }
260 runner := &gatedGoalRunner{started: make(chan struct{}), release: make(chan struct{})}
261 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
262 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
263 cleanupGoalDriverController(t, c)
264 c.Send("start a deduplicated target")
265 select {
266 case <-runner.started:
267 case <-time.After(5 * time.Second):
268 t.Fatal("automatic round did not start")
269 }
270 var kicks sync.WaitGroup
271 for range 32 {
272 kicks.Go(func() {
273 c.kickGoalDriver()
274 })
275 }
276 kicks.Wait()
277 runner.mu.Lock()
278 calls := runner.calls
279 runner.mu.Unlock()
280 if calls != 2 {
281 t.Fatalf("parallel idle kicks admitted %d calls, want initial + one goal round", calls)
282 }
283 close(runner.release)
284 deadline := time.Now().Add(5 * time.Second)
285 for time.Now().Before(deadline) {
286 view, _ := c.goalLifecycleView()
287 if view != nil && view.Phase == goaldomain.PhaseComplete {
288 return
289 }
290 time.Sleep(time.Millisecond)
291 }
292 t.Fatal("deduplicated goal round did not finish")
293 }
294
295 type cancelGoalRunner struct {
296 started chan struct{}
297 }
298
299 func (r *cancelGoalRunner) Run(ctx context.Context, _ string) error {
300 binding, ok := tool.GoalLifecycleFromContext(ctx)
301 if !ok {
302 return context.Canceled
303 }
304 if binding.Authority.Source == tool.GoalSourceDirectHuman {
305 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "pause the running target"}, binding.Authority)
306 return err
307 }
308 close(r.started)
309 <-ctx.Done()
310 return ctx.Err()
311 }
312
313 func TestPausingRunningGoalRoundCancelsActivityAndPersistsPaused(t *testing.T) {
314 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
315 if err != nil {
316 t.Fatal(err)
317 }
318 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
319 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-cancel"})
320 if err != nil {
321 t.Fatal(err)
322 }
323 runner := &cancelGoalRunner{started: make(chan struct{})}
324 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
325 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
326 cleanupGoalDriverController(t, c)
327 c.Send("start then pause")
328 select {
329 case <-runner.started:
330 case <-time.After(5 * time.Second):
331 t.Fatal("automatic goal round did not start")
332 }
333 if !c.PauseGoal() {
334 t.Fatal("PauseGoal rejected an active goal round")
335 }
336 deadline := time.Now().Add(5 * time.Second)
337 for c.Running() && time.Now().Before(deadline) {
338 time.Sleep(time.Millisecond)
339 }
340 view, err := c.goalLifecycleView()
341 if err != nil || view == nil || view.Phase != goaldomain.PhasePaused || view.Activation != goaldomain.ActivationDisarmed || view.RoundsStarted != 1 {
342 t.Fatalf("paused view = %+v, err = %v", view, err)
343 }
344 }
345
346 type completeThenCancelGoalRunner struct {
347 completed chan struct{}
348 }
349
350 func (r *completeThenCancelGoalRunner) Run(ctx context.Context, _ string) error {
351 binding, ok := tool.GoalLifecycleFromContext(ctx)
352 if !ok {
353 return context.Canceled
354 }
355 if binding.Authority.Source == tool.GoalSourceDirectHuman {
356 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "complete before cancellation"}, binding.Authority)
357 return err
358 }
359 view, err := binding.Owner.GetGoal(ctx)
360 if err != nil {
361 return err
362 }
363 if _, err := binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority); err != nil {
364 return err
365 }
366 close(r.completed)
367 <-ctx.Done()
368 return ctx.Err()
369 }
370
371 func TestCancellationAfterAcceptedCompleteDoesNotRewriteGoalToPaused(t *testing.T) {
372 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
373 if err != nil {
374 t.Fatal(err)
375 }
376 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
377 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-complete-then-cancel"})
378 if err != nil {
379 t.Fatal(err)
380 }
381 runner := &completeThenCancelGoalRunner{completed: make(chan struct{})}
382 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
383 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
384 cleanupGoalDriverController(t, c)
385 c.Send("start and finish the target")
386 select {
387 case <-runner.completed:
388 case <-time.After(5 * time.Second):
389 t.Fatal("automatic goal round did not complete")
390 }
391 c.Cancel()
392 deadline := time.Now().Add(5 * time.Second)
393 for c.Running() && time.Now().Before(deadline) {
394 time.Sleep(time.Millisecond)
395 }
396 view, err := c.goalLifecycleView()
397 if err != nil || view == nil || view.Phase != goaldomain.PhaseComplete || view.StopReason != "complete" {
398 t.Fatalf("completed goal after cancellation = %+v, err = %v", view, err)
399 }
400 }
401
402 type unlimitedGoalRunner struct {
403 mu sync.Mutex
404 calls uint64
405 autoLimit uint64
406 done chan struct{}
407 }
408
409 func (r *unlimitedGoalRunner) Run(ctx context.Context, _ string) error {
410 r.mu.Lock()
411 r.calls++
412 r.mu.Unlock()
413 binding, ok := tool.GoalLifecycleFromContext(ctx)
414 if !ok {
415 return context.Canceled
416 }
417 if binding.Authority.Source == tool.GoalSourceDirectHuman {
418 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "run beyond the old hidden ceiling"}, binding.Authority)
419 return err
420 }
421 if binding.Authority.Round == r.autoLimit {
422 view, err := binding.Owner.GetGoal(ctx)
423 if err != nil {
424 return err
425 }
426 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
427 close(r.done)
428 return err
429 }
430 return nil
431 }
432
433 func TestUnlimitedGoalDriverRunsBeyondHarnessDefaultCeiling(t *testing.T) {
434 service := goalRoundTestService(t)
435 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-unlimited"})
436 if err != nil {
437 t.Fatal(err)
438 }
439 runner := &unlimitedGoalRunner{autoLimit: 257, done: make(chan struct{})}
440 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
441 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
442 cleanupGoalDriverController(t, c)
443 c.Send("exercise the unlimited goal driver")
444 select {
445 case <-runner.done:
446 // This asserts the admission count, not disk throughput. The enclosing
447 // test command supplies the watchdog for a genuinely stalled driver.
448 case <-t.Context().Done():
449 t.Fatal("unlimited goal did not cross 256 admitted automatic rounds")
450 }
451 view, err := c.goalLifecycleView()
452 if err != nil || view == nil || view.Phase != goaldomain.PhaseComplete || view.RoundsStarted != 257 || view.MaxGoalRounds != nil {
453 t.Fatalf("unlimited goal view = %+v, err = %v", view, err)
454 }
455 waitForGoalDriverIdle(t, c, runtime)
456 }
457
458 func waitForGoalDriverIdle(t *testing.T, c *Controller, runtime *session.Runtime) {
459 t.Helper()
460 deadline := time.Now().Add(5 * time.Second)
461 for time.Now().Before(deadline) {
462 c.goalDriverMu.Lock()
463 settled := !c.goalDriverPending && c.goalDriverActive == nil
464 c.goalDriverMu.Unlock()
465 if settled && !c.Running() && runtime.Snapshot().Phase == session.RuntimeIdle {
466 return
467 }
468 time.Sleep(time.Millisecond)
469 }
470 t.Fatal("goal driver did not return to idle after terminal update")
471 }
472
473 type budgetedGoalRunner struct {
474 usage event.Sink
475 calls int
476 }
477
478 func (r *budgetedGoalRunner) Run(ctx context.Context, _ string) error {
479 r.calls++
480 binding, ok := tool.GoalLifecycleFromContext(ctx)
481 if !ok {
482 return context.Canceled
483 }
484 if binding.Authority.Source == tool.GoalSourceDirectHuman {
485 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "respect the host resource budget"}, binding.Authority)
486 return err
487 }
488 r.usage.Emit(event.Event{Kind: event.Usage, UsageSource: event.UsageSourceExecutor,
489 Usage: &provider.Usage{PromptTokens: 80, CompletionTokens: 40, TotalTokens: 120, RequestCount: 1}})
490 return nil
491 }
492
493 func TestGoalDriverBlocksAtExplicitHostTokenBudget(t *testing.T) {
494 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
495 if err != nil {
496 t.Fatal(err)
497 }
498 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
499 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-budget"})
500 if err != nil {
501 t.Fatal(err)
502 }
503 runner := &budgetedGoalRunner{}
504 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
505 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, GoalTokenBudget: 100,
506 SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
507 runner.usage = c.goalUsageTee
508 cleanupGoalDriverController(t, c)
509 c.Send("run within a fixed token budget")
510 deadline := time.Now().Add(5 * time.Second)
511 for time.Now().Before(deadline) {
512 view, _ := c.goalLifecycleView()
513 if view != nil && view.Phase == goaldomain.PhaseBlocked {
514 if view.BlockedReason == nil || view.BlockedReason.Code != "resource-budget" || view.RoundsStarted != 1 || runner.calls != 2 {
515 t.Fatalf("blocked view/calls = %+v / %d", view, runner.calls)
516 }
517 runtimeView := c.GoalRuntime()
518 if runtimeView.TokensUsed != 120 || runtimeView.TokensLimit != 100 || runtimeView.RequestsUsed != 1 {
519 t.Fatalf("resource runtime = %+v", runtimeView)
520 }
521 return
522 }
523 time.Sleep(time.Millisecond)
524 }
525 t.Fatal("goal did not stop at the explicit host token budget")
526 }
527
528 type modelErrorGoalRunner struct {
529 calls int
530 }
531
532 func (r *modelErrorGoalRunner) Run(ctx context.Context, _ string) error {
533 r.calls++
534 binding, ok := tool.GoalLifecycleFromContext(ctx)
535 if !ok {
536 return context.Canceled
537 }
538 if binding.Authority.Source == tool.GoalSourceDirectHuman {
539 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "stop safely on provider errors"}, binding.Authority)
540 return err
541 }
542 return errors.New("provider failed")
543 }
544
545 func TestGoalRoundModelErrorDisarmsWithoutCompleting(t *testing.T) {
546 service, err := session.NewService("desktop", session.NewFilesystemPersistence(t.TempDir()))
547 if err != nil {
548 t.Fatal(err)
549 }
550 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
551 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-model-error"})
552 if err != nil {
553 t.Fatal(err)
554 }
555 runner := &modelErrorGoalRunner{}
556 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
557 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
558 cleanupGoalDriverController(t, c)
559 c.Send("exercise model failure")
560 deadline := time.Now().Add(5 * time.Second)
561 for time.Now().Before(deadline) {
562 view, _ := c.goalLifecycleView()
563 if view != nil && view.Activation == goaldomain.ActivationDisarmed && view.StopReason == "model-error" {
564 if view.Phase != goaldomain.PhaseActive || view.RoundsStarted != 1 || runner.calls != 2 {
565 t.Fatalf("error view/calls = %+v / %d", view, runner.calls)
566 }
567 return
568 }
569 time.Sleep(time.Millisecond)
570 }
571 t.Fatal("model error did not disarm the active goal")
572 }
573
574 type failingFlushPersistence struct {
575 session *session.Session
576 }
577
578 func (p failingFlushPersistence) Create(session.CreateOptions) (*session.Session, error) {
579 return p.session, nil
580 }
581 func (p failingFlushPersistence) Open(string, session.AccessMode) (*session.Session, error) {
582 return nil, session.ErrSessionNotFound
583 }
584 func (p failingFlushPersistence) Stat(context.Context, string) (session.SessionInfo, error) {
585 return session.SessionInfo{}, session.ErrSessionNotFound
586 }
587 func (p failingFlushPersistence) List(context.Context, string, int) (session.SessionPage, error) {
588 return session.SessionPage{}, nil
589 }
590
591 func TestGoalDriverFlushFailureStartsNoAutomaticModelCall(t *testing.T) {
592 store, err := session.CreateWithOptions(t.TempDir()+"/goal-flush", "goal-flush", session.OpenOptions{
593 Sync: func(*os.File) error { return errors.New("injected sync failure") },
594 })
595 if err != nil {
596 t.Fatal(err)
597 }
598 service, err := session.NewService("desktop", failingFlushPersistence{session: store})
599 if err != nil {
600 t.Fatal(err)
601 }
602 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
603 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-flush"})
604 if err != nil {
605 t.Fatal(err)
606 }
607 runner := &modelErrorGoalRunner{}
608 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
609 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
610 cleanupGoalDriverController(t, c)
611 c.Send("create before a failed checkpoint")
612 deadline := time.Now().Add(5 * time.Second)
613 for time.Now().Before(deadline) {
614 view, _ := c.goalLifecycleView()
615 if view != nil && view.StopReason == "persistence-error" {
616 if runner.calls != 1 || view.RoundsStarted != 0 || view.Phase != goaldomain.PhaseActive || view.Activation != goaldomain.ActivationDisarmed {
617 t.Fatalf("persistence failure view/calls = %+v / %d", view, runner.calls)
618 }
619 return
620 }
621 time.Sleep(time.Millisecond)
622 }
623 t.Fatal("failed durability checkpoint did not stop automatic scheduling")
624 }
625
626 type userWinsRunner struct {
627 mu sync.Mutex
628 sources []tool.GoalSource
629 }
630
631 func (r *userWinsRunner) Run(ctx context.Context, _ string) error {
632 binding, ok := tool.GoalLifecycleFromContext(ctx)
633 if !ok {
634 return context.Canceled
635 }
636 r.mu.Lock()
637 r.sources = append(r.sources, binding.Authority.Source)
638 call := len(r.sources)
639 r.mu.Unlock()
640 if call == 1 {
641 _, err := binding.Owner.CreateGoal(ctx, goaldomain.CreateRequest{Objective: "let queued user input win"}, binding.Authority)
642 return err
643 }
644 view, err := binding.Owner.GetGoal(ctx)
645 if err != nil {
646 return err
647 }
648 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
649 return err
650 }
651
652 func TestUserInputArrivingDuringGoalFlushWinsAdmission(t *testing.T) {
653 flushStarted := make(chan struct{})
654 releaseFlush := make(chan struct{})
655 var once sync.Once
656 store, err := session.CreateWithOptions(t.TempDir()+"/goal-user-wins", "goal-user-wins", session.OpenOptions{
657 Sync: func(*os.File) error {
658 once.Do(func() { close(flushStarted) })
659 <-releaseFlush
660 return nil
661 },
662 })
663 if err != nil {
664 t.Fatal(err)
665 }
666 service, err := session.NewService("desktop", failingFlushPersistence{session: store})
667 if err != nil {
668 t.Fatal(err)
669 }
670 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
671 runtime, err := service.Create(t.Context(), session.CreateOptions{SessionID: "goal-user-wins"})
672 if err != nil {
673 t.Fatal(err)
674 }
675 runner := &userWinsRunner{}
676 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
677 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
678 cleanupGoalDriverController(t, c)
679 c.Send("create the target")
680 select {
681 case <-flushStarted:
682 case <-time.After(5 * time.Second):
683 t.Fatal("goal driver did not reach the durability checkpoint")
684 }
685 c.Send("finish it from my newer message")
686 close(releaseFlush)
687 deadline := time.Now().Add(5 * time.Second)
688 for time.Now().Before(deadline) {
689 view, _ := c.goalLifecycleView()
690 if view != nil && view.Phase == goaldomain.PhaseComplete {
691 runner.mu.Lock()
692 sources := append([]tool.GoalSource(nil), runner.sources...)
693 runner.mu.Unlock()
694 if len(sources) != 2 || sources[0] != tool.GoalSourceDirectHuman || sources[1] != tool.GoalSourceDirectHuman || view.RoundsStarted != 0 {
695 t.Fatalf("sources/view = %v / %+v", sources, view)
696 }
697 return
698 }
699 time.Sleep(time.Millisecond)
700 }
701 t.Fatal("queued user turn did not win goal-round admission")
702 }
703
704 type restoredGoalRunner struct {
705 mu sync.Mutex
706 sources []tool.GoalSource
707 done chan struct{}
708 }
709
710 func (r *restoredGoalRunner) Run(ctx context.Context, _ string) error {
711 binding, ok := tool.GoalLifecycleFromContext(ctx)
712 if !ok {
713 return context.Canceled
714 }
715 r.mu.Lock()
716 r.sources = append(r.sources, binding.Authority.Source)
717 call := len(r.sources)
718 r.mu.Unlock()
719 view, err := binding.Owner.GetGoal(ctx)
720 if err != nil || view == nil {
721 return err
722 }
723 switch call {
724 case 1:
725 if view.Phase != goaldomain.PhaseActive || view.Activation != goaldomain.ActivationDisarmed {
726 return errors.New("restored goal was not active/disarmed")
727 }
728 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionResume}, binding.Authority)
729 return err
730 case 2:
731 return nil
732 case 3:
733 _, err = binding.Owner.UpdateGoal(ctx, tool.GoalUpdateRequest{Ref: view.Ref(), Action: tool.GoalActionComplete}, binding.Authority)
734 close(r.done)
735 return err
736 default:
737 return errors.New("unexpected extra goal round")
738 }
739 }
740
741 func TestColdRestoredGoalCanResumeFromNaturalUserRequestAndContinue(t *testing.T) {
742 root := t.TempDir()
743 persistence := session.NewFilesystemPersistence(root)
744 seedService, err := session.NewService("desktop", persistence)
745 if err != nil {
746 t.Fatal(err)
747 }
748 t.Cleanup(func() { _ = seedService.CloseAll(context.Background()) })
749 seedRuntime, err := seedService.Create(t.Context(), session.CreateOptions{SessionID: "restored-goal"})
750 if err != nil {
751 t.Fatal(err)
752 }
753 machine := goaldomain.NewMachine(nil, func() string { return "restored-goal-id" })
754 if _, err := machine.Create(goaldomain.CreateRequest{Objective: "finish the restored target"}); err != nil {
755 t.Fatal(err)
756 }
757 payload, err := machine.Encode()
758 if err != nil {
759 t.Fatal(err)
760 }
761 if _, err := seedRuntime.Session().Append(t.Context(), session.Batch{OperationID: "seed-goal", Events: []session.Event{{Kind: "goal/state", Payload: payload}}}); err != nil {
762 t.Fatal(err)
763 }
764 if err := seedService.Close(t.Context(), seedRuntime.Ref()); err != nil {
765 t.Fatal(err)
766 }
767
768 service, err := session.NewService("desktop", persistence)
769 if err != nil {
770 t.Fatal(err)
771 }
772 t.Cleanup(func() { _ = service.CloseAll(context.Background()) })
773 binding, err := service.Open(t.Context(), session.SessionRef{HostID: "desktop", SessionID: "restored-goal"})
774 if err != nil {
775 t.Fatal(err)
776 }
777 runtime := binding.Runtime()
778 runner := &restoredGoalRunner{done: make(chan struct{})}
779 exec := agent.New(nil, tool.NewRegistry(), agent.NewSession("system"), agent.Options{}, event.Discard)
780 c := newOwnedTestController(t, Options{Runner: runner, Executor: exec, Sink: event.Discard, SessionService: service, SessionRuntime: runtime, ExclusiveSession: true})
781 cleanupGoalDriverController(t, c)
782 t.Cleanup(func() { _ = binding.Release(context.Background()) })
783 c.Send("继续把这个目标做完")
784 select {
785 case <-runner.done:
786 case <-time.After(5 * time.Second):
787 t.Fatal("restored goal did not resume and continue")
788 }
789 view, err := c.goalLifecycleView()
790 if err != nil || view == nil || view.Phase != goaldomain.PhaseComplete || view.RoundsStarted != 2 {
791 t.Fatalf("restored goal view = %+v, err = %v", view, err)
792 }
793 runner.mu.Lock()
794 sources := append([]tool.GoalSource(nil), runner.sources...)
795 runner.mu.Unlock()
796 if len(sources) != 3 || sources[0] != tool.GoalSourceDirectHuman || sources[1] != tool.GoalSourceGoalRound || sources[2] != tool.GoalSourceGoalRound {
797 t.Fatalf("restored goal authorities = %v", sources)
798 }
799 }
800
800 lines GO