返回 DeepSeek-Reasonix
background_scope_test.go
根目录 / internal / control / background_scope_test.go
1 package control
2
3 import (
4 "context"
5 "io"
6 "testing"
7
8 "reasonix/internal/event"
9 "reasonix/internal/jobs"
10 )
11
12 func TestModelReplacementFencesCandidateAndRetiredCallbacks(t *testing.T) {
13 isolateControlConfigHome(t)
14 m := jobs.NewManager(event.Discard)
15 scope := jobs.NewSessionBackgroundScope(m, nil)
16 old := newOwnedTestController(t, Options{Jobs: m, BackgroundScope: scope})
17 old.SetToolApprovalMode(ToolApprovalYolo)
18 process := m.StartSessionProcess("", "bash", "gateway", func(ctx context.Context, _ io.Writer) (string, error) { <-ctx.Done(); return "", ctx.Err() })
19 if ModelReplacementBlocked(old) {
20 t.Fatal("independent process blocked model replacement")
21 }
22 _, finish, abort, err := ReserveBackgroundReplacement(old)
23 if err != nil {
24 t.Fatal(err)
25 }
26 defer abort()
27 if err := scope.Acquire(); err != nil {
28 t.Fatal(err)
29 }
30 next := newOwnedTestController(t, Options{Jobs: m, BackgroundScope: scope})
31 if err := finish(next); err != nil {
32 t.Fatal(err)
33 }
34 // Candidate setup can narrow its initial mode before migration restores the
35 // old mode. It must not cancel jobs still owned by the outgoing controller.
36 next.SetToolApprovalMode(ToolApprovalYolo)
37 next.SetToolApprovalMode(ToolApprovalAsk)
38 if len(m.Running()) != 1 {
39 t.Fatal("candidate initialization cancelled outgoing process")
40 }
41 if old.receivesBackgroundRuntimeEvents() || next.receivesBackgroundRuntimeEvents() {
42 t.Fatal("replacement published background callbacks before activation")
43 }
44 if err := ActivateControllerReplacement(old, next); err == nil {
45 t.Fatal("permission-changing replacement kept background process")
46 }
47 next.SetToolApprovalMode(old.ToolApprovalMode())
48 if err := ActivateControllerReplacement(old, next); err != nil {
49 t.Fatal(err)
50 }
51 if old.receivesBackgroundRuntimeEvents() || !next.receivesBackgroundRuntimeEvents() {
52 t.Fatal("background callbacks did not transfer to published runtime")
53 }
54 if len(m.Running()) != 1 || m.Running()[0].ID != process.ID {
55 t.Fatal("activation replaced gateway identity")
56 }
57 }
58
59 func TestModelApplicationCancelOnlySelectedRuntimeTasks(t *testing.T) {
60 isolateControlConfigHome(t)
61 m := jobs.NewManager(event.Discard)
62 scope := jobs.NewSessionBackgroundScope(m, nil)
63 c := newOwnedTestController(t, Options{Jobs: m, BackgroundScope: scope})
64 start := func(process bool, cancelled chan struct{}) *jobs.Job {
65 run := func(ctx context.Context, _ io.Writer) (string, error) {
66 <-ctx.Done()
67 close(cancelled)
68 return "", ctx.Err()
69 }
70 if process {
71 return m.StartSessionProcess("", "bash", "gateway", run)
72 }
73 return m.StartForSession("", "task", "dependent", run)
74 }
75 selectedExit, otherExit, processExit := make(chan struct{}), make(chan struct{}), make(chan struct{})
76 selected, other, process := start(false, selectedExit), start(false, otherExit), start(true, processExit)
77 if !ModelReplacementBlocked(c) {
78 t.Fatal("runtime task did not block replacement")
79 }
80 c.CancelModelApplicationBlockers([]string{selected.ID, process.ID, "missing"})
81 <-selectedExit
82 for _, exit := range []chan struct{}{otherExit, processExit} {
83 select {
84 case <-exit:
85 t.Fatal("unselected or session process cancelled")
86 default:
87 }
88 }
89 if len(c.ModelReplacementJobs()) == 0 {
90 t.Fatalf("remaining task %s lost", other.ID)
91 }
92 }
93
93 lines GO