// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "git.stormux.org/storm/cthulhu/internal/tentacle" "path/filepath" "strings" "testing" ) func TestManagedRepositoryDiscoveryRefreshRemoveAndRestart(t *testing.T) { registryPath := filepath.Join(t.TempDir(), "tentacles.toml") runtime, _ := managed_tentacle_fixture(t, registryPath, false) origin := "https://example.org/team/tentacles" repository := tentacle.Repository{URL: origin, ID: "example.tentacles", Name: "Example", Revision: strings.Repeat("a", 40), Tentacles: []tentacle.Manifest{{FormatVersion: 1, ID: "echo", Name: "Echo", Version: "0.1"}}} fail := false runtime.fetchRepository = func(context.Context, string) (tentacle.Repository, error) { if fail { return tentacle.Repository{}, errors.New("invalid metadata") } return repository, nil } runtime.newManager = func() (*tentacle.Manager, error) { t.Error("repository discovery started host") return nil, tentacle.ErrUnavailable } if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } manage := func(action string) error { return runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: action, URL: origin}) } if err := manage("repository-add"); err != nil { t.Fatal(err) } if len(runtime.Snapshot().Repositories) != 1 || len(runtime.Snapshot().Entries) != 0 { t.Fatal(runtime.Snapshot()) } if err := manage("repository-add"); err == nil { t.Fatal("duplicate accepted") } fail = true if err := manage("repository-refresh"); err == nil { t.Fatal("invalid refresh accepted") } if runtime.Snapshot().Repositories[0].Revision != repository.Revision { t.Fatal("lost last good catalog") } fail = false repository.Revision = strings.Repeat("b", 40) if err := manage("repository-refresh"); err != nil { t.Fatal(err) } repository.ID = "different" if err := manage("repository-refresh"); err == nil { t.Fatal("changed identity accepted") } disk := &tentacle.Registry{Path: registryPath} if _, err := disk.Load(); err != nil || len(disk.Repositories()) != 1 || disk.Repositories()[0].ID != "example.tentacles" || disk.Repositories()[0].Revision != strings.Repeat("b", 40) { t.Fatalf("persisted repository: %+v %v", disk.Repositories(), err) } // Cached catalogs load at startup without invoking any fetch function. restarted, _ := managed_tentacle_fixture(t, registryPath, false) restarted.fetchRepository = func(context.Context, string) (tentacle.Repository, error) { t.Error("startup fetched repository") return repository, nil } if err := restarted.Start(context.Background()); err != nil { t.Fatal(err) } waitTentacle(t, func() bool { return len(restarted.Snapshot().Repositories) == 1 }) if err := manage("repository-remove"); err != nil { t.Fatal(err) } if len(runtime.Snapshot().Repositories) != 0 { t.Fatal("source not removed") } } func TestRepositoryRemovalRetainsInstalledEntries(t *testing.T) { registryPath := filepath.Join(t.TempDir(), "tentacles.toml") origin := "https://example.org/team/tentacles" registry := &tentacle.Registry{Path: registryPath} if _, err := registry.Load(); err != nil { t.Fatal(err) } entry := probe_entry(t, "installed", "called") entry.Repository = origin entry.RepositoryID = "example.tentacles" entry.LocalID = "echo" repository := tentacle.Repository{URL: origin, ID: "example.tentacles", Name: "Example", Revision: strings.Repeat("a", 40)} if err := registry.SaveState([]tentacle.Entry{entry}, []tentacle.Repository{repository}); err != nil { t.Fatal(err) } runtime, _ := managed_tentacle_fixture(t, registryPath, false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } if err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "repository-remove", URL: origin}); err != nil { t.Fatal(err) } snapshot := runtime.Snapshot() if len(snapshot.Repositories) != 0 || len(snapshot.Entries) != 1 || snapshot.Entries[0].Entry.ID != entry.ID { t.Fatal(snapshot) } }