Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lua/opencode/server_job.lua
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,23 @@ function M.try_connect_to_custom_server(base_url, timeout, promise, custom_port,
log.warn('failed to connect to %s: %s', base_url, vim.inspect(err))
if config.server.spawn_command and custom_port and custom_url then
spawn_and_retry(base_url, custom_port, custom_url, promise, timeout)
elseif not config.server.auto_kill then
-- Server is externally managed (auto_kill=false). Retry connecting
-- instead of spawning a local server that would leak as an orphan.
log.debug('try_connect_to_custom_server: auto_kill=false, retrying instead of spawning local')
retry_connect(base_url, timeout, 5, function(url)
local existing_started_by_nvim = port_mapping.started_by_nvim(custom_port)
port_mapping.register(custom_port, vim.fn.getcwd(), existing_started_by_nvim, 'attach', url, nil)
state.jobs.set_server(opencode_server.from_custom(url, custom_port, 'attach'))
log.notify(
string.format('Connected to external server at %s on port %d.', base_url, custom_port),
vim.log.levels.INFO
)
promise:resolve(state.opencode_server)
end, function(retry_err)
log.error('try_connect_to_custom_server: exhausted retries for external server: %s', vim.inspect(retry_err))
promise:reject(string.format('Failed to connect to external server at %s after retries', base_url))
end)
else
M.spawn_local_server(promise, custom_port, custom_url)
end
Expand Down
132 changes: 132 additions & 0 deletions tests/unit/server_job_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -262,5 +262,137 @@ describe('server_job', function()
assert.equal(1, spawn_count)
assert.same(fake_local, result._value or result)
end)

it('retries and connects when auto_kill=false and health check eventually succeeds', function()
local original_auto_kill = config.values.server.auto_kill
local original_retry_delay = config.values.server.retry_delay
local original_defer_fn = vim.defer_fn

config.values.server.url = 'http://192.168.1.100'
config.values.server.port = 5555
config.values.server.spawn_command = nil
config.values.server.auto_kill = false
config.values.server.retry_delay = 0

-- Make vim.defer_fn fire immediately so retries don't block
vim.defer_fn = function(fn, _delay)
vim.schedule(fn)
end

local request_count = 0
curl.request = function(opts)
vim.schedule(function()
request_count = request_count + 1
if request_count <= 2 then
-- First two attempts fail (initial + first retry)
opts.callback({ status = 503, body = '{}' })
else
-- Third attempt succeeds
opts.callback({ status = 200, body = '{"ok":true}' })
end
end)
end

local registered_mode
port_mapping.register = function(_port, _dir, _started, mode)
registered_mode = mode
end

local result = server_job.ensure_server():wait()
assert.is_not_nil(result)
assert.equal('http://192.168.1.100:5555', result.url)
assert.equal(5555, result.port)
assert.equal('attach', registered_mode)
assert.is_true(request_count >= 3)

config.values.server.auto_kill = original_auto_kill
config.values.server.retry_delay = original_retry_delay
vim.defer_fn = original_defer_fn
end)

it('rejects after exhausting retries when auto_kill=false', function()
local original_auto_kill = config.values.server.auto_kill
local original_retry_delay = config.values.server.retry_delay
local original_defer_fn = vim.defer_fn

config.values.server.url = 'http://192.168.1.100'
config.values.server.port = 5555
config.values.server.spawn_command = nil
config.values.server.auto_kill = false
config.values.server.retry_delay = 0

vim.defer_fn = function(fn, _delay)
vim.schedule(fn)
end

-- All attempts fail
curl.request = function(opts)
vim.schedule(function()
opts.callback({ status = 503, body = '{}' })
end)
end

local ok, err = pcall(function()
server_job.ensure_server():wait()
end)

assert.is_false(ok)
assert.truthy(tostring(err):match('Failed to connect to external server'))

config.values.server.auto_kill = original_auto_kill
config.values.server.retry_delay = original_retry_delay
vim.defer_fn = original_defer_fn
end)

it('does not spawn local server when auto_kill=false', function()
local original_auto_kill = config.values.server.auto_kill
local original_retry_delay = config.values.server.retry_delay
local original_defer_fn = vim.defer_fn

config.values.server.url = 'http://192.168.1.100'
config.values.server.port = 5555
config.values.server.spawn_command = nil
config.values.server.auto_kill = false
config.values.server.retry_delay = 0

vim.defer_fn = function(fn, _delay)
vim.schedule(fn)
end

-- All attempts fail
curl.request = function(opts)
vim.schedule(function()
opts.callback({ status = 503, body = '{}' })
end)
end

local spawn_count = 0
opencode_server.new = function()
return {
url = 'http://127.0.0.1:8080',
port = nil,
is_running = function()
return spawn_count > 0
end,
spawn = function(self, opts)
spawn_count = spawn_count + 1
vim.schedule(function()
opts.on_ready({}, self.url)
end)
end,
shutdown = function() end,
}
end

pcall(function()
server_job.ensure_server():wait()
end)

assert.equal(0, spawn_count)

config.values.server.auto_kill = original_auto_kill
config.values.server.retry_delay = original_retry_delay
vim.defer_fn = original_defer_fn
end)
end)
end)