How to Test Applications Integrated With the Copilot SDK
Test applications integrated with the Copilot SDK using the Node.js mock MCP server and YAML snapshot testing to validate JSON-RPC interactions without requiring the real Copilot CLI binary.
The github/copilot-sdk repository provides a unified testing strategy for applications that integrate with the Copilot SDK. Because the SDK communicates via a JSON-RPC protocol, the repository ships with a language-agnostic test harness that mocks the Copilot CLI server, enabling deterministic unit and integration tests across Node.js, Python, Go, .NET, Java, and Rust.
Understanding the Test Harness Architecture
The Copilot SDK testing strategy centers on a record-and-replay mechanism that intercepts JSON-RPC traffic between your application and a mock server.
The Mock MCP Server
At the core of the testing infrastructure is the Node.js-based mock server defined in test/harness/server.ts. This lightweight implementation simulates the Copilot CLI's Multi-Modal Copilot (MCP) protocol, responding to standard JSON-RPC methods such as initialize, tools/list, and session/send.
When you invoke startMockMcpServer(), the harness spawns a mock server on a random free port. Your test then injects this URL into the SDK client via the endpoint parameter or environment variables, isolating your application from real Copilot services.
Snapshot-Driven Validation
The harness implements record-and-replay testing through YAML snapshot files stored in test/snapshots/. These files define sequences of expected JSON-RPC requests and responses. During test execution, the harness compares actual traffic against these snapshots using assertSnapshotMatches, failing the test if any request parameter or response body diverges from the recorded baseline.
This approach ensures that breaking changes in the SDK's public contract are caught immediately. For example, the snapshot at test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml validates that custom tools execute correctly during parallel invocations.
Cross-Language Reusability
All language-specific SDKs reuse the same Node.js harness, creating a uniform testing experience across the monorepo. Whether you are testing a TypeScript application with Vitest or a Python module with pytest, the underlying mock server and snapshot format remain identical. This consistency is enforced by each SDK's CI script, which boots the harness (cd test/harness && npm ci) before executing language-specific tests.
Setting Up the Test Harness
Before writing tests, install the harness dependencies once:
cd test/harness && npm ci
This installs the mock server and utilities required by all subsequent tests. The harness exports configuration from test/harness/vitest.config.ts for Node.js projects, while other languages import the running server via setup hooks.
Writing Tests for Copilot SDK Applications
The following patterns demonstrate how to test applications integrated with the Copilot SDK using the provided harness.
Basic Session Testing
Create a test that initializes a session, sends a prompt, and verifies the mock server's response:
import { createCopilotSession } from '@github/copilot-sdk';
import { expect } from 'vitest';
import { startMockMcpServer } from '../test/harness/server';
test('session sends a prompt and receives a response', async () => {
// Start the mock MCP server
const { url, close } = await startMockMcpServer();
// Create a client pointing at the mock server
const session = await createCopilotSession({
endpoint: url,
tools: [],
permissionHandler: () => true,
});
// Send a prompt
const reply = await session.send({ prompt: 'Hello, world!' });
// Verify the mock response
expect(reply).toContain('Hello, world!');
// Cleanup
await session.dispose();
await close();
});
This pattern appears throughout the Node.js test suite and demonstrates the standard lifecycle: spawn server, inject endpoint, execute logic, assert results, dispose resources.
Testing Custom Tools and Permission Handlers
Validate that your permission handling logic correctly allows or denies tool execution:
import { createCopilotSession, defineTool } from '@github/copilot-sdk';
import { expect } from 'vitest';
import { startMockMcpServer } from '../test/harness/server';
test('custom tool execution respects permission handler', async () => {
const { url, close } = await startMockMcpServer();
const myTool = defineTool({
name: 'echo',
description: 'Echoes back the supplied string',
parameters: { type: 'object', properties: { text: { type: 'string' } } },
handler: async ({ text }) => ({ result: text }),
});
const session = await createCopilotSession({
endpoint: url,
tools: [myTool],
permissionHandler: () => false, // Deny all tools
});
const result = await session.send({ prompt: 'Please echo "test".' });
expect(result).toMatch(/denied/i);
await session.dispose();
await close();
});
The snapshot test test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml provides the baseline for this validation, ensuring consistent behavior across SDK versions.
Using Utility Helpers
The harness provides platform-agnostic utilities in test/harness/util.ts for asynchronous test patterns:
import { iife, sleep, ShellConfig } from '../test/harness/util';
iife(async () => {
console.log('Running on shell:', ShellConfig.bash.shellToolName);
await sleep(500);
});
These helpers include iife for immediately invoked async functions, sleep for timed delays, and ShellConfig for cross-platform shell compatibility.
Running Tests Across Language SDKs
Each language SDK follows the same workflow: boot the harness, then execute the native test suite.
Node.js / TypeScript:
cd nodejs && npm test
Python:
cd python && uv run pytest
Go:
cd go && go test ./...
.NET:
cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj
Java:
cd java && mvn verify -Dskip.test.harness=true
As documented in CONTRIBUTING.md (lines 42-90), these commands share the underlying mock server implementation, ensuring consistent behavior validation across all supported languages.
Key Testing Files and References
Understanding the repository structure helps navigate the testing codebase:
test/harness/server.ts– Mock MCP server implementing JSON-RPC methodstest/harness/util.ts– Async helpers (iife,sleep) and shell configurationtest/harness/vitest.config.ts– Vitest configuration for Node.js teststest/snapshots/**/*.yaml– Record-and-replay test definitionsCONTRIBUTING.md– Cross-language testing workflow documentationdocs/troubleshooting/mcp-debugging.md– Manual MCP server debugging guide
Summary
- Test applications integrated with the Copilot SDK using the Node.js mock MCP server to eliminate dependencies on the real Copilot CLI binary.
- The harness validates JSON-RPC interactions through YAML snapshots stored in
test/snapshots/, enabling record-and-replay testing. - All language SDKs (Node, Python, Go, .NET, Java, Rust) reuse the same harness, ensuring uniform testing across the monorepo.
- Use
startMockMcpServer()to spawn isolated mock servers, inject the endpoint URL intocreateCopilotSession(), and assert against expected snapshots. - Run language-specific commands (
npm test,uv run pytest,go test ./...) after booting the harness withcd test/harness && npm ci.
Frequently Asked Questions
Do I need the Copilot CLI installed to run tests?
No. The test/harness/server.ts mock server implements the complete JSON-RPC protocol required by the SDK, allowing you to test applications integrated with the Copilot SDK without installing or launching the real Copilot CLI binary. This isolation makes tests fast, deterministic, and CI-friendly.
How do I add new test cases for custom behavior?
Create a new YAML snapshot file in test/snapshots/ describing the expected JSON-RPC request and response sequence. The harness will automatically compare live traffic against your snapshot using assertSnapshotMatches. You can also write imperative tests using startMockMcpServer() directly for dynamic behavior that doesn't fit the snapshot pattern.
Can I use the test harness for end-to-end testing?
Yes. While primarily designed for unit and integration tests, the harness supports end-to-end scenarios by simulating complex interaction patterns such as permission handler rejections, tool-call errors, and custom agent reloads. The snapshot files under test/snapshots/hooks/ and test/snapshots/elicitation/ demonstrate advanced use cases.
Why does the harness use YAML snapshots instead of JSON?
YAML snapshots provide better human readability for the complex nested structures of JSON-RPC messages while maintaining strict parsing compatibility. The format allows developers to easily review diffs when the protocol changes, and the assertSnapshotMatches utility processes YAML files to validate exact request/response matching.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →