// FILE: node/packages/mcp-server/src/repro.qa_2026_07_23.finalize.test.ts /** * MCP-server repro test for ADEU-MCP-QA-REPORT.md (2026-07-23, black-box QA * of v1.29.0+4bb70f9). Finding covered here: * * F9 (medium) Cell anchors are regenerated by finalize_document. * * Why this is a DOCUMENTATION test, not an engine-stability test: the anchor * churn is DELIBERATE. {#cell:...} anchors are projections of w14:paraId * attributes (core ingest.ts / mapper.ts), and finalize_document strips * w14:paraId / w14:textId as metadata in EVERY sanitize mode via * strip_para_ids (core sanitize/transforms.ts, wired unconditionally into * the common-transform block of sanitize/core.ts) — with a dedicated unit * test (sanitize/sanitize.test.ts "should strip w14:paraId and w14:textId") * pinning that behavior. After finalize, a later read regenerates fallback * ids for empty cells (hash-based, ingest.ts/mapper.ts) — hence the QA * observation of {#cell:D4F421F4} becoming {#cell:D2F41ECE}. * * The bug is therefore the unqualified tool-description claim: the published * description says the {#cell:} anchors are "stable (Word-assigned) and * safe to reuse across reads" (PROCESS_BATCH_OPERATIONS_DESC in * src/index.ts; the QA report attributed the sentence to read_docx, but the * string ships on the process_document_batch tool) without mentioning that * finalize/sanitize regenerates/invalidates them. This test fetches the live * tool list from the REAL compiled MCP server over stdio JSON-RPC and * requires every description that makes the stability claim to qualify it. * A fix that instead drops the stability claim entirely also passes. * * Every test in this file is written test-first: it fails on current main * and passes once the finding is fixed. */ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { spawn, ChildProcess } from "node:child_process"; import { resolve } from "node:path"; import { existsSync } from "node:fs"; describe("QA 2026-07-23 F9 — anchor-stability claim must be qualified for finalize", () => { let serverProc: ChildProcess; beforeAll(() => { const serverPath = resolve(__dirname, "../dist/index.js"); if (!existsSync(serverPath)) { throw new Error( "MCP server not built. Run 'npm run build' before tests.", ); } serverProc = spawn("node", [serverPath]); }); afterAll(() => { if (serverProc && !serverProc.killed) serverProc.kill(); }); function sendRpc(method: string, params: any, id: number): Promise { return new Promise((res, rej) => { const timeout = setTimeout(() => rej(new Error("RPC Timeout")), 8000); let buffer = ""; const listener = (data: Buffer) => { buffer += data.toString(); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.trim().startsWith("{")) continue; try { const parsed = JSON.parse(line); if (parsed.id === id) { clearTimeout(timeout); serverProc.stdout?.removeListener("data", listener); res(parsed); } } catch { /* ignore partial chunks */ } } }; serverProc.stdout?.on("data", listener); serverProc.stdin?.write( JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n", ); }); } it("tool descriptions claiming {#cell:} anchors are stable also mention finalize/sanitize invalidation", async () => { const r = await sendRpc("tools/list", {}, 901); const tools = (r.result?.tools || []) as Array<{ name: string; description?: string; }>; expect(tools.length).toBeGreaterThan(0); // Every published description paragraph that claims the {#cell:} // anchors are stable / safe to reuse must qualify the claim IN THAT // PARAGRAPH: finalize_document (deliberately) strips the backing // w14:paraId attributes, so anchors do NOT survive finalize/sanitize, // and an unqualified claim teaches agents to reuse dead anchors across // a finalize boundary. The check is paragraph-scoped because the same // description legitimately uses the word "Finalize" in an unrelated // sentence ("'accept'/'reject': Finalize or revert a tracked change"), // which must not count as the qualification — the QA fix is "one // clarifying sentence" next to the stability claim (or dropping the // claim, which also passes). const offenders: string[] = []; for (const t of tools) { const desc = t.description || ""; for (const paragraph of desc.split(/\n\s*\n/)) { const claimsStability = paragraph.includes("{#cell:") && /stable|safe to reuse/i.test(paragraph); if (claimsStability && !/finaliz|sanitiz/i.test(paragraph)) { offenders.push(t.name); } } } expect( offenders, `These tools claim {#cell:} anchors are stable/safe to reuse without ` + `qualifying that finalize/sanitize regenerates or invalidates them: ` + `[${offenders.join(", ")}]. Either qualify the stability claim or ` + `remove it.`, ).toEqual([]); }, 20000); });