// FILE: node/packages/mcp-server/src/mcp.bugs.test.ts import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { resolve, join } from "node:path"; import { tmpdir } from "node:os"; import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs"; import { DocumentObject, RedlineEngine } from "@adeu/core"; import { startTestServer, type TestServer } from "./test-rpc.js"; describe("Resolved Bugs MCP Server Verification", () => { let server: TestServer; let cleanDocPath: string; let dirtyDocPath: string; beforeAll(async () => { const fixturePath = resolve( __dirname, "../../../../shared/fixtures/golden.docx", ); cleanDocPath = join(tmpdir(), `adeu_clean_${Date.now()}.docx`); dirtyDocPath = join(tmpdir(), `adeu_dirty_${Date.now()}.docx`); const fixtureBuf = readFileSync(fixturePath); writeFileSync(cleanDocPath, fixtureBuf); const doc = await DocumentObject.load(fixtureBuf); const engine = new RedlineEngine(doc, "Reviewer"); engine.process_batch([ { type: "modify", target_text: "document", new_text: "dirty modified document", }, ]); writeFileSync(dirtyDocPath, await doc.save()); server = await startTestServer("mcp-bugs"); }); afterAll(() => { server?.stop(); if (existsSync(cleanDocPath)) unlinkSync(cleanDocPath); if (existsSync(dirtyDocPath)) unlinkSync(dirtyDocPath); }); function sendRpc(method: string, params: any): Promise { return server.rpc(method, params); } it("BUG-5: Rejects empty changes array early without writing files", async () => { const outPath = join(tmpdir(), `adeu_out_${Date.now()}.docx`); if (existsSync(outPath)) unlinkSync(outPath); const res = await sendRpc( "tools/call", { name: "process_document_batch", arguments: { reasoning: "test", original_docx_path: cleanDocPath, author_name: "Agent", changes: [], output_path: outPath, }, }, 101, ); expect(res.result.content[0].text).toBe("Error: No changes provided."); expect(existsSync(outPath)).toBe(false); // Proves no-op }); it("BUG-9: diff_docx_files tool respects compare_clean parameter", async () => { // 1. compare_clean = true (Default) -> Should output clean text comparison const resClean = await sendRpc( "tools/call", { name: "diff_docx_files", arguments: { reasoning: "test", original_path: cleanDocPath, modified_path: dirtyDocPath, compare_clean: true, }, }, 102, ); const cleanText = resClean.result.content[0].text; expect(cleanText).toContain("dirty modified"); expect(cleanText).not.toContain("{++"); // No CriticMarkup // 2. compare_clean = false -> Should output raw CriticMarkup comparison const resRaw = await sendRpc( "tools/call", { name: "diff_docx_files", arguments: { reasoning: "test", original_path: cleanDocPath, modified_path: dirtyDocPath, compare_clean: false, }, }, 103, ); const rawText = resRaw.result.content[0].text; // The zero-width insertion anchors right before "document", where it // coalesces with the pre-existing {++golden ++} run in the raw // projection ({++golden dirty modified ++}). A standalone // "{++dirty modified ++}" block only existed while the insertion-anchor // bug dropped the text at paragraph start, so assert the raw-markup // intent instead of that exact placement. expect(rawText).toContain("dirty modified"); expect(rawText).toContain("{++"); // CriticMarkup IS present in raw mode expect(rawText).toContain("[Chg:5 insert] Reviewer"); }); it("BUG-10: Traps ENOENT and returns clean File Not Found errors", async () => { const res = await sendRpc( "tools/call", { name: "read_docx", arguments: { reasoning: "test", file_path: join(tmpdir(), "DEF_DOES_NOT_EXIST.docx"), }, }, 104, ); expect(res.result.isError).toBe(true); expect(res.result.content[0].text).toContain("file not found:"); // Lean agent error: no CLI install blurb, no raw node error. expect(res.result.content[0].text).not.toContain("uv tool install adeu"); expect(res.result.content[0].text).not.toContain( "sandboxed/containerized environment", ); expect(res.result.content[0].text).not.toContain("ENOENT"); // Should surface an available-files listing to enable one-turn self-correction. expect(res.result.content[0].text).toMatch( /available files:|no \.docx files found/, ); }); it("Double-Serialization: process_document_batch fails with TypeError when changes array contains double-serialized JSON strings", async () => { const res = await sendRpc( "tools/call", { name: "process_document_batch", arguments: { reasoning: "test", original_docx_path: cleanDocPath, author_name: "Agent", changes: [ JSON.stringify({ type: "modify", target_text: "document", new_text: "clean document", }), ], }, }, 105, ); // On unpatched code, the tool catches the TypeError and returns it inside a standard MCP error response, causing this test to fail. // On patched code, the tool successfully parses and applies the double-serialized JSON strings, returning a successful response. expect(res.result.isError).toBeUndefined(); expect(res.result.content[0].text).toContain("Batch complete."); }); it("Unparseable String: process_document_batch gracefully rejects raw strings instead of crashing", async () => { const res = await sendRpc( "tools/call", { name: "process_document_batch", arguments: { reasoning: "test", original_docx_path: cleanDocPath, author_name: "Agent", changes: [ "modify document to be clean document", // Raw unparseable string ], }, }, 110, ); expect(res.result.isError).toBe(true); expect(res.result.content[0].text).toContain( "Batch rejected. Some edits failed validation", ); expect(res.result.content[0].text).toContain("Invalid change format"); expect(res.result.content[0].text).toContain("received a primitive string"); }); it("BUG-12: Accepts stringified numbers for numeric arguments without Zod validation errors", async () => { const res = await sendRpc( "tools/call", { name: "read_docx", arguments: { reasoning: "test", file_path: cleanDocPath, page: "1", outline_max_level: "3", }, }, 106, ); // If Zod validation failed, we would get an error payload back or `res.error` // from the MCP protocol (error code -32602). expect(res.error).toBeUndefined(); expect(res.result).toBeDefined(); expect(res.result.isError).toBeUndefined(); expect(res.result.content[0].text).toContain("golden"); }); it("BUG-read_docx-negative-page: Rejects negative page numbers with out of range error", async () => { const res = await sendRpc( "tools/call", { name: "read_docx", arguments: { reasoning: "test", file_path: cleanDocPath, page: -1, }, }, 107, ); expect(res.error).toBeUndefined(); expect(res.result).toBeDefined(); expect(res.result.isError).toBe(true); expect(res.result.content[0].text).toContain("Invalid page parameter: '-1'"); }); it("creates non-existent output parent directory when saving batch results", async () => { const nestedOutPath = join( tmpdir(), `adeu_test_dir_${Date.now()}`, "sub_dir", `adeu_out_${Date.now()}.docx`, ); const res = await sendRpc( "tools/call", { name: "process_document_batch", arguments: { reasoning: "test non-existent directory creation", original_docx_path: cleanDocPath, author_name: "Agent", output_path: nestedOutPath, changes: [ { type: "modify", target_text: "document", new_text: "updated document", }, ], }, }, 115, ); expect(res.result.isError).toBeUndefined(); expect(res.result.content[0].text).toContain("Batch complete."); expect(existsSync(nestedOutPath)).toBe(true); if (existsSync(nestedOutPath)) unlinkSync(nestedOutPath); }); });