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}