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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
- name: Generate coverage report
run: |
if [ -f "luacov.stats.out" ]; then
mise exec -- luacov
./.luarocks/bin/luacov

echo "Creating lcov.info from luacov.report.out"
{
Expand Down
18 changes: 9 additions & 9 deletions lua/claudecode/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ M.defaults = {
{ name = "Claude Haiku (Latest)", value = "haiku" },
{ name = "Default (account recommended)", value = "default" },
},
terminal = nil, -- Will be lazy-loaded to avoid circular dependency
-- Keep a minimal terminal config here instead of requiring claudecode.terminal
-- during config.apply(). Loading the terminal module pulls in the server/main
-- module graph and makes coverage-enabled config validation unexpectedly slow.
terminal = {
provider = "auto",
provider_opts = {
external_terminal_cmd = nil,
},
},
}

---Validates the provided configuration table.
Expand Down Expand Up @@ -191,14 +199,6 @@ end
function M.apply(user_config)
local config = vim.deepcopy(M.defaults)

-- Lazy-load terminal defaults to avoid circular dependency
if config.terminal == nil then
local terminal_ok, terminal_module = pcall(require, "claudecode.terminal")
if terminal_ok and terminal_module.defaults then
config.terminal = terminal_module.defaults
end
end

if user_config then
-- Use vim.tbl_deep_extend if available, otherwise simple merge
if vim.tbl_deep_extend then
Expand Down
23 changes: 11 additions & 12 deletions lua/claudecode/server/tcp.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---@brief TCP server implementation using vim.loop
local client_manager = require("claudecode.server.client")
local utils = require("claudecode.server.utils")

local M = {}

Expand All @@ -19,20 +18,21 @@ local M = {}
---@param max_port number Maximum port to try
---@return number|nil port Available port number, or nil if none found
function M.find_available_port(min_port, max_port)
if min_port > max_port then
return nil -- Or handle error appropriately
end
assert(type(min_port) == "number", "min_port must be a number")
assert(type(max_port) == "number", "max_port must be a number")

local ports = {}
for i = min_port, max_port do
table.insert(ports, i)
if min_port > max_port then
return nil
end

-- Shuffle the ports
utils.shuffle_array(ports)
local port_count = max_port - min_port + 1
local start_offset = math.random(port_count) - 1

-- Try to bind to a port from the shuffled list
for _, port in ipairs(ports) do
-- Pick a random starting point, then scan the range once. This keeps the
-- selection spread across the configured range without building and shuffling
-- a 55k-entry table for the default 10000-65535 range on every startup.
for checked = 0, port_count - 1 do
local port = min_port + ((start_offset + checked) % port_count)
local test_server = vim.loop.new_tcp()
if test_server then
local success = test_server:bind("127.0.0.1", port)
Expand All @@ -42,7 +42,6 @@ function M.find_available_port(min_port, max_port)
return port
end
end
-- Continue to next port if test_server creation failed or bind failed
end

return nil
Expand Down
33 changes: 6 additions & 27 deletions lua/claudecode/server/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ local function bxor(a, b)
return result
end

local bit_ok, bit = pcall(require, "bit")
local native_bxor = bit_ok and bit and bit.bxor or nil

local function bnot(a)
return bxor(a, 0xFFFFFFFF)
end
Expand Down Expand Up @@ -364,32 +367,6 @@ function M.bytes_to_uint64(bytes)
return num
end

---XOR lookup table for faster operations
local xor_table = {}
for i = 0, 255 do
xor_table[i] = {}
for j = 0, 255 do
local result = 0
local a, b = i, j
local bit_val = 1

while a > 0 or b > 0 do
local a_bit = a % 2
local b_bit = b % 2

if a_bit ~= b_bit then
result = result + bit_val
end

a = math.floor(a / 2)
b = math.floor(b / 2)
bit_val = bit_val * 2
end

xor_table[i][j] = result
end
end

