Skip to content

Commit f43353a

Browse files
authored
test: add MCP tools/call end-to-end test (#137)
Extend the managed install e2e suite with a test that calls doc_set through the MCP server and verifies the file was modified on disk. This closes the loop: install binary, start MCP, initialize, list tools, call a tool, and confirm the side effect. Also bumps the --version timeout from 5s to 15s to avoid intermittent failures on cold binary starts. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent 1e946a0 commit f43353a

1 file changed

Lines changed: 65 additions & 1 deletion

File tree

test/unit/patchloomCli.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,7 @@ describe("managed install end-to-end MCP", { timeout: 120_000 }, async () => {
647647

648648
// Verify the binary is executable
649649
test("managed install produces a runnable binary", async () => {
650-
const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 5000 });
650+
const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 15000 });
651651
const output = `${stdout}${stderr}`.trim();
652652
const version = parsePatchloomVersion(output);
653653
assert.ok(version, `should parse version from managed binary: ${output}`);
@@ -757,6 +757,70 @@ describe("managed install end-to-end MCP", { timeout: 120_000 }, async () => {
757757
}
758758
});
759759

760+
test("MCP tools/call modifies a file on disk", async () => {
761+
// Create a temp directory with a JSON file to edit via MCP.
762+
// The MCP server resolves paths relative to its cwd, so we
763+
// spawn the server inside the temp directory and use a relative path.
764+
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "patchloom-mcp-call-"));
765+
await fs.writeFile(path.join(workDir, "config.json"), '{"port": 3000}\n', "utf8");
766+
767+
const child = execFile(binaryPath, ["mcp-server"], { timeout: 15000, cwd: workDir });
768+
let stdout = "";
769+
child.stdout!.on("data", (data: Buffer) => { stdout += data.toString(); });
770+
771+
// Initialize
772+
child.stdin!.write(JSON.stringify({
773+
jsonrpc: "2.0", id: 1, method: "initialize",
774+
params: {
775+
protocolVersion: "2024-11-05", capabilities: {},
776+
clientInfo: { name: "e2e-test", version: "0.0.1" }
777+
}
778+
}) + "\n");
779+
780+
let deadline = Date.now() + 10000;
781+
while (!stdout.includes('"id":1') && Date.now() < deadline) {
782+
await new Promise((resolve) => setTimeout(resolve, 100));
783+
}
784+
785+
child.stdin!.write(JSON.stringify({
786+
jsonrpc: "2.0", method: "notifications/initialized"
787+
}) + "\n");
788+
789+
// Call doc_set to change port from 3000 to 8080 (relative path)
790+
child.stdin!.write(JSON.stringify({
791+
jsonrpc: "2.0", id: 3, method: "tools/call",
792+
params: {
793+
name: "doc_set",
794+
arguments: { path: "config.json", selector: "port", value: 8080 }
795+
}
796+
}) + "\n");
797+
798+
deadline = Date.now() + 10000;
799+
while (!stdout.includes('"id":3') && Date.now() < deadline) {
800+
await new Promise((resolve) => setTimeout(resolve, 100));
801+
}
802+
803+
child.kill();
804+
805+
// Verify the tool call succeeded
806+
const lines = stdout.trim().split("\n");
807+
const callLine = lines.find((line) => line.includes('"id":3'));
808+
assert.ok(callLine, "should have a tools/call response");
809+
const callResponse = JSON.parse(callLine) as Record<string, unknown>;
810+
assert.equal(callResponse.jsonrpc, "2.0");
811+
assert.equal(callResponse.id, 3);
812+
const callResult = callResponse.result as Record<string, unknown>;
813+
assert.ok(callResult, "tools/call should return a result (not an error)");
814+
assert.ok(!callResult.isError,
815+
`tools/call should not be an error: ${JSON.stringify(callResult)}`);
816+
817+
// Verify the file was actually modified on disk
818+
const content = JSON.parse(await fs.readFile(path.join(workDir, "config.json"), "utf8")) as Record<string, unknown>;
819+
assert.equal(content.port, 8080, "doc_set should have changed port to 8080");
820+
821+
await fs.rm(workDir, { recursive: true, force: true });
822+
});
823+
760824
// Cleanup
761825
test("cleanup managed install temp directory", async () => {
762826
await fs.rm(installDir, { recursive: true, force: true });

0 commit comments

Comments
 (0)