Files
gitmost/packages/mcp/test/unit/node-ops.test.mjs
claude code agent 227 f80276d41a refactor(review): address PR #185 review (lease leak, tests, changelog, jsonb seam)
8-point multi-aspect review of the batch PR; security/regressions were clean.

1. Lease leak: the #180 reorder moved `toolsFor` (which leases external MCP
   clients, refCount+1) ahead of buildSystemPrompt + forUser, but the only
   release (closeExternalClients) was bound to the streamText callbacks. A throw
   in between leaked the lease (refCount stuck, undici sockets held until
   restart). Define closeExternalClients right after the lease and wrap
   buildSystemPrompt+forUser in try/catch that closes-then-rethrows.
2. Cover the patch_node/delete_node dup-id refusal (#159 #6): extract the guard
   into a pure `assertUnambiguousMatch` (node-ops) and unit-test 0/1/>1.
3. Regress the body-before-title order (#159 #10): mock-HTTP test (collab fails
   fast against a server with no WS upgrade) asserts /pages/update (title) is
   NEVER posted when the body write fails — for updatePage AND updatePageJson.
4. CHANGELOG [Unreleased]: #180, #168 (Added); #163 (Fixed).
5. Add the missing en-US i18n keys (Back to references / {{label}}).
6. Drop the duplicate content/empty/blank cases in ai-chat.prompt.spec.ts (they
   repeat the buildMcpToolingBlock unit tests); keep only sandwich placement +
   both-safety-copies.
7. CI Postgres pg16 -> pg18 (match docker-compose).
8. jsonb decode seam: shared `parseJsonbValue(value, guard)` in database/utils.ts
   holds the legacy double-encoding self-heal in one place; parseToolAllowlist /
   parseModelConfig keep only a type-guard.

Verified: server build + 124 unit + 15 integration; mcp 311; prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 11:36:01 +03:00

520 lines
16 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
blockPlainText,
replaceNodeById,
deleteNodeById,
assertUnambiguousMatch,
insertNodeRelative,
} from "../../build/lib/node-ops.js";
// ProseMirror builders. Blocks carry a stable id in attrs.id.
const textNode = (text) => ({ type: "text", text });
const para = (id, ...children) => ({
type: "paragraph",
attrs: { id },
content: children,
});
const doc = (...children) => ({ type: "doc", content: children });
const snapshot = (v) => JSON.parse(JSON.stringify(v));
// A callout / table-cell wraps its children in `content`, just like any other
// block, so recursion reaches a paragraph nested inside it.
const callout = (id, ...children) => ({
type: "callout",
attrs: { id, type: "info" },
content: children,
});
const tableDoc = (innerPara) =>
doc({
type: "table",
attrs: { id: "table-1" },
content: [
{
type: "tableRow",
attrs: { id: "row-1" },
content: [
{
type: "tableCell",
attrs: { id: "cell-1" },
content: [innerPara],
},
],
},
],
});
// ---------------------------------------------------------------------------
// blockPlainText
// ---------------------------------------------------------------------------
test("blockPlainText concatenates nested text", () => {
const node = {
type: "callout",
content: [
para("p-1", textNode("Hello "), textNode("world")),
para("p-2", textNode("!")),
],
};
assert.equal(blockPlainText(node), "Hello world!");
});
test("blockPlainText returns '' for nullish / non-object", () => {
assert.equal(blockPlainText(null), "");
assert.equal(blockPlainText(undefined), "");
assert.equal(blockPlainText("just a string"), "");
});
test("blockPlainText reads a bare text node", () => {
assert.equal(blockPlainText(textNode("solo")), "solo");
});
// ---------------------------------------------------------------------------
// replaceNodeById
// ---------------------------------------------------------------------------
test("replaceNodeById replaces the matching block and leaves others, count===1", () => {
const input = doc(
para("p-1", textNode("one")),
para("p-2", textNode("two")),
para("p-3", textNode("three")),
);
const newNode = para("p-2", textNode("REPLACED"));
const { doc: out, replaced } = replaceNodeById(input, "p-2", newNode);
assert.equal(replaced, 1);
// Target replaced.
assert.equal(out.content[1].content[0].text, "REPLACED");
// Siblings untouched (text and ids).
assert.equal(out.content[0].content[0].text, "one");
assert.equal(out.content[2].content[0].text, "three");
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "p-2", "p-3"],
);
});
test("replaceNodeById on no-match returns replaced===0 and does not throw", () => {
const input = doc(para("p-1", textNode("one")));
const { doc: out, replaced } = replaceNodeById(
input,
"missing",
para("x", textNode("x")),
);
assert.equal(replaced, 0);
// Document content is preserved.
assert.equal(out.content[0].content[0].text, "one");
});
test("replaceNodeById replaces EVERY node sharing the id (count reflects all)", () => {
const input = doc(
para("dup", textNode("a")),
para("dup", textNode("b")),
para("keep", textNode("c")),
);
const { doc: out, replaced } = replaceNodeById(
input,
"dup",
para("dup", textNode("NEW")),
);
assert.equal(replaced, 2);
assert.equal(out.content[0].content[0].text, "NEW");
assert.equal(out.content[1].content[0].text, "NEW");
assert.equal(out.content[2].content[0].text, "c");
// The two replacements must not share a reference (deep clone per match).
assert.notEqual(out.content[0], out.content[1]);
});
test("replaceNodeById reaches a node nested inside a callout", () => {
const input = doc(callout("c-1", para("inner", textNode("old"))));
const { doc: out, replaced } = replaceNodeById(
input,
"inner",
para("inner", textNode("new")),
);
assert.equal(replaced, 1);
assert.equal(out.content[0].content[0].content[0].text, "new");
});
test("replaceNodeById reaches a node nested inside a table cell", () => {
const input = tableDoc(para("deep", textNode("before")));
const { doc: out, replaced } = replaceNodeById(
input,
"deep",
para("deep", textNode("after")),
);
assert.equal(replaced, 1);
const cellPara = out.content[0].content[0].content[0].content[0];
assert.equal(cellPara.content[0].text, "after");
});
test("replaceNodeById does NOT mutate input (deep-equal snapshot)", () => {
const input = doc(
para("p-1", textNode("one")),
callout("c-1", para("inner", textNode("old"))),
);
const snap = snapshot(input);
const { doc: out } = replaceNodeById(
input,
"inner",
para("inner", textNode("changed")),
);
assert.deepEqual(input, snap);
assert.notEqual(out, input);
});
// ---------------------------------------------------------------------------
// deleteNodeById
// ---------------------------------------------------------------------------
test("deleteNodeById removes the block and reports deleted===1", () => {
const input = doc(
para("p-1", textNode("one")),
para("p-2", textNode("two")),
para("p-3", textNode("three")),
);
const { doc: out, deleted } = deleteNodeById(input, "p-2");
assert.equal(deleted, 1);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "p-3"],
);
});
test("deleteNodeById on no-match returns deleted===0 and leaves content", () => {
const input = doc(para("p-1", textNode("one")));
const { doc: out, deleted } = deleteNodeById(input, "missing");
assert.equal(deleted, 0);
assert.equal(out.content.length, 1);
});
test("deleteNodeById removes a node nested inside a callout", () => {
const input = doc(
callout("c-1", para("inner", textNode("x")), para("keep", textNode("y"))),
);
const { doc: out, deleted } = deleteNodeById(input, "inner");
assert.equal(deleted, 1);
assert.deepEqual(
out.content[0].content.map((n) => n.attrs.id),
["keep"],
);
});
test("deleteNodeById removes EVERY node sharing the id", () => {
const input = doc(
para("dup", textNode("a")),
para("keep", textNode("b")),
para("dup", textNode("c")),
);
const { doc: out, deleted } = deleteNodeById(input, "dup");
assert.equal(deleted, 2);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["keep"],
);
});
test("deleteNodeById does NOT mutate input (deep-equal snapshot)", () => {
const input = doc(para("p-1", textNode("one")), para("p-2", textNode("two")));
const snap = snapshot(input);
const { doc: out } = deleteNodeById(input, "p-2");
assert.deepEqual(input, snap);
assert.notEqual(out, input);
});
// ---------------------------------------------------------------------------
// insertNodeRelative
// ---------------------------------------------------------------------------
test("insertNodeRelative before by anchorNodeId", () => {
const input = doc(para("p-1", textNode("one")), para("p-2", textNode("two")));
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "before",
anchorNodeId: "p-2",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative after by anchorNodeId", () => {
const input = doc(para("p-1", textNode("one")), para("p-2", textNode("two")));
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorNodeId: "p-1",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative before/after by anchorNodeId reaches a nested sibling", () => {
const input = doc(
callout("c-1", para("a", textNode("a")), para("b", textNode("b"))),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorNodeId: "a",
});
assert.equal(inserted, true);
// Inserted as a sibling inside the callout's content array.
assert.deepEqual(
out.content[0].content.map((n) => n.attrs.id),
["a", "new", "b"],
);
});
test("insertNodeRelative before by anchorText (top-level)", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "beta",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative after by anchorText (top-level)", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorText: "alpha",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative anchorText scans TOP-LEVEL blocks via recursive plain text", () => {
// anchorText matches the FIRST top-level block whose (recursive) blockPlainText
// includes the string. "deeptext" lives nested in a top-level callout, so the
// callout itself is the matched top-level block and the node lands as its
// sibling at the top level (not inside the callout).
const input = doc(
callout("c-1", para("inner", textNode("deeptext"))),
para("p-2", textNode("tail")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorText: "deeptext",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["c-1", "new", "p-2"],
);
});
test("insertNodeRelative anchorText does NOT match text only present below top level when no top-level block contains it", () => {
// The only block whose plain text includes "lonely" is a paragraph nested two
// levels deep, but the top-level scan still sees it through the callout's
// recursive plain text. To prove the scan is TOP-LEVEL (parent-array) only,
// assert the insertion happens at the top level beside the callout, never
// inside it.
const input = doc(callout("c-1", para("inner", textNode("lonely word"))));
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "lonely",
});
assert.equal(inserted, true);
// Inserted at the top level (siblings of the callout), not into the callout.
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["new", "c-1"],
);
// The callout's own children are untouched.
assert.deepEqual(
out.content[1].content.map((n) => n.attrs.id),
["inner"],
);
});
test("insertNodeRelative append pushes the node at the end of top-level content", () => {
const input = doc(para("p-1", textNode("one")), para("p-2", textNode("two")));
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "append",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "p-2", "new"],
);
});
test("insertNodeRelative inserted===false when anchorNodeId missing", () => {
const input = doc(para("p-1", textNode("one")));
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorNodeId: "nope",
});
assert.equal(inserted, false);
assert.deepEqual(out, input);
});
test("insertNodeRelative inserted===false when anchorText missing", () => {
const input = doc(para("p-1", textNode("one")));
const node = para("new", textNode("NEW"));
const { inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "nomatch",
});
assert.equal(inserted, false);
});
test("insertNodeRelative does NOT mutate input (deep-equal snapshot)", () => {
const input = doc(para("p-1", textNode("one")), para("p-2", textNode("two")));
const snap = snapshot(input);
const node = para("new", textNode("NEW"));
const { doc: out } = insertNodeRelative(input, node, {
position: "after",
anchorNodeId: "p-1",
});
assert.deepEqual(input, snap);
assert.notEqual(out, input);
});
// ---------------------------------------------------------------------------
// anchorText markdown-normalization fallback (locating only)
// ---------------------------------------------------------------------------
test("insertNodeRelative before by markdown-wrapped anchorText matches the plain block", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
// "**beta**" has no verbatim match; the stripped "beta" matches "p-2".
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "**beta**",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative after by markdown-wrapped anchorText matches the plain block", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorText: "**alpha**",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative anchorText with markdown AND a trailing emoji matches the plain block", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "**beta** ✨",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative exact anchorText still wins (no normalization)", () => {
// A block literally contains "a*b"; the exact anchor must match it directly.
const input = doc(
para("p-1", textNode("a*b")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { doc: out, inserted } = insertNodeRelative(input, node, {
position: "after",
anchorText: "a*b",
});
assert.equal(inserted, true);
assert.deepEqual(
out.content.map((n) => n.attrs.id),
["p-1", "new", "p-2"],
);
});
test("insertNodeRelative truly-missing anchor still returns inserted:false", () => {
const input = doc(
para("p-1", textNode("alpha")),
para("p-2", textNode("beta")),
);
const node = para("new", textNode("NEW"));
const { inserted } = insertNodeRelative(input, node, {
position: "before",
anchorText: "**gamma**",
});
assert.equal(inserted, false);
});
// assertUnambiguousMatch (#159, #185 review pt 2): the patch_node/delete_node
// guard. Docmost duplicates block ids on copy/paste, so a write by id that
// matches >1 node must be REFUSED (the caller already skipped the write for any
// count !== 1; this reports the error). The duplicate COUNT itself is covered by
// the replaceNodeById/deleteNodeById tests above (count===2 for a 2-dup doc).
test("assertUnambiguousMatch: count 0 throws 'no node found'", () => {
assert.throws(
() => assertUnambiguousMatch("patch_node", "replace", 0, "n1", "p1"),
/patch_node: no node with id "n1" found on page p1/,
);
});
test("assertUnambiguousMatch: count > 1 refuses with an 'ambiguous' error", () => {
assert.throws(
() => assertUnambiguousMatch("patch_node", "replace", 2, "dup", "p1"),
/ambiguous.*Refusing to replace all of them; nothing was changed/,
);
assert.throws(
() => assertUnambiguousMatch("delete_node", "delete", 3, "dup", "p1"),
/ambiguous.*Refusing to delete all of them; nothing was changed/,
);
});
test("assertUnambiguousMatch: exactly one match does NOT throw", () => {
assert.doesNotThrow(() =>
assertUnambiguousMatch("patch_node", "replace", 1, "n1", "p1"),
);
assert.doesNotThrow(() =>
assertUnambiguousMatch("delete_node", "delete", 1, "n1", "p1"),
);
});