Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:
- name: Run tests
run: npm test

- name: Verify subpath exports (build + dist-grep + self-ref probes)
run: npm run build:verify

visual:
name: 'Visual Tests'
if: "!contains(github.event.head_commit.message, 'SKIP CI')"
Expand Down
7 changes: 6 additions & 1 deletion __tests__/lib/mdxish/magic-blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1956,12 +1956,17 @@ asdasdasd
expect(() => mdxish(md)).not.toThrow();
});

// CPU-bound robustness guard (no latency assertion): parsing 10k chars
// takes ~2s in isolation but can exceed the 5s default under full-suite
// parallel contention in CI. A generous explicit timeout still catches a
// genuine hang/catastrophic-backtracking regression while tolerating
// worker contention.
it('should handle very long content without crashing', () => {
const longContent = 'a'.repeat(10000);
const md = `[block:callout]{"body":"${longContent}"}[/block]`;

expect(() => mdxish(md)).not.toThrow();
});
}, 30000);

it('should handle multiple blocks in sequence', () => {
const md = '[block:callout]{}[/block][block:callout]{}[/block]';
Expand Down
396 changes: 396 additions & 0 deletions __tests__/lib/render-diff/differ.test.ts

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions __tests__/lib/render-diff/no-engine-imports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Source-level direct forbidden-import guard for lib/render-diff/.
*
* SCOPE: DIRECT imports only — not a transitive proof.
*
* This test regex-scans every .ts file under lib/render-diff/ for direct
* import statements that would couple the render-diff module to engine code
* (lib/compile, lib/mdxish, lib/renderMdxish, lib/run, lib/exports, or
* processor/*).
*
* Intentional limitation: a transitive chain such as
* lib/render-diff/foo.ts → ../shared-helper.ts → ../compile.ts
* would not be caught here because the regex only inspects files under
* lib/render-diff/ directly.
*
* Transitive leaks are defended at the bundle layer by two complementary
* mechanisms:
* - scripts/verify-render-diff.cjs scans dist/render-diff.node.js for
* engine identifiers regardless of how they entered the bundle.
* - the bundlewatch budget catches the ~10x size regression that any
* engine inclusion would produce.
*/
import { readdirSync, readFileSync } from 'fs';
import path from 'path';

// ------------------------------------------------------------------ constants

const RENDER_DIFF_DIR = path.join(__dirname, '..', '..', '..', 'lib', 'render-diff');

/**
* Forbidden direct import patterns.
*
* Each regex matches a TypeScript import statement whose module specifier
* resolves to one of the five engine modules (or the processor/ directory).
* Patterns use single-quote OR double-quote to be robust to code style.
*
* Note: patterns are intentionally anchored to the relative import prefix
* `../` so they only match direct engine imports, not substrings inside
* identifiers or comments.
*/
const FORBIDDEN_PATTERNS: RegExp[] = [
/from ['"]\.\.\/compile['"]/,
/from ['"]\.\.\/mdxish['"]/,
/from ['"]\.\.\/renderMdxish['"]/,
/from ['"]\.\.\/run['"]/,
/from ['"]\.\.\/exports['"]/,
/from ['"]\.\.\/\.\.\/processor\//,
];

// ------------------------------------------------------------------ helpers

function collectTsFiles(dir: string): string[] {
return readdirSync(dir)
.filter(name => name.endsWith('.ts') || name.endsWith('.tsx'))
.map(name => path.join(dir, name));
}

// ------------------------------------------------------------------ tests

describe('lib/render-diff — direct forbidden import guard', () => {
const files = collectTsFiles(RENDER_DIFF_DIR);
const repoRoot = path.join(__dirname, '..', '..', '..');

it('finds TypeScript source files under lib/render-diff/', () => {
expect(files.length).toBeGreaterThan(0);
});

it.each(files.map(filePath => [filePath.replace(`${repoRoot}/`, ''), filePath]))(
'%s contains no direct engine imports',
(_relativePath, filePath) => {
const source = readFileSync(filePath, 'utf-8');

FORBIDDEN_PATTERNS.forEach(pattern => {
expect(source).not.toMatch(pattern);
});
},
);
});
138 changes: 138 additions & 0 deletions __tests__/lib/render-fixture/loadFixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

import { loadFixture } from '../../../lib/render-fixture/loadFixture';

const FIXTURES_DIR = join(__dirname, '..', '..', 'regression', 'fixtures');

describe('loadFixture', () => {
describe('convergent fixture', () => {
it('returns a non-empty body string', () => {
const { body } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(typeof body).toBe('string');
expect(body.length).toBeGreaterThan(0);
});

it('returns ctx.variables with defaults array and user object', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(ctx.variables).toBeDefined();
expect(Array.isArray(ctx.variables.defaults)).toBe(true);
expect(typeof ctx.variables.user).toBe('object');
});

it('returns ctx.glossary as an array', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(Array.isArray(ctx.glossary)).toBe(true);
});

it('loads components from components/ directory', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(ctx.components.length).toBeGreaterThan(0);
});

it('registers Note.mdx as tag "Note" with non-empty source', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
const noteBlock = ctx.components.find(c => c.tag === 'Note');
expect(noteBlock).toBeDefined();
expect(typeof noteBlock!.source).toBe('string');
expect(noteBlock!.source.length).toBeGreaterThan(0);
});
});

describe('divergent fixture', () => {
it('returns a non-empty body string', () => {
const { body } = loadFixture(join(FIXTURES_DIR, 'divergent'));
expect(typeof body).toBe('string');
expect(body.length).toBeGreaterThan(0);
});

it('returns ctx.variables with defaults array and user object', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'divergent'));
expect(ctx.variables).toBeDefined();
expect(Array.isArray(ctx.variables.defaults)).toBe(true);
expect(typeof ctx.variables.user).toBe('object');
});

it('returns ctx.glossary as an array', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'divergent'));
expect(Array.isArray(ctx.glossary)).toBe(true);
});

it('loads with ctx.components as [] when no components/ directory exists', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'divergent'));
expect(ctx.components).toStrictEqual([]);
});
});

