From d59539778b4108d71939c56c68d98a9e1aa2c6ac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Nov 2025 16:13:27 +0000 Subject: [PATCH] fix: stabilize windows ci --- src/cli.ts | 10 +++--- src/cli/daemon-command.ts | 6 ++-- src/daemon/client.ts | 49 ++++++++++++++--------------- src/daemon/host.ts | 30 +++++++----------- src/daemon/launch.ts | 2 +- src/daemon/protocol.ts | 8 +---- src/daemon/runtime-wrapper.ts | 12 ++----- tests/config-import-paths.test.ts | 11 +++---- tests/keep-alive-runtime.test.ts | 14 ++++----- tests/runtime-process-utils.test.ts | 10 +++--- 10 files changed, 63 insertions(+), 89 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 84d926b..ae6f254 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,13 +20,13 @@ import { getActiveLogger, getActiveLogLevel, logError, logInfo, logWarn, setLogL import { consumeOutputFormat } from './cli/output-format.js'; import { DEBUG_HANG, dumpActiveHandles, terminateChildProcesses } from './cli/runtime-debug.js'; import { boldText, dimText, extraDimText, supportsAnsiColor } from './cli/terminal.js'; -import { analyzeConnectionError } from './error-classifier.js'; -import { parseLogLevel } from './logging.js'; import { resolveConfigPath } from './config.js'; -import { createRuntime, MCPORTER_VERSION } from './runtime.js'; import { DaemonClient } from './daemon/client.js'; import { createKeepAliveRuntime } from './daemon/runtime-wrapper.js'; +import { analyzeConnectionError } from './error-classifier.js'; import { isKeepAliveServer } from './lifecycle.js'; +import { parseLogLevel } from './logging.js'; +import { createRuntime, MCPORTER_VERSION } from './runtime.js'; export { parseCallArguments } from './cli/call-arguments.js'; export { handleCall } from './cli/call-command.js'; @@ -142,9 +142,7 @@ export async function runCli(argv: string[]): Promise { .map((entry) => entry.name) ); const daemonClient = - keepAliveServers.size > 0 - ? new DaemonClient({ configPath: configResolution.path, rootDir: rootOverride }) - : null; + keepAliveServers.size > 0 ? new DaemonClient({ configPath: configResolution.path, rootDir: rootOverride }) : null; const runtime = createKeepAliveRuntime(baseRuntime, { daemonClient, keepAliveServers }); const inference = inferCommandRouting(command, args, runtime.getDefinitions()); diff --git a/src/cli/daemon-command.ts b/src/cli/daemon-command.ts index 43665e2..1d9d1c7 100644 --- a/src/cli/daemon-command.ts +++ b/src/cli/daemon-command.ts @@ -1,8 +1,8 @@ -import { createRuntime } from '../runtime.js'; -import { isKeepAliveServer } from '../lifecycle.js'; import { DaemonClient, resolveDaemonPaths } from '../daemon/client.js'; -import { launchDaemonDetached } from '../daemon/launch.js'; import { runDaemonHost } from '../daemon/host.js'; +import { launchDaemonDetached } from '../daemon/launch.js'; +import { isKeepAliveServer } from '../lifecycle.js'; +import { createRuntime } from '../runtime.js'; interface DaemonCliOptions { readonly configPath: string; diff --git a/src/daemon/client.ts b/src/daemon/client.ts index dea5bc9..2b7e0f8 100644 --- a/src/daemon/client.ts +++ b/src/daemon/client.ts @@ -1,19 +1,18 @@ -import crypto from 'node:crypto'; +import crypto, { randomUUID } from 'node:crypto'; import net from 'node:net'; import path from 'node:path'; -import { randomUUID } from 'node:crypto'; -import { - type CallToolParams, - type CloseServerParams, - type DaemonRequest, - type DaemonResponse, - type DaemonRequestMethod, - type ListResourcesParams, - type ListToolsParams, - type StatusResult, -} from './protocol.js'; -import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js'; import { launchDaemonDetached } from './launch.js'; +import { getDaemonMetadataPath, getDaemonSocketPath } from './paths.js'; +import type { + CallToolParams, + CloseServerParams, + DaemonRequest, + DaemonRequestMethod, + DaemonResponse, + ListResourcesParams, + ListToolsParams, + StatusResult, +} from './protocol.js'; export interface DaemonClientOptions { readonly configPath: string; @@ -36,14 +35,12 @@ export function resolveDaemonPaths(configPath: string): DaemonPaths { } export class DaemonClient { - private readonly configKey: string; private readonly socketPath: string; private readonly metadataPath: string; private startingPromise: Promise | null = null; constructor(private readonly options: DaemonClientOptions) { const paths = resolveDaemonPaths(options.configPath); - this.configKey = paths.key; this.socketPath = paths.socketPath; this.metadataPath = paths.metadataPath; } @@ -118,16 +115,18 @@ export class DaemonClient { await this.startingPromise; return; } - this.startingPromise = Promise.resolve().then(() => { - launchDaemonDetached({ - configPath: this.options.configPath, - rootDir: this.options.rootDir, - metadataPath: this.metadataPath, - socketPath: this.socketPath, + this.startingPromise = Promise.resolve() + .then(() => { + launchDaemonDetached({ + configPath: this.options.configPath, + rootDir: this.options.rootDir, + metadataPath: this.metadataPath, + socketPath: this.socketPath, + }); + }) + .finally(() => { + this.startingPromise = null; }); - }).finally(() => { - this.startingPromise = null; - }); await this.startingPromise; } @@ -207,7 +206,7 @@ export class DaemonClient { let parsed: DaemonResponse; try { parsed = JSON.parse(trimmed) as DaemonResponse; - } catch (error) { + } catch { const parseError = new Error('Failed to parse daemon response.'); (parseError as NodeJS.ErrnoException).code = 'ECONNRESET'; throw parseError; diff --git a/src/daemon/host.ts b/src/daemon/host.ts index f09789f..d1cd17c 100644 --- a/src/daemon/host.ts +++ b/src/daemon/host.ts @@ -1,17 +1,17 @@ -import net from 'node:net'; import fs from 'node:fs/promises'; +import net from 'node:net'; import path from 'node:path'; -import { createRuntime, type Runtime } from '../runtime.js'; import type { ServerDefinition } from '../config.js'; -import { keepAliveIdleTimeout, isKeepAliveServer } from '../lifecycle.js'; -import { - type CallToolParams, - type CloseServerParams, - type DaemonRequest, - type DaemonResponse, - type ListResourcesParams, - type ListToolsParams, - type StatusResult, +import { isKeepAliveServer, keepAliveIdleTimeout } from '../lifecycle.js'; +import { createRuntime, type Runtime } from '../runtime.js'; +import type { + CallToolParams, + CloseServerParams, + DaemonRequest, + DaemonResponse, + ListResourcesParams, + ListToolsParams, + StatusResult, } from './protocol.js'; interface DaemonHostOptions { @@ -160,13 +160,7 @@ async function handleSocketRequest( metadata: { configPath: string; socketPath: string; startedAt: number }, shutdown: () => Promise ): Promise { - const { response, shouldShutdown } = await processRequest( - rawPayload, - runtime, - managedServers, - activity, - metadata - ); + const { response, shouldShutdown } = await processRequest(rawPayload, runtime, managedServers, activity, metadata); socket.write(JSON.stringify(response), () => { socket.end(() => { if (shouldShutdown) { diff --git a/src/daemon/launch.ts b/src/daemon/launch.ts index 9333429..cf478ff 100644 --- a/src/daemon/launch.ts +++ b/src/daemon/launch.ts @@ -1,5 +1,5 @@ -import path from 'node:path'; import { spawn } from 'node:child_process'; +import path from 'node:path'; export interface DaemonLaunchOptions { readonly configPath: string; diff --git a/src/daemon/protocol.ts b/src/daemon/protocol.ts index 5b37b1a..7fd55cb 100644 --- a/src/daemon/protocol.ts +++ b/src/daemon/protocol.ts @@ -1,10 +1,4 @@ -export type DaemonRequestMethod = - | 'callTool' - | 'listTools' - | 'listResources' - | 'closeServer' - | 'status' - | 'stop'; +export type DaemonRequestMethod = 'callTool' | 'listTools' | 'listResources' | 'closeServer' | 'status' | 'stop'; export interface DaemonRequest { readonly id: string; diff --git a/src/daemon/runtime-wrapper.ts b/src/daemon/runtime-wrapper.ts index 1816769..654608a 100644 --- a/src/daemon/runtime-wrapper.ts +++ b/src/daemon/runtime-wrapper.ts @@ -1,7 +1,7 @@ +import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js'; import type { ServerDefinition } from '../config.js'; import { isKeepAliveServer } from '../lifecycle.js'; import type { CallOptions, ListToolsOptions, Runtime } from '../runtime.js'; -import type { ListResourcesRequest } from '@modelcontextprotocol/sdk/types.js'; import type { DaemonClient } from './client.js'; interface KeepAliveRuntimeOptions { @@ -9,10 +9,7 @@ interface KeepAliveRuntimeOptions { readonly keepAliveServers: Set; } -export function createKeepAliveRuntime( - base: Runtime, - options: KeepAliveRuntimeOptions -): Runtime { +export function createKeepAliveRuntime(base: Runtime, options: KeepAliveRuntimeOptions): Runtime { if (!options.daemonClient || options.keepAliveServers.size === 0) { return base; } @@ -47,10 +44,7 @@ class KeepAliveRuntime implements Runtime { } } - async listTools( - server: string, - options?: ListToolsOptions - ): Promise>> { + async listTools(server: string, options?: ListToolsOptions): Promise>> { if (this.shouldUseDaemon(server)) { return (await this.daemon.listTools({ server, diff --git a/tests/config-import-paths.test.ts b/tests/config-import-paths.test.ts index 75859f3..68c0648 100644 --- a/tests/config-import-paths.test.ts +++ b/tests/config-import-paths.test.ts @@ -1,17 +1,15 @@ import os from 'node:os'; import path from 'node:path'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { pathsForImport } from '../src/config-imports.js'; describe('pathsForImport on Windows', () => { - let platformSpy: ReturnType; - let homeSpy: ReturnType; const homeDir = path.join(os.tmpdir(), 'mcporter-win-home'); const appData = path.join(homeDir, 'AppData', 'Roaming'); beforeEach(() => { - platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); - homeSpy = vi.spyOn(os, 'homedir').mockReturnValue(homeDir); + vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); + vi.spyOn(os, 'homedir').mockReturnValue(homeDir); process.env.HOME = homeDir; process.env.USERPROFILE = homeDir; process.env.APPDATA = appData; @@ -19,8 +17,7 @@ describe('pathsForImport on Windows', () => { }); afterEach(() => { - platformSpy.mockRestore(); - homeSpy.mockRestore(); + vi.restoreAllMocks(); delete process.env.HOME; delete process.env.USERPROFILE; delete process.env.APPDATA; diff --git a/tests/keep-alive-runtime.test.ts b/tests/keep-alive-runtime.test.ts index 2e58780..2ab3f91 100644 --- a/tests/keep-alive-runtime.test.ts +++ b/tests/keep-alive-runtime.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { ServerDefinition } from '../src/config.js'; -import type { CallOptions, ListToolsOptions, Runtime } from '../src/runtime.js'; import { createKeepAliveRuntime } from '../src/daemon/runtime-wrapper.js'; +import type { CallOptions, ListToolsOptions, Runtime } from '../src/runtime.js'; class FakeRuntime implements Runtime { private readonly definitions: ServerDefinition[]; @@ -34,16 +34,16 @@ class FakeRuntime implements Runtime { // no-op for tests } - async listTools(server: string, options?: ListToolsOptions): Promise> { - return this.listToolsMock(server, options); + async listTools(server: string, options?: ListToolsOptions): Promise>> { + return await this.listToolsMock(server, options); } async callTool(server: string, toolName: string, options?: CallOptions): Promise { - return this.callToolMock(server, toolName, options); + return await this.callToolMock(server, toolName, options); } async listResources(server: string, options?: unknown): Promise { - return this.listResourcesMock(server, options); + return await this.listResourcesMock(server, options); } async connect(): Promise { @@ -91,8 +91,8 @@ describe('createKeepAliveRuntime', () => { await keepAliveRuntime.listTools('alpha', { includeSchema: true }); expect(daemon.listTools).toHaveBeenCalledWith({ server: 'alpha', includeSchema: true, autoAuthorize: undefined }); - await keepAliveRuntime.listResources('alpha', { cursor: 1 }); - expect(daemon.listResources).toHaveBeenCalledWith({ server: 'alpha', params: { cursor: 1 } }); + await keepAliveRuntime.listResources('alpha', { cursor: '1' }); + expect(daemon.listResources).toHaveBeenCalledWith({ server: 'alpha', params: { cursor: '1' } }); await keepAliveRuntime.close('alpha'); expect(daemon.closeServer).toHaveBeenCalledWith({ server: 'alpha' }); diff --git a/tests/runtime-process-utils.test.ts b/tests/runtime-process-utils.test.ts index 021c52c..fd22782 100644 --- a/tests/runtime-process-utils.test.ts +++ b/tests/runtime-process-utils.test.ts @@ -1,5 +1,5 @@ import type { ChildProcess } from 'node:child_process'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const execFileMock = vi.fn(); @@ -12,16 +12,14 @@ vi.mock('node:child_process', async () => { }); describe('runtime-process-utils Windows process tree', () => { - let platformSpy: ReturnType; - beforeEach(() => { vi.resetModules(); execFileMock.mockReset(); - platformSpy = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); + vi.spyOn(process, 'platform', 'get').mockReturnValue('win32'); }); afterEach(() => { - platformSpy.mockRestore(); + vi.restoreAllMocks(); }); it('parses PowerShell output to enumerate descendants', async () => { @@ -33,7 +31,7 @@ describe('runtime-process-utils Windows process tree', () => { { ProcessId: rootPid + 3, ParentProcessId: 42 }, ]); - execFileMock.mockImplementation((command, args, options, callback) => { + execFileMock.mockImplementation((command, _args, options, callback) => { const cb = typeof options === 'function' ? options : callback; if (command === 'powershell.exe') { cb?.(null, powershellOutput, '');