返回 DeepSeek-Reasonix
skill_watch_service_test.go
根目录 / desktop / skill_watch_service_test.go
1 package main
2
3 import (
4 "context"
5 "io"
6 "testing"
7
8 "reasonix/internal/skill/skillwatch"
9 )
10
11 // The host owns one watcher for its whole lifetime: a second service would be a
12 // second helper process, and a replacement would strand the subscriptions the
13 // first one holds. An existing service must be reused, never re-created.
14 func TestSharedSkillWatchServiceIsReusedPerApp(t *testing.T) {
15 app := NewApp()
16 seeded := &skillwatch.Service{}
17 app.skillWatch = seeded
18 for range 3 {
19 if got := app.sharedSkillWatchService(); got != seeded {
20 t.Fatal("an existing host skill watch service must be reused, not replaced")
21 }
22 }
23 }
24
25 // The desktop must apply the same production-binary gate as boot. A real
26 // service created here would spawn a helper through os.Executable(), and in a
27 // package test binary that helper is this test binary — it would re-run the
28 // suite in a child process.
29 func TestSharedSkillWatchServiceIsNilInTestBinaries(t *testing.T) {
30 if service := NewApp().sharedSkillWatchService(); service != nil {
31 _ = service.Close()
32 t.Fatal("a package test binary must not create a skill watch service")
33 }
34 }
35
36 // The host watcher is closed only here: boot.Build deliberately leaves a
37 // caller-owned service alone, so a missed close would leave the helper process
38 // behind after exit. ScanOnly keeps this process-free.
39 func TestCloseSharedSkillWatchServiceClosesTheWatcher(t *testing.T) {
40 app := NewApp()
41 service := skillwatch.NewService(skillwatch.Options{ScanOnly: true, Stderr: io.Discard})
42 sub := service.Subscribe(t.TempDir(), 1,
43 func(context.Context, string, int) ([]string, bool) { return nil, true },
44 func(context.Context, string, int) ([32]byte, int, bool) { return [32]byte{}, 0, true },
45 func(string) {})
46 t.Cleanup(sub.Release)
47 if service.Diagnostics().LogicalSubscriptions != 1 {
48 t.Fatal("fixture: the subscription did not register")
49 }
50
51 app.skillWatch = service
52 app.closeSharedSkillWatchService()
53 if service.Diagnostics().LogicalSubscriptions != 0 {
54 t.Fatal("the host watcher must be closed")
55 }
56 }
57
58 // A rebuild that races shutdown must not re-create the watcher: nothing runs
59 // the shutdown step again, so a fresh service would leave its helper behind.
60 func TestSharedSkillWatchServiceIsNotRecreatedAfterClose(t *testing.T) {
61 app := NewApp()
62 service := skillwatch.NewService(skillwatch.Options{ScanOnly: true, Stderr: io.Discard})
63 app.skillWatch = service
64 app.closeSharedSkillWatchService()
65 if got := app.sharedSkillWatchService(); got != service {
66 t.Fatal("a build after shutdown must get the closed host service, not a new one")
67 }
68 sub := service.Subscribe(t.TempDir(), 1,
69 func(context.Context, string, int) ([]string, bool) { return nil, true },
70 func(context.Context, string, int) ([32]byte, int, bool) { return [32]byte{}, 0, true },
71 func(string) {})
72 t.Cleanup(sub.Release)
73 if service.Diagnostics().LogicalSubscriptions != 0 {
74 t.Fatal("a closed host service must hand out dead subscriptions")
75 }
76 app.closeSharedSkillWatchService()
77 }
78
79 // shutdownStatus carries no step names, but the coordinator records them in
80 // finished, which a package test can read. This fails if the step is dropped or
81 // renamed, which is the wiring half of the shutdown path.
82 func TestShutdownRunsTheSkillWatchStep(t *testing.T) {
83 app := NewApp()
84 app.shutdown(context.Background())
85 state := app.shutdownState()
86 state.mu.Lock()
87 ran := state.finished["skill-watch-service"]
88 state.mu.Unlock()
89 if !ran {
90 t.Fatal("shutdown did not run the skill-watch step")
91 }
92 }
93
93 lines GO