Skip to content

Make MCP socket tests portable to Windows - #348

Open
npapagna wants to merge 1 commit into
mainfrom
npapagna/windows-portable-socket-tests
Open

Make MCP socket tests portable to Windows#348
npapagna wants to merge 1 commit into
mainfrom
npapagna/windows-portable-socket-tests

Conversation

@npapagna

Copy link
Copy Markdown
Collaborator

What

Makes the MCP socket tests runnable on Windows, and closes two gaps found while
porting them.

The socket tests each rolled their own tmp-path helper and assumed a POSIX
filesystem, so the Windows CI run could never go green on them.
temporarySocketPath() replaces those ad hoc helpers with one shared,
truly-unique path (pid + process.hrtime, plus a named-pipe form on Windows)
that survives Vitest's per-file module resets, unlike a plain in-module counter.

While porting the ownership tests, a mislabeled describe block turned up: four
unrelated behaviors sat under the title "fails to start if the parent directory
is a file"
, a scenario nothing actually tested. The block is renamed and the
real regression test for that ENOTDIR path now exists, inside the POSIX-only
"real socket file behavior" block, since it builds a filesystem path directly
and a Windows named pipe fails with a generic EACCES instead.

Also folds two pre-existing, differently-spelled "skip on Windows" test helpers
(unixOnly, itUnlessWin32) into the same platformGates.ts module this work
introduces, and switches mcpSocketServer.ts to the shared isWindows()
instead of re-deriving process.platform === 'win32'.

Testing

npm run lint, npm run format:check, npm run compile, and npm test pass
locally on macOS. The Windows Health Check CI run is the one that matters here.

🤖 Generated with Claude Code

@npapagna
npapagna marked this pull request as ready for review July 30, 2026 19:59
@npapagna
npapagna force-pushed the npapagna/windows-portable-socket-tests branch from cd70eb9 to 6abd214 Compare July 30, 2026 19:59
@npapagna
npapagna added this pull request to the merge queue Jul 30, 2026
Comment thread client/src/mcpSocketServer.ts Outdated
Comment thread client/src/mcpSocketServer.ts Outdated
Comment thread client/src/mcpSocketServer.ts
Comment thread client/src/__tests__/platformGates.ts
Comment thread client/src/__tests__/mcpSocketOwnership.test.ts Outdated
Comment thread client/src/__tests__/mcpSocketOwnership.test.ts Outdated
Comment thread client/src/__tests__/mcpSocketServerIntegration.test.ts Outdated
@npapagna
npapagna removed this pull request from the merge queue due to a manual request Jul 30, 2026
The socket tests each rolled their own tmp-path helper and assumed a
POSIX filesystem, so the Windows CI run could never go green on them.
temporarySocketPath() replaces those ad hoc helpers with one shared,
truly-unique path (pid + process.hrtime, plus a named-pipe form on
Windows) that survives Vitest's per-file module resets, unlike a plain
in-module counter.

While porting the ownership tests, a mislabeled describe block turned
up: four unrelated behaviors sat under the title "fails to start if
the parent directory is a file", a scenario nothing actually tested.
The block is renamed and the real regression test for that ENOTDIR
path now exists, inside the POSIX-only "real socket file behavior"
block, since it builds a filesystem path directly and a Windows named
pipe fails with a generic EACCES instead.

