api_storage_test.go

  1package plugin
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"testing"
  8
  9	lua "github.com/yuin/gopher-lua"
 10)
 11
 12func TestLuaStoreRoundTrip(t *testing.T) {
 13	setTestHome(t)
 14
 15	m := newTestManager()
 16	defer m.Close()
 17	m.currentPlugin = "test_plugin"
 18
 19	err := m.state.DoString(`
 20		local matcha = require("matcha")
 21		matcha.store_set("token", "abc123")
 22		result = matcha.store_get("token")
 23	`)
 24	if err != nil {
 25		t.Fatal(err)
 26	}
 27
 28	if got := m.state.GetGlobal("result"); got.String() != "abc123" {
 29		t.Fatalf("expected abc123, got %q", got.String())
 30	}
 31}
 32
 33func TestLuaStoreSetWithoutPluginContext(t *testing.T) {
 34	setTestHome(t)
 35
 36	m := newTestManager()
 37	defer m.Close()
 38
 39	err := m.state.DoString(`
 40		local matcha = require("matcha")
 41		matcha.store_set("token", "abc123")
 42	`)
 43	if err == nil {
 44		t.Fatal("expected store_set to fail without plugin context")
 45	}
 46	if !strings.Contains(err.Error(), "no plugin context") {
 47		t.Fatalf("expected plugin context error, got %v", err)
 48	}
 49}
 50
 51// store_delete is intentionally a silent no-op outside a plugin context to
 52// match store_get's read-side behavior. Only store_set raises in that case.
 53func TestLuaStoreDeleteWithoutPluginContextIsNoOp(t *testing.T) {
 54	setTestHome(t)
 55
 56	m := newTestManager()
 57	defer m.Close()
 58
 59	err := m.state.DoString(`
 60		local matcha = require("matcha")
 61		matcha.store_delete("token")
 62	`)
 63	if err != nil {
 64		t.Fatalf("expected store_delete to be silent without plugin context, got %v", err)
 65	}
 66}
 67
 68func TestLuaStorePluginsAreIsolated(t *testing.T) {
 69	setTestHome(t)
 70
 71	m := newTestManager()
 72	defer m.Close()
 73
 74	pluginA := writePlugin(t, t.TempDir(), "a.lua", `
 75		local matcha = require("matcha")
 76		matcha.store_set("shared", "a")
 77	`)
 78	pluginB := writePlugin(t, t.TempDir(), "b.lua", `
 79		local matcha = require("matcha")
 80		matcha.store_set("shared", "b")
 81	`)
 82
 83	m.loadPlugin("plugin_a", pluginA)
 84	m.loadPlugin("plugin_b", pluginB)
 85
 86	storeA, err := newPluginStore("plugin_a")
 87	if err != nil {
 88		t.Fatal(err)
 89	}
 90	storeB, err := newPluginStore("plugin_b")
 91	if err != nil {
 92		t.Fatal(err)
 93	}
 94
 95	gotA, ok := storeA.Get("shared")
 96	if !ok {
 97		t.Fatal("expected plugin_a key")
 98	}
 99	gotB, ok := storeB.Get("shared")
100	if !ok {
101		t.Fatal("expected plugin_b key")
102	}
103	if gotA != "a" {
104		t.Fatalf("expected plugin_a value a, got %q", gotA)
105	}
106	if gotB != "b" {
107		t.Fatalf("expected plugin_b value b, got %q", gotB)
108	}
109}
110
111func TestLuaStoreHookUsesRegisteredPluginContext(t *testing.T) {
112	setTestHome(t)
113
114	m := newTestManager()
115	defer m.Close()
116
117	pluginA := writePlugin(t, t.TempDir(), "a.lua", `
118		local matcha = require("matcha")
119		matcha.on("startup", function()
120			matcha.store_set("hook", "a")
121		end)
122	`)
123	pluginB := writePlugin(t, t.TempDir(), "b.lua", `
124		local matcha = require("matcha")
125		matcha.on("startup", function()
126			matcha.store_set("hook", "b")
127		end)
128	`)
129
130	m.loadPlugin("plugin_a", pluginA)
131	m.loadPlugin("plugin_b", pluginB)
132	m.CallHook(HookStartup)
133
134	assertStoredValue(t, "plugin_a", "hook", "a")
135	assertStoredValue(t, "plugin_b", "hook", "b")
136}
137
138func TestLuaStoreKeyBindingUsesRegisteredPluginContext(t *testing.T) {
139	setTestHome(t)
140
141	m := newTestManager()
142	defer m.Close()
143
144	pluginA := writePlugin(t, t.TempDir(), "a.lua", `
145		local matcha = require("matcha")
146		matcha.bind_key("ctrl+a", "inbox", "A", function()
147			matcha.store_set("binding", "a")
148		end)
149	`)
150	pluginB := writePlugin(t, t.TempDir(), "b.lua", `
151		local matcha = require("matcha")
152		matcha.bind_key("ctrl+b", "inbox", "B", function()
153			matcha.store_set("binding", "b")
154		end)
155	`)
156
157	m.loadPlugin("plugin_a", pluginA)
158	m.loadPlugin("plugin_b", pluginB)
159
160	bindings := m.Bindings(StatusInbox)
161	if len(bindings) != 2 {
162		t.Fatalf("expected 2 bindings, got %d", len(bindings))
163	}
164	for _, binding := range bindings {
165		m.CallKeyBinding(binding)
166	}
167
168	assertStoredValue(t, "plugin_a", "binding", "a")
169	assertStoredValue(t, "plugin_b", "binding", "b")
170}
171
172func TestLuaStoreKeysAndDelete(t *testing.T) {
173	setTestHome(t)
174
175	m := newTestManager()
176	defer m.Close()
177	m.currentPlugin = "test_plugin"
178
179	err := m.state.DoString(`
180		local matcha = require("matcha")
181		matcha.store_set("a", "1")
182		matcha.store_set("b", "2")
183		matcha.store_delete("a")
184		keys = matcha.store_keys()
185		deleted = matcha.store_get("a")
186	`)
187	if err != nil {
188		t.Fatal(err)
189	}
190
191	if got := m.state.GetGlobal("deleted"); got != lua.LNil {
192		t.Fatalf("expected deleted key to be nil, got %v", got)
193	}
194
195	keys, ok := m.state.GetGlobal("keys").(*lua.LTable)
196	if !ok {
197		t.Fatalf("expected keys table")
198	}
199	if keys.Len() != 1 {
200		t.Fatalf("expected 1 key, got %d", keys.Len())
201	}
202	if got := keys.RawGetInt(1); got.String() != "b" {
203		t.Fatalf("expected remaining key b, got %q", got.String())
204	}
205}
206
207func writePlugin(t *testing.T, dir, name, body string) string {
208	t.Helper()
209
210	path := filepath.Join(dir, name)
211	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
212		t.Fatal(err)
213	}
214	return path
215}
216
217func assertStoredValue(t *testing.T, pluginName, key, want string) {
218	t.Helper()
219
220	store, err := newPluginStore(pluginName)
221	if err != nil {
222		t.Fatal(err)
223	}
224	got, ok := store.Get(key)
225	if !ok {
226		t.Fatalf("expected %s key %q", pluginName, key)
227	}
228	if got != want {
229		t.Fatalf("expected %s key %q to be %q, got %q", pluginName, key, want, got)
230	}
231}