---Apply XOR mask to payload data
---@param data string The data to mask/unmask
---@param mask string The 4-byte mask
Expand All @@ -401,7 +378,9 @@ function M.apply_mask(data, mask)
for i = 1, #data do
local mask_idx = ((i - 1) % 4) + 1
local data_byte = data:byte(i)
result[i] = string.char(xor_table[data_byte][mask_bytes[mask_idx]])
local mask_byte = mask_bytes[mask_idx]
local masked_byte = native_bxor and native_bxor(data_byte, mask_byte) or bxor(data_byte, mask_byte)
result[i] = string.char(masked_byte)
end

return table.concat(result)
Expand Down
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ echo "Checking Lua files for syntax errors..."
# (macOS) too — `find -exec ... \;` swallows the child's status there.
find lua -name "*.lua" -type f -print0 | xargs -0 -I{} luajit -e "assert(loadfile('{}'))"
echo "Running luacheck..."
luacheck lua/ tests/ --no-unused-args --no-max-line-length
./.luarocks/bin/luacheck lua/ tests/ --no-unused-args --no-max-line-length
'''

[tasks.test]
Expand All @@ -115,7 +115,7 @@ TEST_FILES=$(find tests -type f -name "*_test.lua" -o -name "*_spec.lua" | sort)
echo "Found test files:"
echo "$TEST_FILES"
if [ -n "$TEST_FILES" ]; then
busted --coverage -v $TEST_FILES
./.luarocks/bin/busted --coverage -v $TEST_FILES
else
echo "No test files found"
fi
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/config_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ describe("Configuration", function()
expect(final_config.env).to_be_table() -- Should inherit default empty table
end)

it("should not load terminal module while applying configuration", function()
package.loaded["claudecode.terminal"] = nil

local final_config = config.apply({ auto_start = false, log_level = "info" })

expect(final_config.terminal).to_be_table()
expect(final_config.terminal.provider).to_be("auto")
expect(package.loaded["claudecode.terminal"]).to_be_nil()
end)

it("should reject invalid port range", function()
local invalid_config = {
port_range = { min = -1, max = 65536 },
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/server/tcp_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,62 @@ describe("TCP server disconnect handling", function()
client_manager.process_data = original_process_data
end)

describe("find_available_port", function()
local original_new_tcp
local original_random

before_each(function()
original_new_tcp = vim.loop.new_tcp
original_random = math.random
end)

after_each(function()
vim.loop.new_tcp = original_new_tcp
rawset(math, "random", original_random)
end)

it("should not build the whole default range when the first candidate is available", function()
local bind_count = 0
vim.loop.new_tcp = function()
return {
bind = function(self, host, port)
bind_count = bind_count + 1
return true
end,
close = function(self) end,
}
end

local port = tcp.find_available_port(10000, 65535)

assert.is_true(type(port) == "number")
assert.is_true(port >= 10000 and port <= 65535)
assert.are.equal(1, bind_count)
end)

it("should wrap and scan each port at most once", function()
local tried_ports = {}
rawset(math, "random", function(max)
assert.are.equal(3, max)
return 3
end)
vim.loop.new_tcp = function()
return {
bind = function(self, host, port)
table.insert(tried_ports, port)
return port == 10000
end,
close = function(self) end,
}
end

local port = tcp.find_available_port(10000, 10002)

assert.are.equal(10000, port)
assert.are.same({ 10002, 10000 }, tried_ports)
end)
end)

it("should call on_disconnect and remove client on EOF", function()
local callbacks = {
on_message = spy.new(function() end),
Expand Down
1 change: 1 addition & 0 deletions tests/unit/terminal_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe("claudecode.terminal (wrapper for Snacks.nvim)", function()
end

before_each(function()
package.loaded["tests.mocks.vim"] = nil
_G.vim = require("tests.mocks.vim")

local spy_instance_methods = {}
Expand Down
Loading