Also folds two pre-existing, differently-spelled "skip on Windows"
test helpers (unixOnly, itUnlessWin32) into the same platformGates.ts
module this work introduces, and switches mcpSocketServer.ts to the
shared isWindows() instead of re-deriving process.platform === 'win32'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@npapagna
npapagna force-pushed the npapagna/windows-portable-socket-tests branch from 6abd214 to ad9222c Compare July 30, 2026 23:09
@npapagna
npapagna requested a review from MatiasFernandez July 30, 2026 23:12
let sidecarPath: string;
beforeEach(() => {
sidecarPath = makeTempSidecarPath();
sidecarPath = temporarySidecarPath();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚫 This swap changes what path.dirname(sidecarPath) means, and the afterEach right below (line 30) still recursively deletes it.

The old makeTempSidecarPath() was path.join(fs.mkdtempSync(...), 'mcp.owner.json'), so dirname was a dedicated temp directory and rmSync(dirname, { recursive: true, force: true }) was safe. The new temporarySidecarPath() returns path.join(os.tmpdir(), 'jasper-mcp-tempfile-<hex>.json') — no intermediate directory — so dirname is now os.tmpdir() itself, and that hook is effectively fs.rmSync(os.tmpdir(), { recursive: true, force: true }).

Three hooks are in this state: lines 30 and 106 here, and mcpServerTreeProvider.test.ts:38. Concretely, on Linux CI that's rm -rf /tmp after each of those tests, and on macOS the same for /var/folders/…/T. Since vitest runs files in parallel workers, it will delete the sockets and sidecars other test files are actively using mid-run — and force: true plus the surrounding try {} catch {} means it fails silently, so the symptom shows up as unrelated flaky failures elsewhere rather than as an error here.

Since the file lives directly in os.tmpdir() now, could these hooks just fs.rmSync(sidecarPath, { force: true }) (no recursive)? That matches what the helper actually creates.

Related: it('creates the parent directory if it does not exist') (line 44) builds nested as path.join(path.dirname(sidecarPath), 'sub', 'mcp.owner.json'), which is now a fixed os.tmpdir()/sub shared by every run instead of a per-test path — so that test loses its isolation too. Worth giving it its own unique directory?

let sidecarPath: string;
beforeEach(() => {
sidecarPath = makeTempSidecarPath();
sidecarPath = temporarySidecarPath();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚫 Same issue as my comment on mcpOwnerSidecar.test.ts:26 — the afterEach below now recursively removes os.tmpdir(), since temporarySidecarPath() no longer nests the file in its own directory.

*
* @param {string} file - The path to the file to delete.
*/
function safelyRemoveFile(file: string): void {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💭 The doc on withTemporaryFileDo promises the file is "deleted afterward regardless of success or failure", but this swallows the failure — so what's actually guaranteed is an attempt, and a leaked file surfaces only as a console.error line in the vitest output that nobody reads.

Is the swallow load-bearing for a case you hit? If the file is always created by createTemporaryFile() a line earlier, unlinkSync failing means something genuinely surprising happened (the consumer replaced the path with a directory, a permissions change), which seems worth failing on rather than logging. Or, if you'd rather keep the test green, soften the doc to say cleanup is best-effort so a future reader doesn't lean on the stronger promise?

Also, is console.error the right channel here? Everything else in the client routes through appendSysadmin, and in a test helper it just adds unattributable noise to the run output — there's no test name attached to it.

*
* @returns An absolute path to a non-existent `.json` file in the OS's temp directory.
*/
export function temporarySidecarPath() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💭 This one is about sidecars, not sockets, and two of its three callers have no socket in sight: mcpOwnerSidecar.test.ts tests the sidecar reader/writer, and mcpServerTreeProvider.test.ts tests ownership rendering in the tree. Both now reach into support/socket for it, which reads oddly at the import site.

Would support/sidecar.ts (or just putting it next to temporaryFilePath in support/file.ts, since that's all it delegates to) be a better home? The doc would get more accurate as a side effect too — "metadata or state alongside a temporarySocketPath()" describes only the mcpSocketOwnership usage.

* test runner's per-file module resets.
**/
export function temporarySocketPath(): string {
const socketName = `jasper-mcp-temp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: jasper-mcp-temp- (sockets) and jasper-mcp-tempfile- (from temporaryFileName()) are near-identical prefixes, and file.ts says its prefix is there "for easy identification". In practice ls /tmp | grep jasper-mcp-temp matches both, so the one thing the prefix is for doesn't quite work. Worth making this one jasper-mcp-socket-?

@@ -0,0 +1,14 @@
import { describe, it } from 'vitest';

const isPosix = process.platform === 'linux' || process.platform === 'darwin';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's a fair point, and I'd rather have the explicit allow-list than silently start running these somewhere we haven't validated — thanks for spelling out the reasoning.

💭 Given that's the intent, should the names say so? isPosix / onPosix* claim a property of the platform (freebsd and aix are POSIX, and would be excluded here), whereas what you're actually encoding is "the platforms we know socket behavior on". Something like isSupportedPosixPlatform with onSupportedPosixIt/Describe — or keeping onPosix* but having the doc say "Linux/macOS only; other POSIX platforms are excluded because we haven't validated socket behavior there" — would put your rationale where the next reader finds it instead of in this thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants