Skip to content

Commit d2a963a

Browse files
committed
fix(opencode_server): ignore non-error stderr and detect early exit
- Do not treat informational stderr lines as startup failures; collect stderr output instead This should fix #355
1 parent 519e95f commit d2a963a

2 files changed

Lines changed: 105 additions & 18 deletions

File tree

lua/opencode/opencode_server.lua

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ end
207207
function OpencodeServer:spawn(opts)
208208
opts = opts or {}
209209
local log = require('opencode.log')
210+
local ready = false
211+
local startup_failed = false
212+
local startup_stderr = {}
210213

211214
local cmd = {
212215
config.opencode_executable,
@@ -225,17 +228,28 @@ function OpencodeServer:spawn(opts)
225228

226229
log.debug('spawn: starting opencode server with command: %s', vim.inspect(cmd))
227230

231+
local function fail_startup(err)
232+
if ready or startup_failed then
233+
return
234+
end
235+
236+
startup_failed = true
237+
self.spawn_promise:reject(err)
238+
safe_call(opts.on_error, err)
239+
end
240+
228241
self.mode = 'serve'
229242
self.job = vim.system(cmd, {
230243
cwd = opts.cwd,
231244
stdout = function(err, data)
232245
if err then
233-
safe_call(opts.on_error, err)
246+
fail_startup(err)
234247
return
235248
end
236249
if data then
237250
local url = data:match('opencode server listening on ([^%s]+)')
238-
if url then
251+
if url and not ready then
252+
ready = true
239253
self.url = url
240254
self.spawn_promise:resolve(self)
241255
safe_call(opts.on_ready, self.job, url)
@@ -245,23 +259,26 @@ function OpencodeServer:spawn(opts)
245259
end,
246260
stderr = function(err, data)
247261
if err then
248-
self.spawn_promise:reject(err)
249-
safe_call(opts.on_error, err)
262+
fail_startup(err)
250263
return
251264
end
252-
if data then
253-
-- Filter out INFO/WARN/DEBUG log lines (not actual errors)
254-
local log_level = data:match('^%s*(%u+)%s')
255-
if log_level and (log_level == 'INFO' or log_level == 'WARN' or log_level == 'DEBUG') then
256-
-- Ignore log lines, don't reject
257-
return
258-
end
259-
-- Only reject on actual errors
260-
self.spawn_promise:reject(data)
261-
safe_call(opts.on_error, data)
265+
if data and data ~= '' then
266+
table.insert(startup_stderr, data)
267+
log.debug('spawn: stderr output: %s', vim.inspect(data))
262268
end
263269
end,
264270
}, function(exit_opts)
271+
if not ready and not startup_failed then
272+
local stderr_output = table.concat(startup_stderr)
273+
local startup_error = stderr_output ~= '' and stderr_output
274+
or string.format(
275+
'Opencode server exited before reporting ready state (code=%s, signal=%s)',
276+
tostring(exit_opts and exit_opts.code),
277+
tostring(exit_opts and exit_opts.signal)
278+
)
279+
fail_startup(startup_error)
280+
end
281+
265282
-- Clear fields if not already cleared by shutdown()
266283
self.job = nil
267284
self.url = nil

tests/unit/opencode_server_spec.lua

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('opencode.opencode_server', function()
8989
assert.is_nil(server.handle)
9090
end)
9191

92-
it('calls on_error when stderr is triggered', function()
92+
it('calls on_error when stderr callback receives an error', function()
9393
local called = { on_error = false }
9494
local opts_captured = {}
9595
vim.system = function(cmd, opts)
@@ -124,20 +124,90 @@ describe('opencode.opencode_server', function()
124124
end,
125125
on_error = function(err)
126126
called.on_error = true
127-
assert.equals('some error', err)
127+
assert.equals('stream error', err)
128128
end,
129129
on_exit = function()
130130
called.on_exit = true
131131
end,
132132
})
133-
-- Simulate stderr after job is set
134-
server.job.stderr(nil, 'some error')
133+
-- Simulate stderr callback error after job is set
134+
server.job.stderr('stream error', nil)
135135
vim.wait(100, function()
136136
return called.on_error
137137
end)
138138
assert.is_true(called.on_error)
139139
end)
140140

141+
it('ignores stderr output before ready when stdout later reports the server URL', function()
142+
local called = { on_error = false }
143+
local server = OpencodeServer.new()
144+
145+
vim.system = function(cmd, opts)
146+
vim.schedule(function()
147+
opts.stderr(nil, 'Performing one time database migration, may take a few minutes...\n')
148+
opts.stderr(nil, 'sqlite-migration:100\n')
149+
opts.stdout(nil, 'opencode server listening on http://127.0.0.1:7777')
150+
end)
151+
152+
return { pid = 45, kill = function() end }
153+
end
154+
155+
local resolved
156+
server:spawn({
157+
cwd = '.',
158+
on_ready = function(_, url)
159+
resolved = url
160+
end,
161+
on_error = function()
162+
called.on_error = true
163+
end,
164+
on_exit = function() end,
165+
})
166+
167+
vim.wait(100, function()
168+
return resolved ~= nil
169+
end)
170+
171+
assert.equals('http://127.0.0.1:7777', resolved)
172+
assert.is_false(called.on_error)
173+
end)
174+
175+
it('rejects startup if the process exits before reporting the server URL', function()
176+
local called = { on_error = nil, on_exit = false }
177+
local server = OpencodeServer.new()
178+
179+
vim.system = function(cmd, opts, on_exit)
180+
vim.schedule(function()
181+
opts.stderr(nil, 'Database migration failed.\n')
182+
on_exit({ code = 1, signal = 0 })
183+
end)
184+
185+
return { pid = 46, kill = function() end }
186+
end
187+
188+
local promise = server:spawn({
189+
cwd = '.',
190+
on_ready = function()
191+
called.on_ready = true
192+
end,
193+
on_error = function(err)
194+
called.on_error = err
195+
end,
196+
on_exit = function()
197+
called.on_exit = true
198+
end,
199+
})
200+
201+
local ok, err = pcall(function()
202+
promise:wait(100)
203+
end)
204+
205+
assert.is_false(ok)
206+
assert.truthy(tostring(err):match('Database migration failed'))
207+
assert.truthy(tostring(called.on_error):match('Database migration failed'))
208+
assert.is_true(called.on_exit)
209+
end)
210+
141211
it('calls on_exit and clears fields when process exits', function()
142212
local called = { on_exit = false }
143213
local opts_captured = {}

0 commit comments

Comments
 (0)