Repository navigation
fix(server): tolerate unknown protocol enum values in initialize - #455
Conversation
📝 WalkthroughWalkthroughThis PR normalizes hover markup enums, adds a hostile-params injection utility and integration test for initialize robustness, enhances replay initialization error reporting, updates a kotatsu dependency pin, and adds a smoke-session fixture. ChangesLSP Protocol Robustness Testing and Related Updates
sequenceDiagram
participant Test
participant InjectionFactory
participant Server
Test->>InjectionFactory: build_params(InitializeParams, mode)
InjectionFactory-->>Test: params, stats
Test->>Server: initialize(hostile_params)
Server-->>Test: initialize response
Test->>Server: initialized notification
Test->>Server: shutdown request
Server-->>Test: shutdown response
Test->>Server: exit
Possibly related PRs:
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
cmake/package.cmake (1)
44-44: Track the kotatsu PR merge method to determine if pin needs updating.As noted in the PR objectives, if clice-io/kotatsu#167 is squash-merged, this commit hash will become invalid and the pin will need to be updated to the squash commit. Monitor the merge strategy when that PR lands.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmake/package.cmake` at line 44, The GIT_TAG pin in cmake/package.cmake (the line containing GIT_TAG 0a1141887ff3a5a48c146416e965fbf90d99e632) may become invalid if clice-io/kotatsu#167 is squash-merged; monitor that PR's merge strategy and, if it is squash-merged, update the GIT_TAG to the resulting squash commit hash (or switch to a stable tag) so the pin resolves correctly; update the comment next to GIT_TAG to note which PR it tracks (clice-io/kotatsu#167) and why the pin may need refreshing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@cmake/package.cmake`:
- Line 44: The GIT_TAG pin in cmake/package.cmake (the line containing GIT_TAG
0a1141887ff3a5a48c146416e965fbf90d99e632) may become invalid if
clice-io/kotatsu#167 is squash-merged; monitor that PR's merge strategy and, if
it is squash-merged, update the GIT_TAG to the resulting squash commit hash (or
switch to a stable tag) so the pin resolves correctly; update the comment next
to GIT_TAG to note which PR it tracks (clice-io/kotatsu#167) and why the pin may
need refreshing.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 370d15dd-a0ca-4fe2-b331-9eb0bb0cd0eb
📒 Files selected for processing (7)
cmake/package.cmakesrc/feature/hover.cpptests/integration/lifecycle/test_protocol_robustness.pytests/integration/utils/injection.pytests/replay.pytests/smoke/session_lsp318.jsonltests/unit/feature/hover_tests.cpp
e83a6a6 to
19314bc
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/integration/lifecycle/test_protocol_robustness.py`:
- Around line 59-65: The test creates the subprocess via
asyncio.create_subprocess_exec and opens stderr as a PIPE (proc.stderr) but
never reads it, risking the pipe filling and deadlocking initialize/shutdown
waits; fix by not leaving stderr unconsumed — either redirect stderr to stdout
(use stderr=asyncio.subprocess.STDOUT) or to DEVNULL, or add an async task to
continuously read proc.stderr (e.g., a reader coroutine that drains proc.stderr)
so the child cannot block on writing to stderr; update the subprocess creation
call or add the draining coroutine and ensure it runs for the lifetime of proc.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8c5791f1-3015-42b9-a787-254a0ba3aa2f
📒 Files selected for processing (7)
cmake/package.cmakesrc/feature/hover.cpptests/integration/lifecycle/test_protocol_robustness.pytests/integration/utils/injection.pytests/replay.pytests/smoke/session_lsp318.jsonltests/unit/feature/hover_tests.cpp
✅ Files skipped from review due to trivial changes (2)
- cmake/package.cmake
- tests/smoke/session_lsp318.jsonl
🚧 Files skipped from review as they are similar to previous changes (4)
- tests/unit/feature/hover_tests.cpp
- tests/replay.py
- src/feature/hover.cpp
- tests/integration/utils/injection.py
19314bc to
06e58c9
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
tests/integration/lifecycle/test_protocol_robustness.py (1)
59-65:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid unconsumed
stderrpipe to prevent test hangs.Line 64 sets
stderrtoPIPE, butproc.stderris never drained. If server logs are verbose, the child can block on write and hang this test.Suggested fix
proc = await asyncio.create_subprocess_exec( str(executable), "server", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/lifecycle/test_protocol_robustness.py` around lines 59 - 65, The test spawns a subprocess with asyncio.create_subprocess_exec and sets stderr=PIPE but never reads proc.stderr, which can cause the child to block; update the test to either redirect stderr to asyncio.subprocess.DEVNULL or ensure proc.stderr is consumed (e.g., read/drain proc.stderr asynchronously or start a task to read it) so the child's stderr buffer cannot fill and hang the test when using proc from asyncio.create_subprocess_exec.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@tests/integration/lifecycle/test_protocol_robustness.py`:
- Around line 59-65: The test spawns a subprocess with
asyncio.create_subprocess_exec and sets stderr=PIPE but never reads proc.stderr,
which can cause the child to block; update the test to either redirect stderr to
asyncio.subprocess.DEVNULL or ensure proc.stderr is consumed (e.g., read/drain
proc.stderr asynchronously or start a task to read it) so the child's stderr
buffer cannot fill and hang the test when using proc from
asyncio.create_subprocess_exec.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0e325340-b0b5-4691-b54c-15ad78fe0873
📒 Files selected for processing (7)
cmake/package.cmakesrc/feature/hover.cpptests/integration/lifecycle/test_protocol_robustness.pytests/integration/utils/injection.pytests/replay.pytests/smoke/session_lsp318.jsonltests/unit/feature/hover_tests.cpp
✅ Files skipped from review due to trivial changes (1)
- cmake/package.cmake
🚧 Files skipped from review as they are similar to previous changes (5)
- tests/unit/feature/hover_tests.cpp
- tests/replay.py
- tests/smoke/session_lsp318.jsonl
- src/feature/hover.cpp
- tests/integration/utils/injection.py
Newer LSP clients (vscode-languageclient 10 / LSP 3.18) send enum values kotatsu's generated protocol rejected, failing the whole InitializeParams parse and aborting startup with InvalidParams. Bump kotatsu to the rev that generates string enums as string wrapper types and keeps integer enums at wire-format width, and adapt the one MarkupKind use. Add generative integration tests injecting unknown string enum values, out-of-range integers, and unknown fields into every enum field reachable from InitializeParams. Record a smoke fixture from real VS Code 1.124 with vscode-languageclient 10, and make replay.py fail a trace whose initialize response is an error (previously error responses replayed as PASS, which is how this regression went unnoticed).
06e58c9 to
9e7cc3f
Compare
## Why clice's existing tests speak LSP through pygls or replay recorded sessions, so nothing exercised a real editor end to end. The very first run of the new nvim harness caught the `initialize` capability-decoding bug independently fixed on main by #455 — exactly the class of breakage this layer exists for: real clients, latest stable versions, full startup path. ## What - **Editor E2E smoke tests** ({nvim, VSCode} × {`hello_world`, `modules/hover_on_imported_symbol`}): startup/attach, didOpen + first diagnostics, index readiness, hover, definition (asserting the modules fixture jumps across the module boundary into `defs.cppm`), completion, clean shutdown. Assertions stop at "well-formed, non-empty" — content correctness stays in the pytest suites. Test code lives with each editor client: `editors/nvim/tests/e2e.lua` (driven by `nvim -l`, starting clice through the shipped LSP config) and `editors/vscode/src/test/e2e.test.ts` (via `@vscode/test-cli`; `pretest` now compiles tests too — `out/test/` was never built before, so the existing sample suite never actually ran). - **CI**: native platforms split into build (upload artifact) and test (download artifact) jobs, mirroring the cross-compile pattern. New `test-editor` job installs the latest stable nvim + VSCode — deliberately unpinned, this job exists to catch new-editor-version breakage. Zed extension gets a `wasm32-wasip1` compile check. - **Local entry point**: `pixi run -e editor editor-test`, fixture prep in `tests/prepare.py`; documented in the dev guide (`docs/{en,zh}/dev/test-and-debug.md`). - **Docs**: `docs/{en,zh}/guide/editors.md` with generic LSP client snippets for Helix, Emacs, Sublime Text, Kate and Vim. ## Verification All run locally (rebased on #455): unit 599 (Debug + RelWithDebInfo), integration 172, smoke 3/3, nvim E2E 2/2 (real nvim 0.12.3 stable), VSCode E2E 2×6 (real VSCode 1.124.2 under WSLg), `vsce package`, `pixi run format`. ## Notes for review - `test` jobs `needs: build`, which gates on all five native build legs (GitHub Actions cannot depend on single matrix legs); a one-leg build failure now skips all test jobs instead of just its own. - Native test jobs (and `test-editor`) run in the `test-run` pixi env relying on the runner's system toolchain for fixture CDB generation — same pattern the green `test-cross` jobs already use, but windows-2025/ubuntu-24.04 x64 exercise it for the first time.
Problem
LSP clients newer than the protocol schema clice was built against can fail the entire
initializehandshake. The failure chain:ClientCapabilitiescarries enum values introduced after LSP 3.17 — e.g.codeActionKind.valueSetcontaining"notebook"/"refactor.move", newSymbolKindmembers in avalueSet, or unknown capability fields.enum class) and narrowed integer enums to the smallest underlying type covering known members (SymbolKind→uint8_t).InitializeParamsfailed the whole message parse. The server answeredInvalidParams, the editor gave up, and clice never started.This is reproducible today with vscode-languageclient 10 (which emits LSP 3.18 capabilities) against any pre-fix clice binary:
It went unnoticed in CI because
tests/replay.pytreated error responses as a successful replay — a recorded session whoseinitializewas rejected still reported PASS.Fix
The root cause is fixed in kotatsu (clice-io/kotatsu#167, merged as
b4bcd3c): string-valued enumerations are now generated asstd::stringwrapper types with named constants, so unknown values pass through decoding unchanged; integer-valued enumerations keep the LSP wire-format width (integer/uinteger), so out-of-range values survive as well.This PR consumes that fix and hardens clice against the failure class ever returning:
cmake/package.cmaketo the merged commitb4bcd3c, and adapt the twoMarkupKindmember spellings in hover to the new wrapper shape.tests/integration/lifecycle/test_protocol_robustness.py+tests/integration/utils/injection.py): walk lsprotocol's type metadata and build aninitializerequest that injects, into every enum field reachable fromInitializeParams, (a) unknown string enum values, (b) out-of-range integers, (c) unknown object fields — then assert the server answersinitializenormally and shuts down cleanly. Three parametrized modes, with a floor assertion so a silently shrinking injection count fails the test.tests/smoke/session_lsp318.jsonl): a session recorded from stock VS Code 1.124 running the clice extension with vscode-languageclient 10 — initialize with LSP 3.18 capabilities, didOpen, hover, code actions, semantic tokens, shutdown, exit. Picked up automatically by the existing smoke-test glob.tests/replay.pynow fails a replay whoseinitializeresponse is an error. With this change alone, a pre-fix binary fails the new fixture with exactly the error above; cancellation errors on other requests still replay as PASS, as before.Verification
initialize rejectederror; the fixed binary passes 3/3.Notes
createOutputChannel(..., { log: true })for itsLogOutputChannelrequirement) is left as a follow-up.