1package plugin
2
3import (
4 "testing"
5
6 lua "github.com/yuin/gopher-lua"
7)
8
9// declareForTest mimics what loadPlugin does so we can drive declareSettings
10// without writing a real plugin file to disk.
11func declareForTest(t *testing.T, m *Manager, name, src string) {
12 t.Helper()
13 prev := m.currentPlugin
14 m.currentPlugin = name
15 defer func() { m.currentPlugin = prev }()
16 if err := m.state.DoString(src); err != nil {
17 t.Fatalf("Lua error loading %q: %v", name, err)
18 }
19}
20
21func TestPluginSettingsSchemaAndProxy(t *testing.T) {
22 m := NewManager()
23 defer m.Close()
24
25 src := `
26 local matcha = require("matcha")
27 cfg = matcha.settings({
28 enabled = {type = "boolean", default = true, label = "Enabled"},
29 limit = {type = "number", default = 5, label = "Limit"},
30 suffix = {type = "string", default = "!"},
31 })
32 `
33 declareForTest(t, m, "demo", src)
34
35 defs := m.Schema("demo")
36 if len(defs) != 3 {
37 t.Fatalf("expected 3 defs, got %d", len(defs))
38 }
39
40 v, ok := m.GetSettingValue("demo", "enabled")
41 if !ok || v.(bool) != true {
42 t.Fatalf("expected default enabled=true, got %v ok=%v", v, ok)
43 }
44 v, _ = m.GetSettingValue("demo", "limit")
45 if v.(float64) != 5 {
46 t.Fatalf("expected default limit=5, got %v", v)
47 }
48
49 // proxy table should reflect live values
50 check := `
51 assert(cfg.enabled == true, "enabled default")
52 assert(cfg.limit == 5, "limit default")
53 assert(cfg.suffix == "!", "suffix default")
54 `
55 if err := m.state.DoString(check); err != nil {
56 t.Fatalf("proxy check failed: %v", err)
57 }
58
59 // Override via SetSettingValue and re-check via proxy
60 if !m.SetSettingValue("demo", "enabled", false) {
61 t.Fatal("SetSettingValue rejected known key")
62 }
63 if !m.SetSettingValue("demo", "limit", float64(42)) {
64 t.Fatal("SetSettingValue rejected number")
65 }
66 if !m.SetSettingValue("demo", "suffix", "?!") {
67 t.Fatal("SetSettingValue rejected string")
68 }
69
70 check = `
71 assert(cfg.enabled == false, "enabled override")
72 assert(cfg.limit == 42, "limit override")
73 assert(cfg.suffix == "?!", "suffix override")
74 `
75 if err := m.state.DoString(check); err != nil {
76 t.Fatalf("proxy override check failed: %v", err)
77 }
78}
79
80func TestPluginSettingsLoadValues(t *testing.T) {
81 m := NewManager()
82 defer m.Close()
83
84 declareForTest(t, m, "demo", `
85 local matcha = require("matcha")
86 cfg = matcha.settings({
87 enabled = {type = "boolean", default = true},
88 limit = {type = "number", default = 5},
89 })
90 `)
91
92 // Simulate loading values from config (JSON unmarshals booleans as bool,
93 // numbers as float64).
94 m.LoadSettingValues(map[string]map[string]interface{}{
95 "demo": {
96 "enabled": false,
97 "limit": float64(99),
98 },
99 })
100
101 v, _ := m.GetSettingValue("demo", "enabled")
102 if v.(bool) != false {
103 t.Fatalf("expected enabled=false after load, got %v", v)
104 }
105 v, _ = m.GetSettingValue("demo", "limit")
106 if v.(float64) != 99 {
107 t.Fatalf("expected limit=99 after load, got %v", v)
108 }
109
110 // AllSettingValues should round-trip through JSON-friendly types.
111 all := m.AllSettingValues()
112 if all["demo"]["enabled"] != false || all["demo"]["limit"].(float64) != 99 {
113 t.Fatalf("AllSettingValues mismatch: %#v", all)
114 }
115}
116
117func TestPluginSettingsProxyReadOnly(t *testing.T) {
118 m := NewManager()
119 defer m.Close()
120
121 declareForTest(t, m, "demo", `
122 local matcha = require("matcha")
123 cfg = matcha.settings({enabled = {type = "boolean", default = true}})
124 `)
125
126 err := m.state.DoString(`cfg.enabled = false`)
127 if err == nil {
128 t.Fatal("expected error writing to read-only proxy")
129 }
130}
131
132func TestPluginSettingsRequiresLoadingPlugin(t *testing.T) {
133 m := NewManager()
134 defer m.Close()
135
136 // currentPlugin is empty (no loadPlugin in flight)
137 err := m.state.DoString(`require("matcha").settings({foo = {type = "boolean", default = false}})`)
138 if err == nil {
139 t.Fatal("expected error when calling matcha.settings outside plugin load")
140 }
141}
142
143func TestPluginSettingsCoercion(t *testing.T) {
144 m := NewManager()
145 defer m.Close()
146
147 declareForTest(t, m, "demo", `
148 local matcha = require("matcha")
149 matcha.settings({
150 flag = {type = "boolean", default = false},
151 n = {type = "number", default = 0},
152 })
153 `)
154
155 // Strings from a JSON file or older config should coerce.
156 m.LoadSettingValues(map[string]map[string]interface{}{
157 "demo": {
158 "flag": "true",
159 "n": "ignored",
160 },
161 })
162
163 v, _ := m.GetSettingValue("demo", "flag")
164 if v.(bool) != true {
165 t.Fatalf("expected flag=true after coercion, got %v", v)
166 }
167 v, _ = m.GetSettingValue("demo", "n")
168 if _, ok := v.(float64); !ok {
169 t.Fatalf("expected n coerced to float64, got %T %v", v, v)
170 }
171}
172
173// Verify that hook callbacks see live values (regression test for the closure
174// capture pattern).
175func TestPluginSettingsAccessibleFromHook(t *testing.T) {
176 m := NewManager()
177 defer m.Close()
178
179 declareForTest(t, m, "demo", `
180 local matcha = require("matcha")
181 local cfg = matcha.settings({n = {type = "number", default = 1}})
182 seen = nil
183 matcha.on("custom", function() seen = cfg.n end)
184 `)
185
186 // Fire hook before any override
187 m.CallHook("custom")
188 v := m.state.GetGlobal("seen")
189 if n, ok := v.(lua.LNumber); !ok || float64(n) != 1 {
190 t.Fatalf("expected seen=1, got %v", v)
191 }
192
193 // Override and fire again
194 m.SetSettingValue("demo", "n", float64(7))
195 m.CallHook("custom")
196 v = m.state.GetGlobal("seen")
197 if n, ok := v.(lua.LNumber); !ok || float64(n) != 7 {
198 t.Fatalf("expected seen=7 after override, got %v", v)
199 }
200}