describe('context.json defaults', () => {
it('uses { defaults: [], user: {} } when variables key is fully specified as such', () => {
// Both checked-in fixtures specify variables fully — this case exercises
// the "specified-as-empty" path. The truly-absent path is covered below
// via an inline temp dir.
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(ctx.variables.defaults).toStrictEqual([]);
expect(ctx.variables.user).toStrictEqual({});
});

it('defaults glossary to [] when absent', () => {
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(ctx.glossary).toStrictEqual([]);
});

describe('with an inline temp fixture (true absent-key coverage)', () => {
// Create a minimal fixture where context.json OMITS the variables key
// entirely, to exercise the runtime fallback in loadFixture rather than
// the "specified-as-empty" path. Same pattern is used for the partial
// case (variables.defaults present, user omitted).
let tmpRoot: string;

beforeAll(() => {
tmpRoot = mkdtempSync(join(tmpdir(), 'loadFixture-defaults-'));

// Variant A: variables key entirely absent.
const absentDir = join(tmpRoot, 'absent');
mkdirSync(absentDir);
writeFileSync(join(absentDir, 'body.md'), '# absent\n');
writeFileSync(join(absentDir, 'context.json'), JSON.stringify({}));

// Variant B: variables.defaults present, variables.user omitted.
const partialDir = join(tmpRoot, 'partial');
mkdirSync(partialDir);
writeFileSync(join(partialDir, 'body.md'), '# partial\n');
writeFileSync(
join(partialDir, 'context.json'),
JSON.stringify({
variables: { defaults: [{ name: 'foo', default: 'bar' }] },
}),
);
});

afterAll(() => {
rmSync(tmpRoot, { recursive: true, force: true });
});

it('defaults variables to { defaults: [], user: {} } when the key is absent', () => {
const { ctx } = loadFixture(join(tmpRoot, 'absent'));
expect(ctx.variables).toStrictEqual({ defaults: [], user: {} });
});

it('defaults variables.user to {} when only variables.defaults is provided', () => {
const { ctx } = loadFixture(join(tmpRoot, 'partial'));
expect(ctx.variables.defaults).toStrictEqual([{ name: 'foo', default: 'bar' }]);
// user was undefined under the old `??` fallback when `defaults` was
// specified but `user` was omitted; both must always be populated.
expect(ctx.variables.user).toStrictEqual({});
});
});
});

describe('components/ filtering', () => {
it('ignores non-.mdx files in components/', () => {
// The convergent fixture only has Note.mdx — exactly 1 component
const { ctx } = loadFixture(join(FIXTURES_DIR, 'convergent'));
expect(ctx.components).toHaveLength(1);
expect(ctx.components[0].tag).toBe('Note');
});
});
});
32 changes: 32 additions & 0 deletions __tests__/lib/render-fixture/renderFixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';

import { renderFixture } from '../../../lib/render-fixture/renderFixture';

describe('renderFixture smoke test', () => {
const body = '# Hello\n\nSmoke test prose.';
const ctx = {
variables: { defaults: [], user: {} },
glossary: [],
components: [],
};

it('MDX engine returns non-empty HTML containing the heading text', () => {
const result = renderFixture(body, ctx, 'mdx');
expect(result.error).toBeNull();
expect(result.html.length).toBeGreaterThan(0);
expect(result.html).toContain('Hello');
});

it('MDXish engine returns non-empty HTML containing the heading text', () => {
const result = renderFixture(body, ctx, 'mdxish');
expect(result.error).toBeNull();
expect(result.html.length).toBeGreaterThan(0);
expect(result.html).toContain('Hello');
});

it('determinism: MDX engine returns byte-identical HTML on repeated calls', () => {
const result1 = renderFixture(body, ctx, 'mdx');
const result2 = renderFixture(body, ctx, 'mdx');
expect(result1.html).toBe(result2.html);
});
});
Loading