Repository navigation
Use @animalabs/mcpl-core 0.4.0, which needs Node 22.12.0 or later - #16
nissa-seru wants to merge 5 commits into
Conversation
Three tests drive a server over a stream pair written as raw lines: - a request whose JSON string holds U+2028 or U+2029 gets its answer; - a malformed line is logged, and the next request is answered; - a stream error ends serve() and is logged. On main, with @animalabs/mcpl-core 0.3.0 and no 'error' listener on the connection, each of the three ends the test process with an unheard 'error'.
0.4.0 reads a line only at \n, so U+2028 and U+2029 inside a JSON string no longer split a frame on Node 24. It also closes a connection once when one of its streams fails, and reports that as 'error'. Its acceptTcp no longer leaves listeners on the server, so the server's own 'error' between accepts is the caller's to hear. - package.json: @animalabs/mcpl-core ^0.4.0, and engines.node >=22.12.0, 0.4.0's own floor. The lockfile takes 0.4.0. - serve() listens for its connection's 'error' and logs it. A malformed line leaves the connection open; a stream error closes it, so serve() returns: over TCP the next host can connect, and under --stdio the process shuts down as it does when stdin ends. - The TCP server gets an 'error' listener once it is listening, which logs. A failed accept while one is pending still rejects that accept. - scripts/e2e/fake-host.mjs recognises a closed connection by ConnectionClosedError's name: two of 0.4.0's three texts for it no longer say "connection closed". The comment above it names the class. - README.md and deploy/github-mcpl.env.example give the Node floor as 22.12.0. - Two changelog fragments: the Node floor (breaking) and the fix.
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.
Reviewed commit 50afa96 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It moves to the next version of the project's own core dependency and adds logging for connection errors through the existing Logger, and it updates the docs and changelog the way the project asks.
4 reasons with sources
The higher Node floor (22.12.0) is a cost operators carry, and the change records it as a breaking change as it should.
- It fixes crashes in the server's own connection handling, using the dependency the project already uses.
@animalabs/mcpl-corewas already the transport. Only its version changes, and the lockfile shows no other package changing version. The new 'error' listeners sit in serve() and in main()'s TCP setup, where the connection and the server are created. (package.json (dependencies)) - Errors are logged through the Logger to stderr, as the README requires. serve() uses this.logger.error, and index.ts uses log.error, so stdout still carries only JSON-RPC. (README.md (development section: "every diagnostic goes through the
Logger")) - The higher Node floor follows from the dependency and is recorded as a breaking change for operators. The breaking fragment opens with
- **Operators:** …, as the convention asks. The fixed fragment uses the documented<slug>.<kind>.mdnaming. The README and deploy/github-mcpl.env.example are updated to the new floor. (changelog.d/README.md) - The new test file and the fake-host change belong to the same version bump. isGone now matches 0.4.0's error name (ConnectionClosedError) instead of message text. The new tests cover the behaviour this change adds. Neither is unrelated work. (scripts/e2e/fake-host.mjs)
Findings
1 finding of medium or high severity, see the inline comment:
- Medium severity, test coverage. CI on Node 22 cannot catch a return of the U+2028/U+2029 bug, which only appears on Node 24.
4 more findings, less severe
- Low severity, maintainability,
scripts/e2e/fake-host.mjs:440-442. isGone now relies on a single error name and nothing tests it. The old check matched 'connection closed' in the message. The new one accepts onlyerr.name === 'ConnectionClosedError'. If 0.4.0 rejects a pending request with the underlying stream error instead (for example after the stream-error change in mcpl-core-ts#11),isGonereturns false. The run would then go on sending commands to a dead server and report each one as a new failure. No test or e2e run in the PR covers this path. - Low severity, maintainability,
src/server.ts:659. Two different paths log the same 'connection error' message. The new listener logs'connection error'. The existing catch block at lines 698-701 logs the same message for any rejection from nextMessage/handleInitialize that is not a ConnectionClosedError. If a stream error both emits 'error' and rejects the pending read with the same error, it is logged twice, and operators cannot tell which path wrote a given line. Use a distinct message, or log in one place only. - Low severity, test coverage,
src/index.ts:203. No test covers the TCP server's 'error' listener. The newtcpServer.on('error', ...)is only shown working by manual runs with injected errors, as the description admits. Nothing stops it from being removed or moved beforelisteningresolves. Moving the TCP accept loop into a function the tests can call would let a test emit a server error while a host is connected. - Low severity, test coverage,
test/connection-errors.test.ts:72-74. The U+2028 test also passes when the request gets an error answer.assert.ok(answer.result ?? answer.error)passes on any JSON-RPC error with the right id, so the test does not check that the cursor string reached the handler intact. The fixed path should answertools/listwith a result, as the malformed-line test expects. Assert onanswer.result.
For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.
|
|
||
| test('a request whose string holds U+2028 or U+2029 is answered', { timeout: 5000 }, async (t) => { | ||
| const h = rawHost(); | ||
| t.after(() => h.toServer.end()); | ||
| await h.initialize(); | ||
|
|
||
| // Written by hand: JSON.stringify leaves U+2028 and U+2029 unescaped, as a host's encoder may. | ||
| h.toServer.write(`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"cursor":"a\u2028b"}}\n`); | ||
| h.toServer.write(`{"jsonrpc":"2.0","id":3,"method":"tools/list","params":{"cursor":"a\u2029b"}}\n`); | ||
|
|
||
| for (const id of [2, 3]) { | ||
| const answer = (await h.answerTo(id)) as { result?: unknown; error?: unknown }; | ||
| assert.ok(answer.result ?? answer.error, `request ${id} gets a JSON-RPC answer`); | ||
| } |
There was a problem hiding this comment.
Medium severity, test coverage. CI on Node 22 cannot catch a return of the U+2028/U+2029 bug, which only appears on Node 24.
The PR description says the U+2028 split only happens with mcpl-core 0.3.0's reader on Node 24. .github/workflows/ci.yml runs only node-version: 22, so on CI this test passes with either 0.3.0 or 0.4.0. It cannot fail there. The red-first run was done by hand on Node 24. Add a Node 24 job to the CI matrix, or the regression this test exists for will not be caught in CI.
On Node 22, readline doesn't end a line at U+2028 or U+2029, so the test for those frames passes there with @animalabs/mcpl-core 0.3.0 too. CI now runs on Node 22 and 24, so it can fail where the split happens. The test now asserts each of the two requests is answered with a result, as the malformed-line test does, rather than with any answer.
main now carries the faster test suite (anima-research#18), which gives CI's job a 10-minute timeout where this branch adds the Node 22 and 24 matrix. The job keeps both.
main now runs CI the testing setup's way (anima-research#19, anima-research#20): the whole suite once at a pinned Node, and an install and start check on engines' floor, Node 24 and the newest Node release. With engines at >=22.12.0 here, the resolution keeps that shape at this branch's floor: - The suite job runs at Node 22.12.0, engines' floor, in place of this branch's Node 22 and 24 matrix of whole suites. - The install and start check runs on Node 24 and on the newest release. main's 22.0.0 leg checked the old floor; the floor is now 22.12.0, where the suite job runs. - README's development line keeps this branch's "Node 22.12 or newer", which covers npm test as well as the server.
This moves github-mcpl to
@animalabs/mcpl-core0.4.0, and raises github-mcpl's Node floor to 22.12.0, 0.4.0's own. With 0.4.0, on Node 24, a host request whose string holds U+2028 or U+2029 is answered, where onmainatc243333one holding U+2028 ends the process. github-mcpl now also listens for the errors its connection reports. A malformed line from the host is logged, and the connection stays open. A stream error is logged and ends that connection, and under--stdiothe process then shuts down as it does when the host closes stdin.3dd5289, this head, mergesmainat77996a2, which holds #19 and #20, into this branch, to resolve the conflicts #19 made in.github/workflows/ci.ymlandREADME.md. At this head, CI runs the typecheck and the whole suite once, on Node 22.12.0, this branch's floor, and checks that the server installs, builds and starts on Node 24 and on the newest Node release.Who needs to act
Operators running github-mcpl on a Node earlier than 22.12.0.
engines.nodegoes from>=22.0.0to>=22.12.0, theengines.nodeof@animalabs/mcpl-core0.4.0 (npm view @animalabs/mcpl-core@0.4.0 engines). README.md anddeploy/github-mcpl.env.examplenow give the new floor.Problem
On
mainatc243333, github-mcpl uses mcpl-core 0.3.0 and attaches no'error'listener to the host's connection. On Node 24, 0.3.0's reader ends a line at U+2028, so a frame whose JSON string holds one arrives split, and the connection reports it as a "Malformed JSON-RPC message"'error'. With no listener, the error is thrown. The same happens for a line that isn't JSON and for an error on the stream.c243333's code: all three fail with the connection's error. The U+2028 and U+2029 test fails on its U+2028 request, with an error that begins "Malformed JSON-RPC message: Unterminated string in JSON".McplConnectionwith no'error'listener, on 0.3.0 and Node 24: the process exits 1 on a request holding U+2028, on a malformed line and on a stream error.c243333, with errors injected: with stdin destroyed by an error under--stdio, or a host resetting its TCP socket, the process exits 1. Over TCP, the next host is then refused.Changes
package.jsonandpackage-lock.json:@animalabs/mcpl-core^0.3.0becomes^0.4.0, locked at 0.4.0. No other package in the lockfile changes version. 0.4.0 ends a line at\nalone (fix(connection): end a stream's line at \n alone, read a WebSocket message whole, and send U+2028/U+2029 escaped mcpl-core-ts#13), so the frame reaches the parser whole.src/server.ts:serve()attaches an'error'listener to the connection that logs the error. In 0.4.0 a malformed line is reported as an'error'and the connection stays open, and a stream error closes the connection and is then reported (Close McplConnection when either stream fails, and report the error once mcpl-core-ts#11). With the listener, a malformed line is logged and the next request is answered. A stream error is logged, andserve()returns. Over TCP the next host can then connect. Under--stdiothe process shuts down as it does when the host closes stdin.src/index.ts: once the TCP server is listening, an'error'listener on it logs the error. 0.4.0'sacceptTcpleaves no listener on the server once it settles (fix(connection): acceptTcp leaves no listener on the server once it settles mcpl-core-ts#12), so without this one, a server error between accepts ends the process, as a run of a build shows below..github/workflows/ci.yml: againstmain, the install and start check loses its Node 22.0.0 leg, which checksmain'senginesfloor, and keeps Node 24 and the newest release. The comment over the jobs now gives 22.12.0 as engines' floor. The whole suite runs at Node 22.12.0, as onmain.scripts/e2e/fake-host.mjs:isGonerecognises a closed connection byerr.name === 'ConnectionClosedError'instead of matching "connection closed" in the message.changelog.d/mcpl-core-0.4.breaking.mdfor the Node floor, andchangelog.d/mcpl-core-0.4.fixed.mdfor U+2028 and U+2029, a malformed line, a stream error, and a TCP server error while a host is connected.Tests
test/connection-errors.test.tsis new, with three tests. Each serves one connection whose host side writes raw lines:serve()and is logged.On this head's files apart from
.github/workflows/ci.yml, on Linux x86_64 with Node 22.12.0, in a clean checkout afternpm ci, with mcpl-core 0.4.0 installed,test/connection-errors.test.tspasses 3 of 3. With the installed reader changed to end a line at U+2028 as well as at\n, the U+2028 and U+2029 test fails and the other two pass. So on Node 22.12.0, where CI runs the tests, the test fails for a reader that splits at U+2028. The comment on 0.4.0's reader says "Lines end at '\n' and nowhere else" (src/connection.ts:299to:300at mcpl-core-ts'sv0.4.0).At
94b403d, the head before this one, on Linux, in a clean checkout afternpm ci: typecheck and build succeed with Node 24.22.1. We ran the command thatnpm testruns, with a TAP reporter added, on Node 24.22.1 and on Node 22.12.0. Each run has 620 tests: 600 pass, 18 are skipped, and 2 fail, the same 2 README tests as below.On Linux with Node 24.22.1, before either merge of
main:6d72345, in a clean checkout afternpm ci: typecheck and build succeed.npm testruns 609 tests: 589 pass, 18 are skipped, and 2 fail. The 2 are the README tests intest/wp8-docs.test.ts, which needjqand a git checkout the machine lacked.c243333, the same way, gives 606, 586, 18 and the same 2.05cd9dbadded them, onc243333's code, in a clean checkout of05cd9dbafternpm ci, all three fail. The U+2028 and U+2029 test then asked for any JSON-RPC answer, where the head's asks for a result.serve()'s listener deleted from a checkout whosesrcand tests are50afa96's: the U+2028 and U+2029 test passes, and the other two fail.50afa96, whosesrcis6d72345's, with a probe and a preload beside it that inject each error:--stdio, a stream error is logged and the process exits 0;What this leaves
mainatc243333. The server's listener logs the error first.main(). The runs of the built server above are its evidence.readlineinterface over its input (src/connection.ts:79at mcpl-core-ts'sv0.3.0). On Linux x86_64, fed one line holding U+2028 and one holding U+2029,node:readlinegave one line each on Node 22.12.0, and two each on Node 24.22.1 and on Node 26.13.0. CI runs the tests only on Node 22.12.0, the version on whichnode:readlinegave one line for each.src/index.tsandsrc/server.ts. This adds one line to each, and merges cleanly with Merge, update-branch, workflow runs and review reads, behind opt-in allow-lists #11's head,9127a61.isGone, so the two conflict there.Ogden-2688 made the change and both merges of
main. Esther-2584 read the change and both merges, and ran the built server. Daniel-2612 wrote this description, and Fiona-2757 updated it for the second merge.