Skip to content

Commit d36ede9

Browse files
committed
add tests for state.lua
1 parent 22b032b commit d36ede9

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

tests/test_state.lua

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
local MiniTest = require("mini.test")
2+
local eq = MiniTest.expect.equality
3+
local child = MiniTest.new_child_neovim()
4+
5+
local T = MiniTest.new_set({
6+
hooks = {
7+
pre_case = function()
8+
child.restart({ "-u", "scripts/minimal_init.lua" })
9+
child.lua([[
10+
_G.captured = {}
11+
local Observer = require('eca.observer')
12+
-- Clear any prior subscriptions by reloading the module (defensive)
13+
package.loaded['eca.observer'] = nil
14+
Observer = require('eca.observer')
15+
16+
-- Subscribe to capture all notifications
17+
Observer.subscribe('test-capture', function(message)
18+
table.insert(_G.captured, message)
19+
end)
20+
21+
-- Instantiate state singleton
22+
_G.State = require('eca.state').new()
23+
24+
-- Helper to filter captured messages by a predicate
25+
_G.filter_msgs = function(pred)
26+
local out = {}
27+
for _, m in ipairs(_G.captured) do
28+
if pred(m) then table.insert(out, m) end
29+
end
30+
return out
31+
end
32+
]])
33+
end,
34+
post_case = function()
35+
child.lua([[require('eca.observer').unsubscribe('test-capture')]])
36+
end,
37+
post_once = child.stop,
38+
},
39+
})
40+
41+
-- Ensure scheduled callbacks run (vim.schedule)
42+
local function flush(ms)
43+
vim.uv.sleep(ms or 50)
44+
-- Force at least one main loop iteration
45+
child.api.nvim_eval("1")
46+
end
47+
48+
T["singleton and defaults"] = MiniTest.new_set()
49+
50+
T["singleton and defaults"]["returns same instance"] = function()
51+
eq(child.lua_get("require('eca.state').new() == require('eca.state').new()"), true)
52+
end
53+
54+
T["singleton and defaults"]["has expected default values"] = function()
55+
eq(child.lua_get("_G.State.status.state"), "idle")
56+
eq(child.lua_get("_G.State.status.text"), "Idle")
57+
58+
eq(child.lua_get("vim.tbl_isempty(_G.State.config.behaviors.list)"), true)
59+
eq(child.lua_get("_G.State.config.behaviors.default"), vim.NIL)
60+
eq(child.lua_get("_G.State.config.behaviors.selected"), vim.NIL)
61+
62+
eq(child.lua_get("vim.tbl_isempty(_G.State.config.models.list)"), true)
63+
eq(child.lua_get("_G.State.config.models.default"), vim.NIL)
64+
eq(child.lua_get("_G.State.config.models.selected"), vim.NIL)
65+
66+
eq(child.lua_get("_G.State.config.welcome_message"), vim.NIL)
67+
68+
eq(child.lua_get("_G.State.usage.tokens.limit"), 0)
69+
eq(child.lua_get("_G.State.usage.tokens.session"), 0)
70+
eq(child.lua_get("_G.State.usage.costs.last_message"), "0.00")
71+
eq(child.lua_get("_G.State.usage.costs.session"), "0.00")
72+
73+
eq(child.lua_get("type(_G.State.tools)"), "table")
74+
end
75+
76+
T["updates via observer notifications"] = MiniTest.new_set()
77+
78+
T["updates via observer notifications"]["updates status on progress content"] = function()
79+
child.lua([[require('eca.observer').notify({
80+
method = 'chat/contentReceived',
81+
params = { content = { type = 'progress', state = 'responding', text = 'Respondendo...' } },
82+
})]])
83+
flush()
84+
85+
eq(child.lua_get("_G.State.status.state"), "responding")
86+
eq(child.lua_get("_G.State.status.text"), "Respondendo...")
87+
88+
-- Verify a state/updated notification was emitted for status
89+
local updates = child.lua_get([[ _G.filter_msgs(function(m)
90+
return type(m) == 'table' and m.type == 'state/updated' and type(m.content) == 'table' and m.content.status ~= nil
91+
end) ]])
92+
eq(#updates >= 1, true)
93+
end
94+
95+
T["updates via observer notifications"]["updates usage on usage content"] = function()
96+
child.lua([[require('eca.observer').notify({
97+
method = 'chat/contentReceived',
98+
params = { content = {
99+
type = 'usage',
100+
limit = { output = 1024 },
101+
sessionTokens = 256,
102+
lastMessageCost = '0.42',
103+
sessionCost = '3.14',
104+
} },
105+
})]])
106+
flush()
107+
108+
eq(child.lua_get("_G.State.usage.tokens.limit"), 1024)
109+
eq(child.lua_get("_G.State.usage.tokens.session"), 256)
110+
eq(child.lua_get("_G.State.usage.costs.last_message"), "0.42")
111+
eq(child.lua_get("_G.State.usage.costs.session"), "3.14")
112+
113+
local updates = child.lua_get([[ _G.filter_msgs(function(m)
114+
return type(m) == 'table' and m.type == 'state/updated' and type(m.content) == 'table' and m.content.usage ~= nil
115+
end) ]])
116+
eq(#updates >= 1, true)
117+
end
118+
119+
T["updates via observer notifications"]["updates config on config/updated"] = function()
120+
child.lua([[require('eca.observer').notify({
121+
method = 'config/updated',
122+
params = { chat = {
123+
behaviors = { 'agent', 'plan' },
124+
defaultBehavior = 'agent',
125+
selectBehavior = 'plan',
126+
models = { 'openai/gpt-5-mini', 'anthropic/claude' },
127+
defaultModel = 'openai/gpt-5-mini',
128+
selectModel = 'anthropic/claude',
129+
welcomeMessage = 'Bem-vindo ao ECA!',
130+
} },
131+
})]])
132+
flush()
133+
134+
eq(child.lua_get("_G.State.config.behaviors.list[1]"), "agent")
135+
eq(child.lua_get("_G.State.config.behaviors.list[2]"), "plan")
136+
eq(child.lua_get("_G.State.config.behaviors.default"), "agent")
137+
eq(child.lua_get("_G.State.config.behaviors.selected"), "plan")
138+
139+
eq(child.lua_get("_G.State.config.models.list[1]"), "openai/gpt-5-mini")
140+
eq(child.lua_get("_G.State.config.models.list[2]"), "anthropic/claude")
141+
eq(child.lua_get("_G.State.config.models.default"), "openai/gpt-5-mini")
142+
eq(child.lua_get("_G.State.config.models.selected"), "anthropic/claude")
143+
144+
eq(child.lua_get("_G.State.config.welcome_message"), "Bem-vindo ao ECA!")
145+
146+
local updates = child.lua_get([[ _G.filter_msgs(function(m)
147+
return type(m) == 'table' and m.type == 'state/updated' and type(m.content) == 'table' and m.content.config ~= nil
148+
end) ]])
149+
eq(#updates >= 1, true)
150+
end
151+
152+
T["updates via observer notifications"]["updates tools on tool/serverUpdated"] = function()
153+
-- Initial add
154+
child.lua([[require('eca.observer').notify({
155+
method = 'tool/serverUpdated',
156+
params = { name = 'server-1', type = 'mcp', status = 'connected' },
157+
})]])
158+
flush()
159+
160+
eq(child.lua_get("_G.State.tools['server-1'].name"), "server-1")
161+
eq(child.lua_get("_G.State.tools['server-1'].type"), "mcp")
162+
eq(child.lua_get("_G.State.tools['server-1'].status"), "connected")
163+
164+
-- Update only status, keep type
165+
child.lua([[require('eca.observer').notify({
166+
method = 'tool/serverUpdated',
167+
params = { name = 'server-1', status = 'disconnected' },
168+
})]])
169+
flush()
170+
171+
eq(child.lua_get("_G.State.tools['server-1'].type"), "mcp")
172+
eq(child.lua_get("_G.State.tools['server-1'].status"), "disconnected")
173+
174+
-- Invalid: missing name should be ignored (no errors, no new entries)
175+
local before = child.lua_get("vim.tbl_count(_G.State.tools)")
176+
child.lua([[require('eca.observer').notify({ method = 'tool/serverUpdated', params = { status = 'x' } })]])
177+
flush()
178+
local after = child.lua_get("vim.tbl_count(_G.State.tools)")
179+
eq(after, before)
180+
181+
local updates = child.lua_get([[ _G.filter_msgs(function(m)
182+
return type(m) == 'table' and m.type == 'state/updated' and type(m.content) == 'table' and m.content.tools ~= nil
183+
end) ]])
184+
eq(#updates >= 1, true)
185+
end
186+
187+
return T

0 commit comments

Comments
 (0)