| 1 | package boot |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "io" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/skill/skillwatch" |
| 9 | ) |
| 10 | |
| 11 | // A package test binary must never spawn a watcher helper: one per test would |
| 12 | // leave stray reasonix-desktop.exe processes behind. The host constructor is |
| 13 | // what the desktop calls, so it has to apply the same gate as Build. |
| 14 | func TestHostSkillWatchServiceIsDisabledInTestBinaries(t *testing.T) { |
| 15 | if !watchSkillsEnabled() { |
| 16 | if service := NewHostSkillWatchService(io.Discard); service != nil { |
| 17 | _ = service.Close() |
| 18 | t.Fatal("host skill watch service was created in a package test binary") |
| 19 | } |
| 20 | return |
| 21 | } |
| 22 | // A production-named binary (an installed desktop service) does own one. |
| 23 | service := NewHostSkillWatchService(io.Discard) |
| 24 | if service == nil { |
| 25 | t.Fatal("host skill watch service is nil in a production binary") |
| 26 | } |
| 27 | if err := service.Close(); err != nil { |
| 28 | t.Fatalf("close host skill watch service: %v", err) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // subscribeScanOnly adds one subscription so the counters can show whether a |
| 33 | // close happened. ScanOnly keeps this process-free, which a real helper would |
| 34 | // not: on Windows the helper is os.Executable(), and here that is this test |
| 35 | // binary — the child would re-run the suite. |
| 36 | func subscribeScanOnly(t *testing.T, service *skillwatch.Service) { |
| 37 | t.Helper() |
| 38 | sub := service.Subscribe(t.TempDir(), 1, |
| 39 | func(context.Context, string, int) ([]string, bool) { return nil, true }, |
| 40 | func(context.Context, string, int) ([32]byte, int, bool) { return [32]byte{}, 0, true }, |
| 41 | func(string) {}) |
| 42 | t.Cleanup(sub.Release) |
| 43 | } |
| 44 | |
| 45 | // A build must not close a caller-owned service: every other controller on the |
| 46 | // host is still subscribed to it, and closing it tears down the helper they all |
| 47 | // watch through. |
| 48 | func TestCloseSkillsWithWatcherLeavesCallerOwnedServiceOpen(t *testing.T) { |
| 49 | host := skillwatch.NewService(skillwatch.Options{ScanOnly: true, Stderr: io.Discard}) |
| 50 | subscribeScanOnly(t, host) |
| 51 | if host.Diagnostics().LogicalSubscriptions != 1 { |
| 52 | t.Fatal("fixture: the subscription did not register") |
| 53 | } |
| 54 | ptr := host |
| 55 | closeSkillsWithWatcher(nil, nil, &host, true) |
| 56 | if host != nil { |
| 57 | t.Fatal("closeSkillsWithWatcher must clear the caller's pointer") |
| 58 | } |
| 59 | if ptr.Diagnostics().LogicalSubscriptions != 1 { |
| 60 | t.Fatal("a caller-owned service must outlive the build that subscribed to it") |
| 61 | } |
| 62 | if err := ptr.Close(); err != nil { |
| 63 | t.Fatalf("close caller-owned service: %v", err) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // The mirror case: a service the build created is closed with the build. |
| 68 | func TestCloseSkillsWithWatcherClosesABuildOwnedService(t *testing.T) { |
| 69 | owned := skillwatch.NewService(skillwatch.Options{ScanOnly: true, Stderr: io.Discard}) |
| 70 | subscribeScanOnly(t, owned) |
| 71 | if owned.Diagnostics().LogicalSubscriptions != 1 { |
| 72 | t.Fatal("fixture: the subscription did not register") |
| 73 | } |
| 74 | ptr := owned |
| 75 | closeSkillsWithWatcher(nil, nil, &owned, false) |
| 76 | if ptr.Diagnostics().LogicalSubscriptions != 0 { |
| 77 | t.Fatal("a build-owned service must be closed with the build") |
| 78 | } |
| 79 | } |
| 80 |