Conversation
Introduces McpClient and supporting helpers for testing the GitKraken
MCP server over stdio JSON-RPC 2.0:
- McpClient: spawns gk CLI, handles initialize/tools/list/tools/call,
auto-cancels elicitation/create requests
- findGkCliFromArgs: derives gk path from --user-data-dir launch arg
(correct for E2E temp directories)
- findLatestIpcFile: locates the live IPC discovery file by checking
process liveness via process.kill(pid, 0)
- waitForCliInstall: polls for gk binary appearance (~5-6s on first run)
- mcpTest Playwright fixture: wires all helpers into a ready-to-use
McpClient scoped to the worker VS Code instance
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Corrects import order (baseTest before mcpHelper)
- Adds console.warn when no live IPC file is found so missing
GK_GL_PATH is surfaced early rather than silently ignored
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Exports McpConfigResult type for use in test files
- Removes any casts: listTools uses typed result shape,
elicitation handler uses msg.id directly
- Adds import for node:process to satisfy no-restricted-globals rule
- Captures stderr in sendRequests and getMcpConfig and includes it
in error/timeout messages for easier debugging
- Adds braces to single-line if statements per no-restricted-syntax rule
- Fixes import order (node:child_process before node:fs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🤖 Augment PR SummarySummary: Adds initial MCP end-to-end testing infrastructure for GitLens. Changes:
Technical Notes: IPC discovery files are located under 🤖 Was this summary useful? React with 👍 or 👎 |
| ], | ||
| 2, | ||
| ); | ||
| return ((msg?.result as { tools?: { name: string }[] })?.tools ?? []).map(t => t.name); |
There was a problem hiding this comment.
tests/e2e/helpers/mcpHelper.ts:106 — listTools() will return an empty array if the server responds with a JSON-RPC error (since result is undefined), which can mask real MCP failures and make test output misleading.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in f3722c7 — listTools() now checks msg.error and throws with the JSON-RPC error code and message instead of silently returning [].
tests/e2e/helpers/mcpHelper.ts
Outdated
| let stderr = ''; | ||
| proc.stdout.on('data', (chunk: Buffer) => (stdout += chunk.toString())); | ||
| proc.stderr.on('data', (chunk: Buffer) => (stderr += chunk.toString())); | ||
| proc.on('close', () => { |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in f3722c7 — getMcpConfig() now checks exit code on close and rejects with stderr when non-zero, before attempting JSON parse.
tests/e2e/helpers/mcpHelper.ts
Outdated
| if (!resolved) { | ||
| resolved = true; | ||
| clearTimeout(timer); | ||
| resolve(undefined); |
There was a problem hiding this comment.
tests/e2e/helpers/mcpHelper.ts:249 — If stdout closes before the targetId response arrives, sendRequests() resolves undefined without surfacing stderr, which loses useful CLI error details compared to the timeout path.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Fixed in f3722c7 — sendRequests() now rejects with stderr on early process close instead of resolving undefined, consistent with the timeout path.
Addresses review feedback: - listTools() now throws on JSON-RPC error responses instead of silently returning an empty array - getMcpConfig() checks process exit code and rejects on non-zero - sendRequests() rejects with stderr when process exits before the expected response arrives, instead of resolving undefined Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds E2E test infrastructure for exercising the GitLens MCP server via the gk CLI, providing helpers to locate the CLI/IPC discovery file and a Playwright fixture that exposes a ready-to-use McpClient to tests.
Changes:
- Introduces
McpClient(stdio JSON-RPC) plus helper utilities for locatinggkand the IPC discovery file. - Adds
mcpTestPlaywright fixture that creates/configures anMcpClientfor the current VS Code worker.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/e2e/helpers/mcpHelper.ts | New MCP client + helpers (findGkCliFromArgs, findLatestIpcFile, waitForCliInstall) used by E2E tests. |
| tests/e2e/fixtures/mcp.ts | New Playwright fixture (mcpTest) that wires helpers into a per-test mcpClient. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/e2e/helpers/mcpHelper.ts
Outdated
| export function findLatestIpcFile(): string | undefined { | ||
| const tmpDir = path.join(os.tmpdir(), 'gitkraken', 'gitlens'); | ||
| if (!existsSync(tmpDir)) return undefined; | ||
|
|
||
| const candidates = readdirSync(tmpDir) | ||
| .filter(f => f.startsWith('gitlens-ipc-server-') && f.endsWith('.json')) | ||
| .map(f => { | ||
| const fullPath = path.join(tmpDir, f); | ||
| try { | ||
| return { fullPath: fullPath, mtime: statSync(fullPath).mtime }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }) | ||
| .filter((x): x is { fullPath: string; mtime: Date } => x != null) | ||
| .sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); | ||
|
|
||
| for (const { fullPath } of candidates) { | ||
| try { | ||
| const data = JSON.parse(readFileSync(fullPath, 'utf8')) as { pid: number }; | ||
| process.kill(data.pid, 0); // throws if process is dead | ||
| return fullPath; | ||
| } catch { | ||
| // dead process or unreadable file — skip | ||
| } | ||
| } | ||
| return undefined; |
There was a problem hiding this comment.
findLatestIpcFile() selects the newest live discovery file in the shared temp directory, which can pick up a different Playwright worker's VS Code session when tests run with fullyParallel and multiple workers. This can make GK_GL_PATH point at the wrong IPC server and cause cross-test flakiness. Consider accepting an expected pid (e.g. vscode.electron.app.process().pid / parsing the pid from the filename) and filtering candidates to that pid before choosing the latest file.
There was a problem hiding this comment.
Fixed in ec99b5f — findLatestIpcFile() now accepts an optional vscodePid parameter and filters candidates by the pid in the filename (gitlens-ipc-server-{pid}-{port}.json). The fixture passes vscode.electron.app.process().pid to scope the lookup.
| proc.on('close', (code: number | null) => { | ||
| // Strip "checking for updates..." noise before parsing | ||
| const clean = stdout.replace(/checking for updates\.\.\./gi, '').trim(); | ||
| if (code != null && code !== 0) { | ||
| reject( | ||
| new Error( | ||
| `gk mcp config exited with code ${code}: ${clean.slice(0, 200)}${stderr ? `\nstderr: ${stderr}` : ''}`, | ||
| ), | ||
| ); | ||
| return; | ||
| } | ||
| try { | ||
| resolve(JSON.parse(clean) as McpConfigResult); | ||
| } catch { | ||
| reject( | ||
| new Error( | ||
| `gk mcp config returned non-JSON: ${clean.slice(0, 200)}${stderr ? `\nstderr: ${stderr}` : ''}`, | ||
| ), | ||
| ); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The close handler only receives code and treats code == null as success, which can misreport process termination-by-signal as a JSON parse failure (or even a success if stdout happens to be parseable). Consider capturing the signal argument from the close event and rejecting when code == null (or signal != null), including stderr/signal in the error message.
There was a problem hiding this comment.
Acknowledged — in practice, signal-based termination only happens via our own timeout handler (which already rejects with a clear message). If the process is killed externally, code=null means stdout is typically empty and the JSON parse error is descriptive enough. Keeping this as-is to avoid over-engineering the test helper, but noted for future reference.
| const gkPath = findGkCliFromArgs(vscode.electron.args); | ||
| await waitForCliInstall(gkPath); | ||
| const ipcFilePath = findLatestIpcFile(); | ||
| if (ipcFilePath == null) { | ||
| console.warn('[mcpTest] No live IPC file found — GK_GL_PATH will not be set'); | ||
| } | ||
| const client = new McpClient(gkPath, ipcFilePath); |
There was a problem hiding this comment.
Since Playwright runs E2E with multiple workers, calling findLatestIpcFile() without scoping can select another worker’s discovery file. Consider passing the current worker’s VS Code pid (e.g. vscode.electron.app.process().pid) into the helper so GK_GL_PATH is derived from the correct session.
There was a problem hiding this comment.
Fixed in ec99b5f — the fixture now passes vscode.electron.app.process().pid to findLatestIpcFile(), scoping the IPC discovery to the current worker's VS Code instance.
findLatestIpcFile() now accepts an optional vscodePid parameter
and filters discovery files by the pid embedded in the filename
(gitlens-ipc-server-{pid}-{port}.json). This prevents parallel
Playwright workers from picking up each other's IPC files.
The mcp fixture passes vscode.electron.app.process().pid to
scope the lookup to the current worker's VS Code instance.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const payload = `${messages.map(m => JSON.stringify(m)).join('\n')}\n`; | ||
| proc.stdin.write(payload); | ||
| proc.stdin.end(); | ||
| }); |
There was a problem hiding this comment.
sendRequests() ends stdin immediately after writing the initial payload, but the elicitation/create handler later attempts to write a cancel response to proc.stdin. This will trigger ERR_STREAM_WRITE_AFTER_END (or silently fail), which can make tests flaky/hang because elicitation requests won't be cancelled. Keep stdin open until the target response is received / the process exits (e.g., don’t call stdin.end() up-front; instead end/kill as part of the resolve/timeout/close paths).
There was a problem hiding this comment.
Fixed in 729cf09 — stdin.end() moved from the initial payload write to the resolve/timeout/close paths. stdin now stays open so elicitation cancel responses can be written back to the server.
Moves proc.stdin.end() from the initial payload write to the resolve/timeout/close paths. Previously stdin was closed immediately after sending requests, which would cause ERR_STREAM_WRITE_AFTER_END if the server sent an elicitation/create request requiring a cancel response, potentially hanging tests until timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/e2e/helpers/mcpHelper.ts
Outdated
| process.kill(data.pid, 0); // throws if process is dead | ||
| return fullPath; | ||
| } catch { | ||
| // dead process or unreadable file — skip |
There was a problem hiding this comment.
findLatestIpcFile() uses process.kill(pid, 0) to validate the PID is alive, but it treats any thrown error as “dead process”. On some platforms/environments kill(pid, 0) can throw EPERM even when the process exists (insufficient permission), which would incorrectly skip a valid discovery file and leave GK_GL_PATH unset. Consider handling EPERM as “process exists” (only treat ESRCH as dead) so the lookup is robust.
| process.kill(data.pid, 0); // throws if process is dead | |
| return fullPath; | |
| } catch { | |
| // dead process or unreadable file — skip | |
| try { | |
| process.kill(data.pid, 0); // throws if process is dead or inaccessible | |
| return fullPath; | |
| } catch (error) { | |
| const code = (error as { code?: string }).code; | |
| if (code === 'EPERM') { | |
| // Process exists but cannot be signaled by this process. | |
| return fullPath; | |
| } | |
| if (code === 'ESRCH') { | |
| // Process is gone — skip this discovery file. | |
| continue; | |
| } | |
| throw error; | |
| } | |
| } catch { | |
| // unreadable or invalid file — skip |
There was a problem hiding this comment.
Fixed in 888ab96 — now handles EPERM as "process exists" and only treats ESRCH as dead. Prevents incorrectly skipping valid discovery files on platforms where kill(pid, 0) throws EPERM for accessible-but-not-signalable processes.
| return new Promise((resolve, reject) => { | ||
| const proc = spawn(this.gkPath, args, { | ||
| env: this.buildEnv(), | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| }); |
There was a problem hiding this comment.
getMcpConfig() spawns gk mcp config ... without any timeout/kill path. If the CLI hangs (e.g., waiting on network/update checks), the E2E run can stall indefinitely. Consider adding a timeout (similar to sendRequests) that terminates the subprocess and rejects with a message that includes captured stderr/stdout.
There was a problem hiding this comment.
Fixed in 888ab96 — getMcpConfig() now has a 30s timeout (configurable via parameter) that kills the subprocess and rejects with stderr if the CLI hangs.
tests/e2e/helpers/mcpHelper.ts
Outdated
| rl.on('close', () => { | ||
| if (!resolved) { | ||
| resolved = true; | ||
| clearTimeout(timer); | ||
| reject( | ||
| new Error( | ||
| `McpClient: process exited before response id=${targetId} was received${stderr ? `\nstderr: ${stderr}` : ''}`, | ||
| ), | ||
| ); | ||
| } | ||
| }); |
There was a problem hiding this comment.
In sendRequests(), failures are detected via the readline interface's close event, but the error message can’t include the child process exit code/signal because proc.on('close', (code, signal) => ...) isn’t tracked. Capturing code/signal (and ideally rejecting based on the process close event) would make debugging flaky CLI failures much easier.
There was a problem hiding this comment.
Fixed in 888ab96 — sendRequests() now captures code and signal from the process close event and includes them in the early-close error message (e.g. process exited (code=1) or process exited (signal=SIGTERM)).
- findLatestIpcFile: handles EPERM from kill(pid,0) as "process exists" instead of incorrectly skipping valid discovery files - getMcpConfig: adds 30s timeout with proc.kill() to prevent indefinite hangs if CLI stalls on network/update checks - sendRequests: captures process exit code/signal and includes them in the early-close error message for easier debugging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Auto-cancel any elicitation requests so tests don't hang | ||
| if (msg.method === 'elicitation/create') { | ||
| proc.stdin.write( | ||
| `${JSON.stringify({ jsonrpc: '2.0', id: msg.id, result: { action: 'cancel' } })}\n`, | ||
| ); | ||
| return; | ||
| } |
There was a problem hiding this comment.
In sendRequests(), the line handler will still process messages after resolved is set and after proc.stdin.end() has been called (resolve/timeout paths). If an elicitation/create arrives during this window, proc.stdin.write(...) can throw ERR_STREAM_WRITE_AFTER_END and crash/flakify the test run. Add a if (resolved) return; guard (or at least guard the elicitation branch) and/or close the readline interface / remove listeners before ending stdin.
There was a problem hiding this comment.
Fixed in 9df709f — added if (settled) return guard at the top of the line handler, and rl.close() is now called before stdin.end() on the resolve path. Elicitation writes can no longer reach a closed stdin.
tests/e2e/helpers/mcpHelper.ts
Outdated
| settled = true; | ||
| clearTimeout(timer); | ||
| // Strip "checking for updates..." noise before parsing | ||
| const clean = stdout.replace(/checking for updates\.\.\./gi, '').trim(); |
There was a problem hiding this comment.
getMcpConfig() strips update-check noise using /checking for updates\.\.\./gi, which is stricter than the production logic (e.g. src/env/node/gk/mcp/integrationBase.ts uses /checking for updates.../i). If the CLI output changes slightly (ellipsis char, extra punctuation/whitespace), this helper may fail to strip it and JSON parsing will break. Consider reusing/mirroring the same more-permissive pattern as the extension code so E2E behavior matches production.
| const clean = stdout.replace(/checking for updates\.\.\./gi, '').trim(); | |
| const clean = stdout.replace(/checking for updates(?:\s*(?:…|\.{3,}))?/gi, '').trim(); |
There was a problem hiding this comment.
Fixed in 9df709f — regex changed from /checking for updates\.\.\.\./gi to /checking for updates.../gi to match the permissive pattern used in production (integrationBase.ts).
| export { expect } from '@playwright/test'; | ||
| export type { McpMessage, McpClient } from '../helpers/mcpHelper.js'; | ||
|
|
There was a problem hiding this comment.
The PR description mentions exporting McpConfigResult for use in tests, but this fixture only re-exports McpMessage/McpClient types. If the intent is for tests to import everything MCP-related from ../fixtures/mcp.js, also export McpConfigResult here (or update the description/usage docs to point at helpers/mcpHelper).
There was a problem hiding this comment.
Fixed in 9df709f — McpConfigResult is now re-exported from the fixture alongside McpMessage and McpClient.
There was a problem hiding this comment.
Fixed in 9df709f — McpConfigResult is now re-exported from the fixture alongside McpMessage and McpClient.
- Removes dead code fallback in callTool() — sendRequests now always
resolves or rejects, never returns undefined
- Replaces competing rl.on('close') + proc.on('close') with a single
proc.on('close') handler that has access to exit code/signal directly
- Adds 'if (settled) return' guard in line handler to prevent
ERR_STREAM_WRITE_AFTER_END on elicitation after resolve/timeout
- Closes readline interface before ending stdin on resolve path
- Aligns "checking for updates" regex with production code (permissive
dot matching instead of escaped literal dots)
- Exports McpConfigResult type from mcp fixture for test convenience
- Renames 'resolved' flag to 'settled' for consistency with getMcpConfig
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/e2e/helpers/mcpHelper.ts
Outdated
| import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import process from 'node:process'; |
There was a problem hiding this comment.
In the E2E test code, other files consistently import process as a namespace (e.g. import * as process from 'node:process';). Here import process from 'node:process' is inconsistent and can behave differently depending on module interop settings. Please switch to the namespace import for consistency with the rest of tests/e2e (e.g. tests/e2e/baseTest.ts, tests/e2e/playwright.config.ts).
| import process from 'node:process'; | |
| import * as process from 'node:process'; |
There was a problem hiding this comment.
Fixed in 6961a9c — switched to import * as process from 'node:process' for consistency with the rest of tests/e2e.
Aligns with the rest of tests/e2e (baseTest.ts, playwright.config.ts) which use `import * as process from 'node:process'`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
McpClient— minimal stdio JSON-RPC 2.0 client that spawnsgk mcpas a fresh process per call, handlesinitialize/tools/list/tools/call, and auto-cancelselicitation/createrequestsfindGkCliFromArgs— derives gk proxy path from--user-data-direlectron launch arg (cross-platform:gk.exeon Windows,gkon macOS/Linux)findLatestIpcFile— locates the live IPC discovery file viaos.tmpdir(), filters dead processes viaprocess.kill(pid, 0)waitForCliInstall— polls for gk binary appearance (~5–6 s on first run, 30 s timeout)mcpTestPlaywright fixture — wires all helpers into a ready-to-useMcpClientscoped to the worker VS Code instance; warns if IPC file is not foundMcpConfigResulttype for use in test filesgksubprocess calls and included in timeout/error messages for easier debuggingTest plan
pnpm run lint— no errorspnpm exec tsc --noEmit— no errorsmcp.smoke.test.ts) will use this fixture in the follow-up PR🤖 Generated with Claude Code