import { describe, expect, it } from "bun:test";
import * as limits from "./limits";
import { parse } from "./parse";
import { serialize } from "./serialize";
import { validate } from "./validate";
const ID = "01K0N4TR8K7JGM4R1J7PW4R8YJ";
const ID2 = "01K0N4V4E7Y6P4MJ5WD8XZF3B2";
const ID3 = "01K0N4W3B7P27CBAEC7A8C8WEA";
function codes(source: string): string[] {
let result = validate(parse(source));
return result.ok ? [] : result.issues.map(issue => issue.code);
}
function accepts(source: string): void {
let result = validate(parse(source));
if (!result.ok) {
throw new Error(
`expected valid, got: ${result.issues.map(i => `${i.code} @ ${i.path}`).join(", ")}`,
);
}
}
/** Canonical output must be a fixed point: serialising it again changes nothing. */
function canonical(source: string): string {
let once = serialize(parse(source));
expect(serialize(parse(once))).toBe(once);
return once;
}
describe("markdown baseline", () => {
it("round-trips core constructs to canonical form", () => {
expect(canonical("# Title")).toBe("# Title\n");
expect(canonical("Some *italic* and **bold**.")).toBe("Some _italic_ and **bold**.\n");
expect(canonical("~~gone~~")).toBe("~~gone~~\n");
expect(canonical("`code`")).toBe("`code`\n");
expect(canonical("* one\n* two")).toBe("- one\n- two\n");
expect(canonical("> quoted")).toBe("> quoted\n");
expect(canonical("***")).toBe("---\n");
});
it("preserves task lists", () => {
expect(canonical("- [ ] todo\n- [x] done")).toBe(
"* [ ] todo\n* [x] done\n".replace(/\*/g, "-"),
);
});
it("preserves tables with alignment", () => {
let out = canonical("| Name | Status |\n| :--- | -----: |\n| API | Ready |");
expect(out).toContain("| :--- | -----: |");
expect(out).toContain("| API | Ready |");
});
it("preserves fenced code with language", () => {
expect(canonical("```ts\nlet a = 1;\n```")).toBe("```ts\nlet a = 1;\n```\n");
});
it("preserves math", () => {
expect(canonical("$a + b$")).toBe("$a + b$\n");
expect(canonical("$$\na + b\n$$")).toBe("$$\na + b\n$$\n");
});
it("preserves mermaid as a code fence", () => {
expect(canonical("```mermaid\ngraph TD;\nA-->B;\n```")).toContain("```mermaid");
});
it("preserves footnotes with ULID identifiers", () => {
let source = `Claim.[^${ID}]\n\n[^${ID}]: Because.\n`;
accepts(source);
expect(canonical(source)).toContain(`[^${ID}]`);
});
});
describe("security boundary", () => {
it("cannot represent ESM imports or exports", () => {
// The mdxjs extension is not enabled, so these never become executable
// nodes — they degrade to inert prose.
for (let source of ["import x from './y'", "export const a = 1"]) {
let tree = parse(source);
expect(tree.children.map(child => child.type)).toEqual(["paragraph"]);
accepts(source);
}
});
it("rejects raw HTML tags as unknown components", () => {
// `mdxMd()` disables HTML, so angle brackets are JSX and fall to the allowlist.
expect(codes("
text
")).toContain("unknown-component");
expect(codes("Inline x here.")).toContain("unknown-component");
});
it("rejects unknown components", () => {
expect(codes(``)).toContain("unknown-component");
});
it("rejects JSX fragments", () => {
expect(codes("<>text>")).toContain("fragment");
});
it("rejects spread and expression attributes", () => {
expect(codes(``)).toContain("spread-attribute");
expect(codes(`hi`))
.toContain("expression-attribute");
});
it("rejects active link protocols", () => {
expect(codes("[x](javascript:alert(1))")).toContain("bad-link-protocol");
expect(codes("[x](data:text/html;base64,PHA+)")).toContain("bad-link-protocol");
});
it("allows https, mailto and repo-relative paths", () => {
accepts("[x](https://example.com)");
accepts("[x](mailto:a@b.com)");
accepts("[x](&plan.mdx)");
accepts("[x](src/index.ts)");
});
it("requires images to be absolute https URLs", () => {
expect(codes("")).toContain("bad-image-protocol");
expect(codes("")).toContain("bad-image-protocol");
// Unlike a link, a relative image has nothing to resolve against.
expect(codes("")).toContain("bad-image");
accepts("");
});
it("rejects frontmatter", () => {
// Without the frontmatter extension this parses as a thematic break plus
// text, never as metadata — assert it cannot smuggle a yaml node.
let tree = parse("---\ntitle: x\n---\n\nbody");
expect(JSON.stringify(tree)).not.toContain('"yaml"');
});
});
describe("serialization safety", () => {
// `mdxMd()` makes every `<` a potential JSX tag, so prose containing angle
// brackets is only safe because the serializer escapes it. Editor content is
// built as a tree and written out, so this is the property that guarantees
// anything we emit can be read back.
it("escapes angle brackets so emitted prose always re-parses", () => {
let samples = [
"x <3 y",
"",
"a < b",
" 3 && 2 < 4",
"use here",
];
for (let value of samples) {
let source = serialize({
type: "root",
children: [{ type: "paragraph", children: [{ type: "text", value }] }],
});
let tree = parse(source);
let paragraph = tree.children[0];
expect(paragraph?.type).toBe("paragraph");
expect(paragraph?.type === "paragraph" && paragraph.children[0]).toMatchObject({
type: "text",
value,
});
}
});
});
describe("components", () => {
it("accepts a questionnaire", () => {
accepts(
`\n`
+ `\n`
+ `\n`
+ `\n`
+ ``,
);
});
it("accepts an accepted comment thread", () => {
accepts(
`\n`
+ `\n`
+ ``,
);
});
it("requires a decision to say who accepted it and when", () => {
expect(codes(``))
.toContain("missing-attribute");
});
it("keeps a note out of the prose", () => {
expect(codes(``)).toContain("bad-parent");
});
/** A note's text is an attribute, so children are a hand-edit gone wrong. */
it("rejects a note written with its text between the tags", () => {
expect(
codes(
`\n`
+ `\n\nToo long.\n\n\n`
+ ``,
),
).toContain("unexpected-children");
});
it("requires ULID ids", () => {
expect(codes(`x`)).toContain("bad-id");
});
/** The dialect carries no versions; one written by hand is just unknown. */
it("rejects a version attribute", () => {
expect(codes(`x`))
.toContain("unknown-attribute");
});
it("enforces required and unknown attributes", () => {
expect(codes(`x`)).toContain("missing-attribute");
expect(codes(`y`))
.toContain("unknown-attribute");
});
it("enforces enum values", () => {
expect(codes(`x`))
.toContain("bad-attribute-value");
});
it("enforces parent constraints", () => {
expect(codes(`x`)).toContain("bad-parent");
expect(codes(``)).toContain("bad-parent");
});
it("enforces allowed children", () => {
expect(codes(`\n\ntext\n\n`)).toContain("unexpected-child");
expect(codes(``)).toContain("missing-children");
expect(codes(``))
.toContain("unexpected-children");
});
it("forbids recursive layout components", () => {
let nested = `\n`
+ `\n`
+ `\n`
+ `\n\nx\n\n\n`
+ `\n`
+ `\n`
+ ``;
expect(codes(nested)).toContain("bad-nesting");
});
it("rejects a flow component used inline", () => {
expect(codes(`text x more`))
.toContain("wrong-kind");
});
it("round-trips components canonically", () => {
let source = `\n\nBody.\n\n`;
let out = canonical(source);
expect(out).toContain(` {
it("rejects oversized source", () => {
let result = validate(parse("text"), { bytes: 512 * 1024 });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.issues[0]!.code).toBe("source-too-large");
});
it("rejects tables beyond the column limit", () => {
let header = `| ${Array.from({ length: 25 }, (_, i) => `c${i}`).join(" | ")} |`;
let divider = `| ${Array.from({ length: 25 }, () => "-").join(" | ")} |`;
expect(codes(`${header}\n${divider}`)).toContain("table-too-wide");
});
it("rejects tables beyond the row limit", () => {
// The counterpart to the width limit, and the one the editor's rails
// hold themselves to: a plan that reaches either is refused by the
// server, which cannot undo the update and so rotates the room's epoch.
let rows = Array.from({ length: limits.MAX_TABLE_ROWS + 1 }, () => "| x |").join("\n");
expect(codes(`| c |\n| - |\n${rows}`)).toContain("table-too-tall");
});
it("rejects excessive nesting", () => {
let deep = Array.from({ length: 24 }, (_, i) => `${" ".repeat(i)}- level`).join("\n");
expect(codes(deep)).toContain("too-deep");
});
it("rejects over-long attribute text", () => {
let long = "x".repeat(120);
expect(codes(`y`))
.toContain("attribute-too-long");
});
});
describe("diagnostics", () => {
it("reports every issue with a structural path", () => {
let result = validate(parse(`x`));
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.issues.length).toBeGreaterThan(1);
for (let issue of result.issues) expect(issue.path).toStartWith("root");
});
it("does not echo document prose", () => {
let secret = "hunter2-should-not-leak";
let result = validate(parse(`${secret}`));
expect(result.ok).toBe(false);
if (result.ok) return;
expect(JSON.stringify(result.issues)).not.toContain(secret);
});
});