Files
gitmost/apps/server/src/integrations/import/services/import.service.processMarkdown.spec.ts
T
agent_coder 3163f50c98 fix(mcp): route #502 extension flags per write tool; add server import flag (#502 review)
Internal review found the two layered-extension flags landed in the SHARED
markdownToProseMirrorCanonical wrapper, which mis-covered two tools. Move the
decision to each caller by the tool's semantics.

BLOCKER 1 — createPage was NOT covered. createPage POSTs to the server
/pages/import endpoint, which imported with DEFAULTS (math + fuzzy autolink ON),
so an agent's `$x=1$` still became a formula and `www.host` still autolinked.
Add an optional `disableMarkdownExtensions` multipart field to /pages/import
(default OFF, so human file uploads keep math ON); import.controller reads it,
import.service.importPage/processMarkdown thread it into markdownToProseMirror.
MCP createPage sends it true.

BLOCKER 2 — import_page_markdown was wrongly disabled. It goes through the same
wrapper, so hardcoding parseMath:false degraded an exported `$x^2$` to literal
text on re-import, breaking the #328 lossless export→import pair. The wrapper no
longer hardcodes the flags: it takes them from the caller and DEFAULTS to the
package importer defaults (extensions ON). Callers now set them explicitly:
  - updatePageMarkdown (updatePageContentRealtime) -> OFF
  - patch_node/insert_node (importMarkdownFragment) -> OFF (unchanged)
  - import_page_markdown -> DEFAULTS (math ON) — #328 round-trip restored

Tests: rewrite the mcp write test around the corrected per-caller semantics
(agent-write OFF, import_page_markdown DEFAULTS incl. the REAL #328 round-trip);
add a collab-backed wiring test driving client.updatePage (OFF) and
client.importPageMarkdown (DEFAULTS) end-to-end; add a createPage multipart-flag
test; add a server import.service.processMarkdown spec (flag OFF -> literal / no
autolink, default -> math ON for human uploads); reword the prosemirror-markdown
round-trip test to its package-default scope. Mutation-verified both caller
wirings (flip updatePageMarkdown -> defaults reddens the OFF wiring test; make
the wrapper default OFF reddens the #328 round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 06:58:48 +03:00

68 lines
2.8 KiB
TypeScript

// Importing ImportService transitively loads import-formatter.ts, which imports
// the ESM-only @sindresorhus/slugify (not in jest's transform allowlist). It is
// irrelevant to this path, so mock it to keep the module graph loadable (mirrors
// the sibling import.service specs).
jest.mock('@sindresorhus/slugify', () => ({
__esModule: true,
default: (input: string) => String(input),
}));
import { ImportService } from './import.service';
// #502 BLOCKER 1: the server markdown import path (`/pages/import`) now accepts an
// optional `disableMarkdownExtensions` flag threaded into `processMarkdown`.
// - MCP agent `createPage` sends it TRUE -> extensions OFF (a `$…$` config span
// stays literal, a schemeless `www.host` is not autolinked).
// - a HUMAN file upload omits it (default FALSE) -> extensions ON, so a real
// `$x^2$` still becomes a formula (human imports unaffected).
// `processMarkdown` only uses the imported converter (no injected deps on this
// path), so the service is constructed with null deps for this focused unit test.
function makeService(): ImportService {
return new ImportService(null as any, null as any, null as any, null as any);
}
function findAll(node: any, type: string, acc: any[] = []): any[] {
if (!node || typeof node !== 'object') return acc;
if (node.type === type) acc.push(node);
if (Array.isArray(node.content)) for (const c of node.content) findAll(c, type, acc);
return acc;
}
function hasLink(node: any): boolean {
return findAll(node, 'text').some((t: any) =>
t.marks?.some((m: any) => m.type === 'link'),
);
}
describe('ImportService.processMarkdown — #502 disableMarkdownExtensions', () => {
it('disableMarkdownExtensions=true (MCP createPage): `$…$` stays literal, no math', async () => {
const svc = makeService();
const doc = await svc.processMarkdown('export A=$FOO and B=$BAR done', true);
expect(findAll(doc, 'mathInline')).toHaveLength(0);
});
it('disableMarkdownExtensions=true: a schemeless www is NOT autolinked', async () => {
const svc = makeService();
const doc = await svc.processMarkdown('see www.example.com here', true);
expect(hasLink(doc)).toBe(false);
});
it('disableMarkdownExtensions=true: an explicit https:// STILL links', async () => {
const svc = makeService();
const doc = await svc.processMarkdown('see https://example.com here', true);
expect(hasLink(doc)).toBe(true);
});
it('DEFAULT (human upload): a real `$x^2$` DOES become a math node', async () => {
const svc = makeService();
const doc = await svc.processMarkdown('$x^2$');
expect(findAll(doc, 'mathInline')).toHaveLength(1);
});
it('DEFAULT (human upload): a schemeless www IS autolinked', async () => {
const svc = makeService();
const doc = await svc.processMarkdown('see www.example.com here');
expect(hasLink(doc)).toBe(true);
});
});