fix: stabilize windows ci

This commit is contained in:
Peter Steinberger 2025-11-10 16:13:27 +00:00
parent 8bd82cbb65
commit d59539778b
10 changed files with 63 additions and 89 deletions

View File

@ -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<void> {
.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());

View File

@ -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;

View File

@ -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<void> | 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<T>;
try {
parsed = JSON.parse(trimmed) as DaemonResponse<T>;
} catch (error) {
} catch {
const parseError = new Error('Failed to parse daemon response.');
(parseError as NodeJS.ErrnoException).code = 'ECONNRESET';
throw parseError;

View File

@ -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<void>
): Promise<void> {
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) {

View File

@ -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;

View File

@ -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<T extends DaemonRequestMethod = DaemonRequestMethod, P = unknown> {
readonly id: string;

View File

@ -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<string>;
}
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<Awaited<ReturnType<Runtime['listTools']>>> {
async listTools(server: string, options?: ListToolsOptions): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
if (this.shouldUseDaemon(server)) {
return (await this.daemon.listTools({
server,

View File

@ -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<typeof vi.spyOn>;
let homeSpy: ReturnType<typeof vi.spyOn>;
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;

View File

@ -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<ReturnType<Runtime['listTools']>> {
return this.listToolsMock(server, options);
async listTools(server: string, options?: ListToolsOptions): Promise<Awaited<ReturnType<Runtime['listTools']>>> {
return await this.listToolsMock(server, options);
}
async callTool(server: string, toolName: string, options?: CallOptions): Promise<unknown> {
return this.callToolMock(server, toolName, options);
return await this.callToolMock(server, toolName, options);
}
async listResources(server: string, options?: unknown): Promise<unknown> {
return this.listResourcesMock(server, options);
return await this.listResourcesMock(server, options);
}
async connect(): Promise<never> {
@ -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' });

View File

@ -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<typeof vi.spyOn>;
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, '');