Skip to content
Open
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
9 changes: 5 additions & 4 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ echo " - stylua (formatter)"
echo " - All other development tools"
echo ""
echo "You can also run development commands directly:"
echo " - make # Run full validation (format, lint, test)"
echo " - make test # Run tests"
echo " - make check # Run linter"
echo " - make format # Format code"
echo " - make # Run full validation (format, lint, test)"
echo " - make test # Run tests (fast, no coverage)"
echo " - make test-cov # Run tests with coverage (luacov)"
echo " - make check # Run linter"
echo " - make format # Format code"
7 changes: 5 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,18 @@ jobs:
run: nix develop .#ci -c make check

- name: Run tests
run: nix develop .#ci -c make test
run: nix develop .#ci -c make test-cov

- name: Check formatting
run: nix flake check

- name: Generate coverage report
run: |
if [ -f "luacov.stats.out" ]; then
nix develop .#ci -c luacov
# `make test-cov` runs `luacov` already, but keep this as a fallback.
if [ ! -f "luacov.report.out" ]; then
nix develop .#ci -c luacov
fi

echo "Creating lcov.info from luacov.report.out"
{
Expand Down
15 changes: 15 additions & 0 deletions .luacov
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
return {
statsfile = "luacov.stats.out",
reportfile = "luacov.report.out",

-- Only collect coverage for the plugin itself; excluding the test suite and
-- mocks makes `--coverage` runs significantly faster.
include = {
"lua/claudecode/",
"plugin/",
},
exclude = {
"tests/",
"fixtures/",
},
}
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

- `make check`: Syntax checks + `luacheck` on `lua/` and `tests/`.
- `make format`: Format with StyLua (via Nix `nix fmt` in this repo).
- `make test`: Run Busted tests with coverage (outputs `luacov.stats.out`).
- `make test`: Run Busted tests (fast, no coverage).
- `make test-cov`: Run Busted tests with coverage (generates `luacov.stats.out` + `luacov.report.out`).
- `make clean`: Remove coverage artifacts.
Examples:
- Run all tests: `make test`
- Run all tests with coverage: `make test-cov`
- Run one file: `busted -v tests/unit/terminal_spec.lua`

## Coding Style & Naming Conventions
Expand All @@ -32,13 +34,13 @@
- File naming: `*_spec.lua` or `*_test.lua` under `tests/unit` or `tests/integration`.
- Mocks/helpers: place in `tests/mocks` and `tests/helpers`; prefer pure‑Lua mocks.
- Coverage: keep high signal; exercise server, tools, diff, and terminal paths.
- Quick tip: prefer `make test` (it sets `LUA_PATH` and coverage flags).
- Quick tip: prefer `make test` (fast) or `make test-cov` (coverage) since they set `LUA_PATH` consistently.

## Commit & Pull Request Guidelines

- Commits: Use Conventional Commits (e.g., `feat:`, `fix:`, `docs:`, `test:`, `chore:`). Keep messages imperative and scoped.
- PRs: include a clear description, linked issues, repro steps, and tests. Update docs (`README.md`, `ARCHITECTURE.md`, or `DEVELOPMENT.md`) when behavior changes.
- Pre-flight: run `make format`, `make check`, and `make test`. Attach screenshots or terminal recordings for UX-visible changes (diff flows, terminal behavior).
- Pre-flight: run `make format`, `make check`, and `make test` (optionally `make test-cov` for CI-like coverage). Attach screenshots or terminal recordings for UX-visible changes (diff flows, terminal behavior).

## Security & Configuration Tips

Expand Down
24 changes: 13 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ claudecode.nvim - A Neovim plugin that implements the same WebSocket-based MCP p

### Testing

- `make test` - Run all tests using busted with coverage
- `make test` - Run all tests (fast, no coverage)
- `make test-cov` - Run all tests with coverage (luacov)
- `busted tests/unit/specific_spec.lua` - Run specific test file
- `busted --coverage -v` - Run tests with coverage
- `busted --coverage -v` - Run tests with coverage (prefer `make test-cov`)

### Code Quality

Expand All @@ -22,12 +23,12 @@ claudecode.nvim - A Neovim plugin that implements the same WebSocket-based MCP p

### Build Commands

- `make` - **RECOMMENDED**: Run formatting, linting, and testing (complete validation)
- `make all` - Run check and format (default target)
- `make test` - Run all tests using busted with coverage
- `make` / `make all` - **RECOMMENDED**: Run formatting, linting, and tests (fast, no coverage)
- `make test` - Run all tests (fast, no coverage)
- `make test-cov` - Run all tests with coverage (luacov)
- `make check` - Check Lua syntax and run luacheck
- `make format` - Format code with stylua (or nix fmt if available)
- `make clean` - Remove generated test files
- `make clean` - Remove coverage artifacts
- `make help` - Show available commands

**Best Practice**: Always use `make` at the end of editing sessions for complete validation.
Expand Down Expand Up @@ -156,7 +157,7 @@ claudecode.nvim implements **100% feature parity** with Anthropic's official VS

### Protocol Validation

Run `make test` to verify MCP compliance:
Run `make test` (or `make test-cov` for coverage) to verify MCP compliance:

- **Tool Format Validation**: All tools return proper MCP structure
- **Schema Compliance**: JSON schemas validated against VS Code specs
Expand Down Expand Up @@ -189,12 +190,13 @@ export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$LUA_PATH"
busted tests/unit/tools/specific_tool_spec.lua --verbose

# Or use make for full validation
make test # Recommended for complete validation
make test # Fast (no coverage)
make test-cov # Coverage (slower)
```

**Coverage Metrics**:

- **320+ tests** covering all MCP tools and core functionality
- **430+ tests** covering all MCP tools and core functionality
- **Unit Tests**: Individual tool behavior and error cases
- **Integration Tests**: End-to-end MCP protocol flow
- **Format Tests**: MCP compliance and VS Code compatibility
Expand Down Expand Up @@ -463,7 +465,7 @@ error({

### Code Quality Standards

- **Test Coverage**: Maintain comprehensive test coverage (currently **320+ tests**, 100% success rate)
- **Test Coverage**: Maintain comprehensive test coverage (currently **430+ tests**, 100% success rate)
- **Zero Warnings**: All code must pass luacheck with 0 warnings/errors
- **MCP Compliance**: All tools must return proper MCP format with JSON-stringified content
- **VS Code Compatibility**: New tools must match VS Code extension behavior exactly
Expand All @@ -473,7 +475,7 @@ error({
### Development Quality Gates

1. **`make check`** - Syntax and linting (0 warnings required)
2. **`make test`** - All tests passing (320/320 success rate required)
2. **`make test`** - All tests passing (optionally `make test-cov` for coverage)
3. **`make format`** - Consistent code formatting
4. **MCP Validation** - Tools return proper format structure
5. **Integration Test** - End-to-end protocol flow verification
Expand Down
5 changes: 4 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ claudecode.nvim/
Run tests using:

```bash
# Run all tests
# Run all tests (fast, no coverage)
make test

# Run all tests with coverage (writes luacov.report.out)
make test-cov

# Run specific test file
nvim --headless -u tests/minimal_init.lua -c "lua require('tests.unit.config_spec')"

Expand Down
44 changes: 27 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
.PHONY: check format test clean
.PHONY: check format test test-cov clean help

# Default target
all: format check test

# Detect if we are already inside a Nix shell
ifeq (,$(IN_NIX_SHELL))
NIX_PREFIX := nix develop .#ci -c
# Detect if the required dev tools are already available; otherwise run via Nix.
ifeq (,$(shell command -v busted >/dev/null 2>&1 && command -v luacheck >/dev/null 2>&1 && echo ok))
NIX_PREFIX := nix develop .\#ci -c
Comment on lines +6 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate Nix fallback on luacov too

The new NIX_PREFIX detection only checks for busted and luacheck, but make test-cov also requires luacov. In an environment where a developer has busted/luacheck on PATH but not luacov (common when coverage isn’t installed), NIX_PREFIX is empty and make test-cov will now fail at the luacov step instead of falling back to nix develop like it did before. Consider including luacov in the tool check or making test-cov explicitly use the Nix wrapper when luacov is missing.

Useful? React with 👍 / 👎.

else
NIX_PREFIX :=
endif
Expand All @@ -21,15 +21,24 @@ check:
format:
nix fmt

# Run tests
# Run tests (fast, no coverage)
test:
@echo "Running all tests..."
@export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$$LUA_PATH"; \
TEST_FILES=$$(find tests -type f -name "*_test.lua" -o -name "*_spec.lua" | sort); \
echo "Found test files:"; \
echo "$$TEST_FILES"; \
@echo "Running all tests (no coverage)..."
@TEST_FILES=$$(find tests -type f \( -name "*_test.lua" -o -name "*_spec.lua" \) | sort); \
if [ -n "$$TEST_FILES" ]; then \
$(NIX_PREFIX) busted --coverage -v $$TEST_FILES; \
$(NIX_PREFIX) sh -c 'export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$$LUA_PATH"; busted -v "$$@"' -- $$TEST_FILES; \
else \
echo "No test files found"; \
fi

# Run tests with coverage
# (Generates luacov.stats.out and luacov.report.out)
test-cov:
@echo "Running all tests with coverage..."
@TEST_FILES=$$(find tests -type f \( -name "*_test.lua" -o -name "*_spec.lua" \) | sort); \
if [ -n "$$TEST_FILES" ]; then \
$(NIX_PREFIX) sh -c 'export LUA_PATH="./lua/?.lua;./lua/?/init.lua;./?.lua;./?/init.lua;$$LUA_PATH"; busted --coverage -v "$$@"' -- $$TEST_FILES; \
$(NIX_PREFIX) luacov; \
else \
echo "No test files found"; \
fi
Expand All @@ -38,13 +47,14 @@ test:
clean:
@echo "Cleaning generated files..."
@rm -f luacov.report.out luacov.stats.out
@rm -f tests/lcov.info
@rm -f lcov.info tests/lcov.info

# Print available commands
help:
@echo "Available commands:"
@echo " make check - Check for syntax errors"
@echo " make format - Format all files (uses nix fmt or stylua)"
@echo " make test - Run tests"
@echo " make clean - Clean generated files"
@echo " make help - Print this help message"
@echo " make check - Check for syntax errors"
@echo " make format - Format all files (uses nix fmt or stylua)"
@echo " make test - Run tests (fast, no coverage)"
@echo " make test-cov - Run tests with coverage (luacov)"
@echo " make clean - Clean generated files"
@echo " make help - Print this help message"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ opts = {

## Contributing

See [DEVELOPMENT.md](./DEVELOPMENT.md) for build instructions and development guidelines. Tests can be run with `make test`.
See [DEVELOPMENT.md](./DEVELOPMENT.md) for build instructions and development guidelines. Tests can be run with `make test` (fast) or `make test-cov` (coverage).

## License

Expand Down
36 changes: 26 additions & 10 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,38 @@ 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)
assert(type(min_port) == "number", "Expected min_port to be a number")
assert(type(max_port) == "number", "Expected max_port to be a number")

min_port = math.floor(min_port)
max_port = math.floor(max_port)

if min_port > max_port then
return nil -- Or handle error appropriately
return nil
end

local ports = {}
for i = min_port, max_port do
table.insert(ports, i)
local range_size = max_port - min_port + 1
assert(range_size >= 1, "Expected port range to be non-empty")

local start_offset = 0
if range_size > 1 then
-- Avoid `math.randomseed` here: it mutates global RNG state and is expensive
-- under coverage. We only need a pseudo-random starting point to avoid always
-- trying min_port first.
local uv = vim.loop
if uv and type(uv.hrtime) == "function" then
start_offset = tonumber(uv.hrtime() % range_size)
elseif uv and type(uv.now) == "function" then
start_offset = uv.now() % range_size
else
start_offset = os.time() % range_size
end
end

-- Shuffle the ports
utils.shuffle_array(ports)
-- Try every port in the range exactly once, starting from a pseudo-random point.
for i = 0, range_size - 1 do
local port = min_port + ((start_offset + i) % range_size)

-- Try to bind to a port from the shuffled list
for _, port in ipairs(ports) do
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 +59,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
56 changes: 18 additions & 38 deletions lua/claudecode/server/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -364,57 +364,37 @@ 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
---@return string masked The masked/unmasked data
local bit_bxor = nil

do
local ok, bit = pcall(require, "bit")
if ok and type(bit.bxor) == "function" then
bit_bxor = bit.bxor
end
end

function M.apply_mask(data, mask)
assert(type(data) == "string", "Expected data to be a string")
assert(type(mask) == "string", "Expected mask to be a string")
assert(#mask == 4, "Expected mask to be 4 bytes")

local result = {}
local mask_bytes = { mask:byte(1, 4) }
local m1, m2, m3, m4 = mask:byte(1, 4)
assert(type(m1) == "number" and type(m2) == "number" and type(m3) == "number" and type(m4) == "number", "Invalid mask")
local mask_bytes = { m1, m2, m3, m4 }

local do_bxor = bit_bxor or bxor
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]])
result[i] = string.char(do_bxor(data_byte, mask_bytes[mask_idx]))
end

return table.concat(result)
end

---Shuffle an array in place using Fisher-Yates algorithm
---@param tbl table The array to shuffle
function M.shuffle_array(tbl)
math.randomseed(os.time())
for i = #tbl, 2, -1 do
local j = math.random(i)
tbl[i], tbl[j] = tbl[j], tbl[i]
end
end

return M
Loading
Loading