diff options
| author | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
|---|---|---|
| committer | spiperac <[email protected]> | 2026-06-15 06:42:13 +0200 |
| commit | 0a477fd288d064d265395c88dbd0f261b723804e (patch) | |
| tree | 9b129f8f41c907285b89ed8385679934bb59c14f /internal/store/plugin_test.go | |
Initial release
Diffstat (limited to 'internal/store/plugin_test.go')
| -rw-r--r-- | internal/store/plugin_test.go | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/internal/store/plugin_test.go b/internal/store/plugin_test.go new file mode 100644 index 0000000..b640628 --- /dev/null +++ b/internal/store/plugin_test.go @@ -0,0 +1,368 @@ +package store_test + +import ( + "fmt" + "sync" + "testing" + + "github.com/shiv/internal/events" + "github.com/shiv/internal/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ── RegisterPlugin ──────────────────────────────────────────────────────────── + +func TestRegisterPlugin_NewPlugin_DefaultsToEnabled(t *testing.T) { + st := newTestStore(t) + + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + + entries, err := st.AllPlugins() + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, "test.lua", entries[0].Name) + assert.Equal(t, "/plugins/test.lua", entries[0].Path) + assert.True(t, entries[0].Enabled) +} + +func TestRegisterPlugin_ExistingPlugin_PreservesEnabledState(t *testing.T) { + st := newTestStore(t) + + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + + // Re-register with a new path — enabled state must be preserved. + require.NoError(t, st.RegisterPlugin("test.lua", "/new/path/test.lua")) + + entries, err := st.AllPlugins() + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, "/new/path/test.lua", entries[0].Path) + assert.False(t, entries[0].Enabled, "enabled state must survive re-registration") +} + +func TestRegisterPlugin_NotifiesPluginEntries(t *testing.T) { + st := newTestStore(t) + + // Drain any startup notifications. + for len(st.PluginEntries) > 0 { + <-st.PluginEntries + } + + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + + select { + case <-st.PluginEntries: + default: + t.Fatal("PluginEntries channel was not notified after RegisterPlugin") + } +} + +func TestRegisterPlugin_MultiplePlugins(t *testing.T) { + st := newTestStore(t) + + require.NoError(t, st.RegisterPlugin("a.lua", "/plugins/a.lua")) + require.NoError(t, st.RegisterPlugin("b.lua", "/plugins/b.lua")) + require.NoError(t, st.RegisterPlugin("c.lua", "/plugins/c.lua")) + + entries, err := st.AllPlugins() + require.NoError(t, err) + assert.Len(t, entries, 3) +} + +// ── PluginEnabled ───────────────────────────────────────────────────────────── + +func TestPluginEnabled_UnknownPlugin_ReturnsTrue(t *testing.T) { + st := newTestStore(t) + + enabled, err := st.PluginEnabled("nonexistent.lua") + // Error is expected (no row) but the safe default is true. + assert.True(t, enabled) + _ = err +} + +func TestPluginEnabled_NewPlugin_ReturnsTrue(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + + enabled, err := st.PluginEnabled("test.lua") + require.NoError(t, err) + assert.True(t, enabled) +} + +func TestPluginEnabled_AfterDisable_ReturnsFalse(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + + enabled, err := st.PluginEnabled("test.lua") + require.NoError(t, err) + assert.False(t, enabled) +} + +func TestPluginEnabled_AfterReEnable_ReturnsTrue(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/plugins/test.lua")) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: true}) + + enabled, err := st.PluginEnabled("test.lua") + require.NoError(t, err) + assert.True(t, enabled) +} + +// ── AllPlugins ──────────────────────────────────────────────────────────────── + +func TestAllPlugins_Empty(t *testing.T) { + st := newTestStore(t) + entries, err := st.AllPlugins() + require.NoError(t, err) + assert.Empty(t, entries) +} + +func TestAllPlugins_OrderedByNameAsc(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("zebra.lua", "/z.lua")) + require.NoError(t, st.RegisterPlugin("alpha.lua", "/a.lua")) + require.NoError(t, st.RegisterPlugin("mango.lua", "/m.lua")) + + entries, err := st.AllPlugins() + require.NoError(t, err) + require.Len(t, entries, 3) + assert.Equal(t, "alpha.lua", entries[0].Name) + assert.Equal(t, "mango.lua", entries[1].Name) + assert.Equal(t, "zebra.lua", entries[2].Name) +} + +func TestAllPlugins_EnabledFieldReflectsState(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("on.lua", "/on.lua")) + require.NoError(t, st.RegisterPlugin("off.lua", "/off.lua")) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "off.lua", Enabled: false}) + + entries, err := st.AllPlugins() + require.NoError(t, err) + require.Len(t, entries, 2) + + byName := make(map[string]store.PluginEntry) + for _, e := range entries { + byName[e.Name] = e + } + assert.True(t, byName["on.lua"].Enabled) + assert.False(t, byName["off.lua"].Enabled) +} + +// ── ScanPlugins ─────────────────────────────────────────────────────────────── + +func TestScanPlugins_RemovesStaleRecords(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("keep.lua", "/keep.lua")) + require.NoError(t, st.RegisterPlugin("gone.lua", "/gone.lua")) + + require.NoError(t, st.ScanPlugins([]string{"keep.lua"})) + + entries, err := st.AllPlugins() + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, "keep.lua", entries[0].Name) +} + +func TestScanPlugins_EmptyActive_RemovesAll(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("a.lua", "/a.lua")) + require.NoError(t, st.RegisterPlugin("b.lua", "/b.lua")) + + require.NoError(t, st.ScanPlugins([]string{})) + + entries, err := st.AllPlugins() + require.NoError(t, err) + assert.Empty(t, entries) +} + +func TestScanPlugins_AllPresent_NothingRemoved(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("a.lua", "/a.lua")) + require.NoError(t, st.RegisterPlugin("b.lua", "/b.lua")) + + require.NoError(t, st.ScanPlugins([]string{"a.lua", "b.lua"})) + + entries, err := st.AllPlugins() + require.NoError(t, err) + assert.Len(t, entries, 2) +} + +func TestScanPlugins_EmptyStore_NoPanic(t *testing.T) { + st := newTestStore(t) + assert.NoError(t, st.ScanPlugins([]string{"a.lua"})) +} + +func TestScanPlugins_NotifiesPluginEntries(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("gone.lua", "/gone.lua")) + + for len(st.PluginEntries) > 0 { + <-st.PluginEntries + } + + require.NoError(t, st.ScanPlugins([]string{})) + + select { + case <-st.PluginEntries: + default: + t.Fatal("PluginEntries channel was not notified after ScanPlugins") + } +} + +// ── PluginLogs ──────────────────────────────────────────────────────────────── + +func TestPluginLogs_UnknownPlugin_ReturnsNil(t *testing.T) { + st := newTestStore(t) + assert.Nil(t, st.PluginLogs("nonexistent.lua")) +} + +func TestPluginLogs_ReturnsLinesInOrder(t *testing.T) { + st := newTestStore(t) + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "line 1"}) + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "line 2"}) + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "line 3"}) + + lines := st.PluginLogs("test.lua") + require.Len(t, lines, 3) + assert.Equal(t, "line 1", lines[0]) + assert.Equal(t, "line 2", lines[1]) + assert.Equal(t, "line 3", lines[2]) +} + +func TestPluginLogs_ReturnsACopy(t *testing.T) { + st := newTestStore(t) + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "original"}) + + lines := st.PluginLogs("test.lua") + lines[0] = "mutated" + + // Original must be unchanged. + assert.Equal(t, "original", st.PluginLogs("test.lua")[0]) +} + +func TestPluginLogs_IsolatedPerPlugin(t *testing.T) { + st := newTestStore(t) + st.ObservePluginLog(events.PluginLogEvent{Name: "a.lua", Message: "from a"}) + st.ObservePluginLog(events.PluginLogEvent{Name: "b.lua", Message: "from b"}) + + assert.Equal(t, []string{"from a"}, st.PluginLogs("a.lua")) + assert.Equal(t, []string{"from b"}, st.PluginLogs("b.lua")) +} + +func TestPluginLogs_RingBuffer_CapsAtMaxLines(t *testing.T) { + st := newTestStore(t) + // Write more than maxPluginLogLines (500) entries. + for i := 0; i < 510; i++ { + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: fmt.Sprintf("line %d", i)}) + } + + lines := st.PluginLogs("test.lua") + assert.Len(t, lines, 500) + // The oldest 10 lines (0-9) were evicted; buffer starts at line 10. + assert.Equal(t, "line 10", lines[0]) + assert.Equal(t, "line 509", lines[499]) +} + +// ── ObservePluginLog ────────────────────────────────────────────────────────── + +func TestObservePluginLog_NotifiesPluginEntries(t *testing.T) { + st := newTestStore(t) + for len(st.PluginEntries) > 0 { + <-st.PluginEntries + } + + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "hello"}) + + select { + case <-st.PluginEntries: + default: + t.Fatal("PluginEntries channel was not notified after ObservePluginLog") + } +} + +func TestObservePluginLog_ChannelNonBlocking_WhenFull(t *testing.T) { + st := newTestStore(t) + // Fill the channel to capacity. + for i := 0; i < cap(st.PluginEntries); i++ { + st.PluginEntries <- struct{}{} + } + // Must not block even when channel is full. + assert.NotPanics(t, func() { + st.ObservePluginLog(events.PluginLogEvent{Name: "test.lua", Message: "msg"}) + }) +} + +func TestObservePluginLog_ConcurrentWrites_Safe(t *testing.T) { + st := newTestStore(t) + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func(i int) { + defer wg.Done() + st.ObservePluginLog(events.PluginLogEvent{ + Name: "test.lua", + Message: fmt.Sprintf("msg %d", i), + }) + }(i) + } + wg.Wait() + + lines := st.PluginLogs("test.lua") + assert.Len(t, lines, goroutines) +} + +// ── ObservePluginEnabled ────────────────────────────────────────────────────── + +func TestObservePluginEnabled_PersistsDisabled(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/test.lua")) + + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + + enabled, err := st.PluginEnabled("test.lua") + require.NoError(t, err) + assert.False(t, enabled) +} + +func TestObservePluginEnabled_PersistsEnabled(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/test.lua")) + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: true}) + + enabled, err := st.PluginEnabled("test.lua") + require.NoError(t, err) + assert.True(t, enabled) +} + +func TestObservePluginEnabled_NotifiesPluginEntries(t *testing.T) { + st := newTestStore(t) + require.NoError(t, st.RegisterPlugin("test.lua", "/test.lua")) + + for len(st.PluginEntries) > 0 { + <-st.PluginEntries + } + + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "test.lua", Enabled: false}) + + select { + case <-st.PluginEntries: + default: + t.Fatal("PluginEntries channel was not notified after ObservePluginEnabled") + } +} + +func TestObservePluginEnabled_UnknownPlugin_NoError(t *testing.T) { + st := newTestStore(t) + // Toggling a plugin not in the DB must not panic or crash. + assert.NotPanics(t, func() { + st.ObservePluginEnabled(events.SetPluginEnabledEvent{Name: "ghost.lua", Enabled: false}) + }) +} |
