Files
pbakaus_impeccable/tests/framework-fixtures.test.mjs
T
Paul BakausandClaude Fable 5.1 bc45026706 Port: Fix Next.js 16 CSP and parent hook discovery (#710)
Upstream sha 672ca29642.

CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook
beside `middleware.*`, but only where it sits at a project root or its `src/`
directory: the scan root itself, or a nested directory carrying a Next
project marker (a `next.config.*`, an `app` / `pages` dir, or a `next`
dependency). A same-named helper elsewhere in the tree is not the framework
hook.

Context. `find_git_boundary_root` gives `resolve_project` a git-boundary
notion: an explicit target inside its own repository resolves against that
repository, and an external target resolves against its own root, so caller
context never leaks across the boundary. `hook_manifest_search_roots`
replaces the cwd/projectRoot/repoRoot triple with a walk up from the
project root that stops at the first git boundary, and each root's own hook
lifecycle config is honored before its manifest counts as coverage.

Verified against origin/main's JS: nine `detect-csp` placements and five
hook-discovery scenarios (enclosing harness root, that root disabled,
sibling target, nested git target, markerless nested git target) produce
identical output.

Oracle: five `csp-proxy-*` cases and five `context-hook-*` /
`context-markerless-nested-git-target` cases. Four route-target goldens were
re-recorded because #710 resolves a `/`-prefixed target outside the
workspace; each was cross-checked against origin/main, and
`surface-brief-write-route` has a DELTAS entry for the one wording
difference (an unwritable filesystem root).

`tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from
the merge importing the deleted `detectCsp`; it now drives `detect-csp`
through the binary like the rest of that file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-03 12:48:40 -07:00

304 lines
14 KiB
JavaScript

/**
* Drives the live-mode verbs of the engine binary against representative
* framework project shapes.
*
* Each fixture under tests/framework-fixtures/ is a small project tree with a
* fixture.json that declares the inject config + expected CSP and wrap
* outcomes. The harness copies the fixture into a tmp git repo, applies the
* fixture's gitignore, and runs `impeccable live-inject`, `live-wrap`, and
* `detect-csp` against it. Skips when no engine binary is available (see
* tests/lib/engine-bin.mjs).
*
* Run with: node --test tests/framework-fixtures.test.mjs
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { ENGINE_MISSING_MESSAGE, engineEnv, findEngineBinary } from './lib/engine-bin.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..');
const FIXTURES_DIR = join(__dirname, 'framework-fixtures');
const ENGINE_BIN = findEngineBinary();
function listFixtures() {
return readdirSync(FIXTURES_DIR, { withFileTypes: true })
.filter((e) => e.isDirectory())
.filter((e) => existsSync(join(FIXTURES_DIR, e.name, 'fixture.json')))
.map((e) => e.name);
}
/**
* Stage a fixture into a fresh tmp git repo. Returns the tmp path + loaded
* fixture.json. Caller is responsible for cleanup.
*/
function stageFixture(name) {
const fixtureRoot = join(FIXTURES_DIR, name);
const fixture = JSON.parse(readFileSync(join(fixtureRoot, 'fixture.json'), 'utf-8'));
const gitignore = readFileSync(join(fixtureRoot, 'gitignore.txt'), 'utf-8');
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-fixture-'));
cpSync(join(fixtureRoot, 'files'), tmp, { recursive: true });
writeFileSync(join(tmp, '.gitignore'), gitignore);
mkdirSync(join(tmp, '.impeccable', 'live'), { recursive: true });
writeFileSync(join(tmp, '.impeccable', 'live', 'config.json'), JSON.stringify(fixture.config));
// The AST scaffolder resolves the app's svelte compiler from the staged
// root; the runtime suite gets it from a real npm install, the static sweep
// links this repo's devDependency so svelte fixtures take the
// component-preview path here too.
const repoSvelte = join(REPO_ROOT, 'node_modules', 'svelte');
if (existsSync(repoSvelte) && !existsSync(join(tmp, 'node_modules', 'svelte'))) {
mkdirSync(join(tmp, 'node_modules'), { recursive: true });
try {
symlinkSync(repoSvelte, join(tmp, 'node_modules', 'svelte'), 'dir');
} catch {
// Windows without Developer Mode cannot symlink; copying is slower but
// keeps the suite runnable there.
cpSync(repoSvelte, join(tmp, 'node_modules', 'svelte'), { recursive: true });
}
}
execFileSync('git', ['init', '-q'], { cwd: tmp });
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmp });
execFileSync('git', ['config', 'user.name', 'Fixture'], { cwd: tmp });
execFileSync('git', ['add', '-A'], { cwd: tmp });
execFileSync('git', ['commit', '-qm', 'fixture'], { cwd: tmp });
return { tmp, fixture };
}
function runVerb(verb, args, opts = {}) {
try {
return execFileSync(ENGINE_BIN, [verb, ...args], {
encoding: 'utf-8',
cwd: opts.cwd,
env: engineEnv(ENGINE_BIN, opts.env || {}),
});
} catch (err) {
return { error: err.stdout?.toString() || '' , stderr: err.stderr?.toString() || '' };
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
for (const name of listFixtures()) {
describe(`fixture · ${name}`, { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
it('loads fixture.json and has expected tree', () => {
const { tmp, fixture } = stageFixture(name);
try {
assert.ok(fixture.name, 'fixture has a name');
assert.ok(Array.isArray(fixture.config.files) && fixture.config.files.length > 0);
rmSync(tmp, { recursive: true, force: true });
} catch (err) {
rmSync(tmp, { recursive: true, force: true });
throw err;
}
});
it('live-inject --port adds the script tag to every config file', () => {
const { tmp } = stageFixture(name);
try {
const out = runVerb('live-inject', ['--port', '9999'], { cwd: tmp });
const result = JSON.parse(typeof out === 'string' ? out : out.error);
assert.equal(result.ok, true, 'inject succeeded');
assert.equal(result.gitIgnore?.mode, 'git-info-exclude', 'live runtime ignores are installed locally');
const ignored = execFileSync('git', [
'check-ignore',
'.impeccable/live/server.json',
'.impeccable/live/sessions/example.jsonl',
'.impeccable/live/previews/example/v1.html',
'.impeccable/live/artifacts/example-r1.jsx',
'.impeccable/live/accept-receipts/example.json',
'.impeccable/live/locks/example.lock',
'.impeccable/live/deferred-svelte-component-accepts.json',
'src/lib/impeccable/ImpeccableLiveRoot.svelte',
'src/lib/impeccable/__runtime.js',
'src/lib/impeccable/a4ac4e74/v3.svelte',
], { cwd: tmp, encoding: 'utf-8' });
assert.match(ignored, /\.impeccable\/live\/server\.json/);
assert.match(ignored, /\.impeccable\/live\/sessions\/example\.jsonl/);
assert.match(ignored, /\.impeccable\/live\/previews\/example\/v1\.html/);
assert.match(ignored, /\.impeccable\/live\/artifacts\/example-r1\.jsx/);
assert.match(ignored, /\.impeccable\/live\/accept-receipts\/example\.json/);
assert.match(ignored, /\.impeccable\/live\/locks\/example\.lock/);
assert.match(ignored, /\.impeccable\/live\/deferred-svelte-component-accepts\.json/);
assert.match(ignored, /src\/lib\/impeccable\/ImpeccableLiveRoot\.svelte/);
assert.match(ignored, /src\/lib\/impeccable\/__runtime\.js/);
assert.match(ignored, /src\/lib\/impeccable\/a4ac4e74\/v3\.svelte/);
if (result.adapter === 'sveltekit') {
const layout = readFileSync(join(tmp, 'src/routes/+layout.svelte'), 'utf-8');
const appHtml = readFileSync(join(tmp, 'src/app.html'), 'utf-8');
const root = readFileSync(join(tmp, 'src/lib/impeccable/ImpeccableLiveRoot.svelte'), 'utf-8');
assert.match(layout, /impeccable-live-svelte-start/, 'SvelteKit layout got the adapter marker');
assert.match(layout, /ImpeccableLiveRoot/, 'SvelteKit layout renders the adapter host');
assert.doesNotMatch(appHtml, /impeccable-live-start/, 'SvelteKit app.html must remain untouched');
assert.doesNotMatch(appHtml, /localhost:9999\/live\.js/, 'SvelteKit app.html must not own live.js');
assert.match(root, /localhost:9999\/live\.js/, 'SvelteKit root component loads live.js');
return;
}
if (result.adapter === 'nuxt') {
const plugin = result.results[0];
const body = readFileSync(join(tmp, plugin.file), 'utf-8');
assert.equal(plugin.inserted, true, 'Nuxt client plugin was created');
assert.match(body, /impeccable-live-nuxt-plugin/);
assert.match(body, /if \(!import\.meta\.dev/);
assert.match(body, /localhost:9999\/live\.js/);
return;
}
if (result.adapter === 'tanstack-start') {
const adapterResult = result.results[0];
const rootDoc = readFileSync(join(tmp, adapterResult.file), 'utf-8');
const component = readFileSync(join(tmp, adapterResult.componentFile), 'utf-8');
assert.equal(adapterResult.inserted, true, 'TanStack Start root document was patched');
assert.match(rootDoc, /impeccable-live-tanstack-start/, 'root document got the adapter marker');
assert.match(rootDoc, /<ImpeccableLiveRoot \/>/, 'root document renders the mount component');
assert.doesNotMatch(rootDoc, /impeccable-live-start/, 'root document must not get the raw script block');
assert.doesNotMatch(rootDoc, /localhost:9999\/live\.js/, 'root document must not own live.js directly');
assert.match(component, /localhost:9999\/live\.js/, 'mount component loads live.js');
assert.match(component, /useEffect/, 'mount component appends the script on mount');
return;
}
for (const r of result.results) {
assert.ok(r.inserted, `${r.file} got the tag (result: ${JSON.stringify(r)})`);
const body = readFileSync(join(tmp, r.file), 'utf-8');
assert.match(body, /impeccable-live-start/);
assert.match(body, /localhost:9999\/live\.js/);
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it('live-inject --remove strips the script tag cleanly', () => {
const { tmp } = stageFixture(name);
try {
runVerb('live-inject', ['--port', '9999'], { cwd: tmp });
const out = runVerb('live-inject', ['--remove'], { cwd: tmp });
const result = JSON.parse(typeof out === 'string' ? out : out.error);
assert.equal(result.ok, true, 'remove succeeded');
if (result.adapter === 'sveltekit') {
const layout = readFileSync(join(tmp, 'src/routes/+layout.svelte'), 'utf-8');
const appHtml = readFileSync(join(tmp, 'src/app.html'), 'utf-8');
assert.doesNotMatch(layout, /ImpeccableLiveRoot/);
assert.doesNotMatch(layout, /impeccable-live-svelte-start/);
assert.doesNotMatch(appHtml, /impeccable-live-start/);
assert.equal(existsSync(join(tmp, 'src/lib/impeccable/ImpeccableLiveRoot.svelte')), false);
return;
}
if (result.adapter === 'nuxt') {
assert.equal(result.results[0].removed, true);
assert.equal(existsSync(join(tmp, result.results[0].file)), false, 'Nuxt client plugin was removed');
return;
}
if (result.adapter === 'tanstack-start') {
const adapterResult = result.results[0];
const rootDoc = readFileSync(join(tmp, adapterResult.file), 'utf-8');
assert.doesNotMatch(rootDoc, /ImpeccableLiveRoot/);
assert.doesNotMatch(rootDoc, /impeccable-live-tanstack-start/);
assert.equal(existsSync(join(tmp, adapterResult.componentFile)), false, 'TanStack mount component was removed');
return;
}
for (const r of result.results) {
const body = readFileSync(join(tmp, r.file), 'utf-8');
assert.doesNotMatch(body, /impeccable-live-start/);
assert.doesNotMatch(body, /live\.js/);
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it('detect-csp classifies CSP shape correctly', () => {
const { tmp, fixture } = stageFixture(name);
try {
const expected = fixture.csp?.shape ?? null;
const result = JSON.parse(runVerb('detect-csp', [], { cwd: tmp }));
assert.equal(
result.shape,
expected,
`expected CSP shape ${expected}, got ${result.shape}; signals: ${JSON.stringify(result.signals)}`
);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it('live-wrap routes to the expected source (or emits the expected fallback)', () => {
const { tmp, fixture } = stageFixture(name);
try {
for (const [i, wc] of (fixture.wrapCases || []).entries()) {
const flags = [];
if (wc.args.elementId) flags.push('--element-id', wc.args.elementId);
if (wc.args.classes) flags.push('--classes', wc.args.classes);
if (wc.args.tag) flags.push('--tag', wc.args.tag);
flags.push('--id', `wraptest${i}`, '--count', '3');
const out = runVerb('live-wrap', flags, { cwd: tmp });
const payload = typeof out === 'string' ? out : (out.error || out.stderr);
const parsed = JSON.parse(payload.trim().split('\n').pop());
if (wc.expectsError) {
assert.equal(parsed.error, wc.expectsError, `wrap case "${wc.name}": expected error ${wc.expectsError}, got ${JSON.stringify(parsed)}`);
} else {
assert.equal(parsed.file, wc.expectedFile, `wrap case "${wc.name}": landed in ${parsed.file}, expected ${wc.expectedFile}`);
if (wc.expectedSourceFile) {
assert.equal(parsed.sourceFile, wc.expectedSourceFile, `wrap case "${wc.name}": source file`);
}
if (wc.expectedPreviewMode) {
assert.equal(parsed.previewMode, wc.expectedPreviewMode, `wrap case "${wc.name}": preview mode`);
}
}
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
});
}
describe('detect-csp — Next.js proxy placement', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSAGE }, () => {
it('accepts proxy files at app roots and src roots but ignores same-named helpers', () => {
const source = `export function proxy() {
const response = new Response();
response.headers.set('Content-Security-Policy', "script-src 'self'");
return response;
}\n`;
for (const [relPath, expectedShape, markers = []] of [
['proxy.ts', 'middleware'],
['src/proxy.ts', 'middleware'],
['apps/web/proxy.ts', 'middleware', ['apps/web/app']],
['apps/docs/src/proxy.ts', 'middleware', ['apps/docs/src/pages']],
['apps/store/proxy.ts', 'middleware', ['apps/store/package.json']],
['lib/network/proxy.ts', null],
['apps/web/lib/proxy.ts', null, ['apps/web/app']],
]) {
const tmp = mkdtempSync(join(tmpdir(), 'impeccable-proxy-placement-'));
try {
mkdirSync(dirname(join(tmp, relPath)), { recursive: true });
for (const marker of markers) {
if (marker.endsWith('package.json')) {
mkdirSync(dirname(join(tmp, marker)), { recursive: true });
writeFileSync(join(tmp, marker), JSON.stringify({ dependencies: { next: '^16.0.0' } }));
} else {
mkdirSync(join(tmp, marker), { recursive: true });
}
}
writeFileSync(join(tmp, relPath), source);
const result = JSON.parse(runVerb('detect-csp', [], { cwd: tmp }));
assert.equal(result.shape, expectedShape, relPath);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
});
});