package app import ( "context" "errors" "log/slog" "os" "path/filepath" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/preferences" "git.stormux.org/storm/cthulhu/internal/tentacle" ) func managed_tentacle_fixture(t *testing.T, path string, contained bool) (*tentacleRuntime, *tentacleBindingRecorder) { t.Helper() bindings := &tentacleBindingRecorder{} runtime := newTentacleRuntime(slog.New(slog.DiscardHandler), bindings, "", "auto") runtime.registry = &tentacle.Registry{Path: path} runtime.requests = make(chan tentacleManagementCall, 16) if contained { executable, err := os.Executable() if err != nil { t.Fatal(err) } runtime.guardianProgram = []string{executable, "-test.run=^TestManagedTentacleGuardianPeer$", "--", "managed-guardian"} } if !contained { runtime.newManager = func() (*tentacle.Manager, error) { return tentacle.NewManager(tentacle.DefaultLimits(), nil) } } t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Error(err) } }) return runtime, bindings } func manage_tentacle(t *testing.T, runtime *tentacleRuntime, action, id string, entry *tentacle.Entry) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() if err := runtime.Manage(ctx, tentacle.ManagementRequest{Action: action, ID: id, Entry: entry}); err != nil { t.Fatalf("%s: %v", action, err) } } func probe_entry(t *testing.T, id, mode string) tentacle.Entry { t.Helper() executable, err := os.Executable() if err != nil { t.Fatal(err) } return tentacle.Entry{ID: id, Program: []string{executable, "-test.run=^TestTentacleReaderPeer$", "--", "reader-peer", filepath.Join(t.TempDir(), mode)}} } func wait_managed_state(t *testing.T, runtime *tentacleRuntime, id, state string) { t.Helper() waitTentacle(t, func() bool { for _, status := range runtime.Snapshot().Entries { if status.Entry.ID == id { return status.State == state } } return false }) } func TestManagedTentaclePersistenceLifecycleAndRecovery(t *testing.T) { path := filepath.Join(t.TempDir(), "tentacles.toml") runtime, bindings := managed_tentacle_fixture(t, path, false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } first := probe_entry(t, "first", "called") second := probe_entry(t, "second", "called") manage_tentacle(t, runtime, "add", first.ID, &first) manage_tentacle(t, runtime, "add", second.ID, &second) if len(bindings.TentacleBindings()) != 0 { t.Fatal("adding disabled entries launched them") } manage_tentacle(t, runtime, "enable", first.ID, nil) wait_managed_state(t, runtime, first.ID, "active") firstCommand := bindings.TentacleBindings()[0].Command manage_tentacle(t, runtime, "enable", first.ID, nil) if bindings.TentacleBindings()[0].Command != firstCommand { t.Fatal("enable of active entry restarted it") } manage_tentacle(t, runtime, "enable", second.ID, nil) wait_managed_state(t, runtime, second.ID, "active") manage_tentacle(t, runtime, "disable", first.ID, nil) wait_managed_state(t, runtime, first.ID, "disabled") waitTentacle(t, func() bool { bound := bindings.TentacleBindings() return len(bound) > 0 && bound[0].Command != firstCommand }) runtime.Invoke(context.Background(), firstCommand) if _, err := os.Stat(first.Program[len(first.Program)-1]); !os.IsNotExist(err) { t.Fatal("stale command invoked disabled child") } secondCommand := bindings.TentacleBindings()[0].Command manage_tentacle(t, runtime, "retry", second.ID, nil) wait_managed_state(t, runtime, second.ID, "active") if bindings.TentacleBindings()[0].Command == secondCommand { t.Fatal("retry reused old session identity") } loaded, err := (&tentacle.Registry{Path: path}).Load() if err != nil || len(loaded) != 2 || loaded[0].Enabled || !loaded[1].Enabled { t.Fatalf("persisted state: %v %v", loaded, err) } runtime.Disable() if err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "enable", ID: first.ID}); !errors.Is(err, tentacle.ErrDisabled) { t.Fatalf("emergency bypass: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Fatal(err) } restarted, _ := managed_tentacle_fixture(t, path, false) if err := restarted.Start(context.Background()); err != nil { t.Fatal(err) } wait_managed_state(t, restarted, second.ID, "active") wait_managed_state(t, restarted, first.ID, "disabled") manage_tentacle(t, restarted, "remove", first.ID, nil) manage_tentacle(t, restarted, "remove", second.ID, nil) loaded, err = (&tentacle.Registry{Path: path}).Load() if err != nil || len(loaded) != 0 { t.Fatalf("remove all: %v %v", loaded, err) } if _, err := os.Stat(second.Program[0]); err != nil { t.Fatal("removing configuration deleted the executable") } } func TestManagedTentacleFailureRetryAndSaveFailure(t *testing.T) { path := filepath.Join(t.TempDir(), "tentacles.toml") runtime, bindings := managed_tentacle_fixture(t, path, false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "crash") entry.Enabled = true manage_tentacle(t, runtime, "add", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") runtime.Invoke(context.Background(), bindings.TentacleBindings()[0].Command) wait_managed_state(t, runtime, entry.ID, "failed") if !runtime.Snapshot().Entries[0].Entry.Enabled { t.Fatal("failure changed desired enablement") } manage_tentacle(t, runtime, "retry", entry.ID, nil) wait_managed_state(t, runtime, entry.ID, "active") entry.Program[len(entry.Program)-1] = filepath.Join(t.TempDir(), "called") entry.Presentation = true manage_tentacle(t, runtime, "update", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") if !runtime.Snapshot().Entries[0].Entry.Presentation { t.Fatal("grant lost") } if err := os.WriteFile(path, []byte("version = 1\n"), 0600); err != nil { t.Fatal(err) } if err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "disable", ID: entry.ID}); err == nil { t.Fatal("overwrote external edits") } if runtime.Snapshot().Entries[0].State != "active" { t.Fatal("failed persistence changed runtime") } } func TestManagedTentacleEmptyAndBrokenRegistryDoNotLaunch(t *testing.T) { for _, broken := range []bool{false, true} { path := filepath.Join(t.TempDir(), "tentacles.toml") if broken { if err := os.WriteFile(path, []byte("invalid"), 0600); err != nil { t.Fatal(err) } } runtime, _ := managed_tentacle_fixture(t, path, false) runtime.newManager = func() (*tentacle.Manager, error) { t.Error("empty/broken registry initialized host") return nil, tentacle.ErrUnavailable } if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "called") err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "add", ID: entry.ID, Entry: &entry}) if (err != nil) != broken { t.Fatalf("broken=%v: %v", broken, err) } if len(runtime.sessions) != 0 { t.Fatal("launched unexpected child") } } } func TestPreferencesTentacleOperationsAreGlobalAndSeparate(t *testing.T) { runtime, _ := managed_tentacle_fixture(t, filepath.Join(t.TempDir(), "tentacles.toml"), false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil) worker.tentacles = runtime entry := probe_entry(t, "probe", "called") request := preferences.Request{Tentacle: &tentacle.ManagementRequest{Action: "add", ID: entry.ID, Entry: &entry}} worker.editApp = "Example" if response := worker.manage_tentacle(context.Background(), request); response.Accepted { t.Fatal("application preferences added a program") } worker.editApp = "" request.Save = true if response := worker.manage_tentacle(context.Background(), request); response.Accepted { t.Fatal("mixed settings request accepted") } request.Save = false if response := worker.manage_tentacle(context.Background(), request); !response.Accepted || response.Tentacles == nil || len(response.Tentacles.Entries) != 1 { t.Fatalf("add: %+v", response) } } func TestManagedTentacleGuardianPeer(t *testing.T) { if len(os.Args) < 2 || os.Args[len(os.Args)-1] != "managed-guardian" { return } if err := tentacle.RunSupervisor(context.Background()); err != nil { os.Exit(1) } os.Exit(0) } func TestManagedSupervisedTentacle(t *testing.T) { runtime, bindings := managed_tentacle_fixture(t, filepath.Join(t.TempDir(), "tentacles.toml"), true) readerGroup, err := os.ReadFile("/proc/self/cgroup") if err != nil { t.Fatal(err) } runtime.logger = slog.New(slog.NewTextHandler(os.Stderr, nil)) defer func() { t.Logf("final status: %+v", runtime.Snapshot()) }() executable, _ := os.Executable() runtime.guardianProgram = []string{executable, "-test.run=^TestManagedTentacleGuardianPeer$", "--", "managed-guardian"} if binary := os.Getenv("CTHULHU_TENTACLE_GUARDIAN_BINARY"); binary != "" { runtime.guardianProgram = []string{binary, "--tentacle-guardian"} } if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "called") entry.Enabled = true manage_tentacle(t, runtime, "add", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") runtime.Invoke(context.Background(), bindings.TentacleBindings()[0].Command) marker := entry.Program[len(entry.Program)-1] waitTentacle(t, func() bool { _, err := os.Stat(marker); return err == nil }) membership, err := os.ReadFile(marker) if err != nil { t.Fatal(err) } var aggregateGroup string if os.Getenv("CTHULHU_TENTACLE_CGROUP_PARENT") != "" { childGroup := filepath.Join("/sys/fs/cgroup", strings.TrimSpace(strings.TrimPrefix(string(membership), "0::"))) aggregateGroup = filepath.Dir(childGroup) for name, want := range map[string]string{"cpu.max": "25000 100000", "memory.max": "268435456", "pids.max": "64"} { value, err := os.ReadFile(filepath.Join(childGroup, name)) if err != nil || strings.TrimSpace(string(value)) != want { t.Fatalf("%s: %q %v", name, value, err) } } } afterGroup, err := os.ReadFile("/proc/self/cgroup") if err != nil || string(readerGroup) != string(afterGroup) { t.Fatal("management moved the reader") } manage_tentacle(t, runtime, "disable", entry.ID, nil) waitTentacle(t, func() bool { return len(bindings.TentacleBindings()) == 0 }) manage_tentacle(t, runtime, "enable", entry.ID, nil) wait_managed_state(t, runtime, entry.ID, "active") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Fatal(err) } if aggregateGroup != "" { waitTentacle(t, func() bool { _, err := os.Stat(aggregateGroup); return os.IsNotExist(err) }) } } func TestManagedTentacleCancelledStartupAndConcurrentDisable(t *testing.T) { for _, operation := range []string{"cancel", "disable", "remove"} { t.Run(operation, func(t *testing.T) { registryPath := filepath.Join(t.TempDir(), "tentacles.toml") owner, bindings := managed_tentacle_fixture(t, registryPath, false) if err := owner.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "stall") manage_tentacle(t, owner, "add", entry.ID, &entry) ctx, cancel := context.WithCancel(context.Background()) defer cancel() result := make(chan error, 1) go func() { result <- owner.Manage(ctx, tentacle.ManagementRequest{Action: "enable", ID: entry.ID}) }() wait_managed_state(t, owner, entry.ID, "starting") if operation == "cancel" { cancel() } else { manage_tentacle(t, owner, operation, entry.ID, nil) } select { case err := <-result: if err == nil { t.Fatal("cancelled startup reported success") } case <-time.After(5 * time.Second): t.Fatal("startup cancellation did not complete") } snapshot := owner.Snapshot() if operation == "remove" { if len(snapshot.Entries) != 0 { t.Fatal(snapshot) } } else if len(snapshot.Entries) != 1 || snapshot.Entries[0].Entry.Enabled { t.Fatal(snapshot) } if len(bindings.TentacleBindings()) != 0 { t.Fatal("cancelled startup published bindings") } disk, err := (&tentacle.Registry{Path: registryPath}).Load() if err != nil || len(disk) > 0 && disk[0].Enabled { t.Fatalf("cancelled startup remained enabled: %+v %v", disk, err) } }) } } func TestManagedTentacleEnableRestartsFailedSession(t *testing.T) { owner, bindings := managed_tentacle_fixture(t, filepath.Join(t.TempDir(), "tentacles.toml"), false) if err := owner.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "crash") entry.Enabled = true manage_tentacle(t, owner, "add", entry.ID, &entry) command := bindings.TentacleBindings()[0].Command owner.Invoke(context.Background(), command) wait_managed_state(t, owner, entry.ID, "failed") manage_tentacle(t, owner, "enable", entry.ID, nil) status := owner.Snapshot().Entries[0] if status.State != "active" || !status.Entry.Enabled || status.Message != "" { t.Fatalf("enable reported success without restarting: %+v", status) } if current := bindings.TentacleBindings(); len(current) != 1 || current[0].Command == command { t.Fatalf("enable did not publish a new session: %+v", current) } } type cancellingTentacleBindings struct { *tentacleBindingRecorder cancel context.CancelFunc } func (bindings *cancellingTentacleBindings) SetTentacleBindings(ctx context.Context, next []input.BindingOverride) error { err := bindings.tentacleBindingRecorder.SetTentacleBindings(ctx, next) if len(next) > 1 { // Cancellation arrives after native grabs succeed, before activation. bindings.cancel() } return err } func TestManagedTentacleCancellationDuringBindingsRollsBack(t *testing.T) { path := filepath.Join(t.TempDir(), "tentacles.toml") owner, bindings := managed_tentacle_fixture(t, path, false) ctx, cancel := context.WithCancel(context.Background()) defer cancel() owner.input = &cancellingTentacleBindings{tentacleBindingRecorder: bindings, cancel: cancel} if err := owner.Start(context.Background()); err != nil { t.Fatal(err) } first := probe_entry(t, "first", "called") first.Enabled = true manage_tentacle(t, owner, "add", first.ID, &first) firstCommand := bindings.TentacleBindings()[0].Command second := probe_entry(t, "second", "called") manage_tentacle(t, owner, "add", second.ID, &second) if err := owner.Manage(ctx, tentacle.ManagementRequest{Action: "enable", ID: second.ID}); err == nil { t.Fatal("startup cancelled during binding reported success") } status := owner.Snapshot().Entries if len(status) != 2 || status[0].State != "active" || status[1].Entry.Enabled { t.Fatalf("incorrect rollback state: %+v", status) } if current := bindings.TentacleBindings(); len(current) != 1 || current[0].Command != firstCommand { t.Fatalf("rollback did not preserve the first session's bindings: %+v", current) } disk, err := (&tentacle.Registry{Path: path}).Load() if err != nil || len(disk) != 2 || !disk[0].Enabled || disk[1].Enabled { t.Fatalf("incorrect persisted rollback: %+v %v", disk, err) } } // Updating an entry must preserve the user's enabled preference. func TestUpdatePreservesEnabledState(t *testing.T) { for _, enabled := range []bool{false, true} { current := tentacle.Entry{ID: "example", Program: []string{"/bin/true"}, Enabled: enabled, Version: "0.1.3"} replacement := tentacle.Entry{ID: "example", Program: []string{"/bin/true"}, Enabled: !enabled, Version: "0.1.4"} next, err := change_tentacle_entries( []tentacle.Entry{current}, tentacle.ManagementRequest{ Action: "update", ID: current.ID, Entry: &replacement, }, ) if err != nil { t.Fatal(err) } if len(next) != 1 || next[0].Enabled != enabled || next[0].Version != "0.1.4" { t.Fatalf("enabled=%v: unexpected result: %+v", enabled, next) } } }