test(04-02): Wave 0 — no-new-function-in-sw-chunk RED + dead-code-grep regression pin

Two new build-gate vitest files at `tests/build/` per Plan 04-02 Wave 0
TDD-strict RED-first contract:

- `no-new-function-in-sw-chunk.test.ts`: SW-chunk CSP-hardening grep gate.
  Narrows the file walk to `dist/assets/index.ts-*.js` (the SW + loader
  chunks; cf. plan-checker iter-1 BLOCKER 1 fix). RED today: 1 occurrence
  of `new Function` in the SW chunk (the pre-existing `setimmediate` npm
  package fallback bundled transitively by vite-plugin-node-polyfills,
  per .planning/phases/01-stabilize-video-pipeline/deferred-items.md).
  Flips GREEN after Task 2's setimmediate replacement lands. Build-prep
  gate (npm run build + dist/assets/ existence + ≥1 SW chunk match)
  precedes the grep gate so the test is self-bootstrapping under
  SKIP_BUILD=0 and self-asserting under SKIP_BUILD=1.

- `dead-code-grep.test.ts`: ROADMAP SC #4 regression pin against `src/`.
  Asserts absence of `permissions.request` (removed in Phase 1 Plan
  01-05 SW shrink). GREEN-on-arrival today; acts as regression guard so
  re-introducing the deleted permission-request flow breaks CI. The
  offscreen-inline-string sub-test is documented as delegated to the
  vite.config.ts review + tests/build/no-remote-fonts.test.ts (no single
  literal sentinel pinnable post-Plan-01-06 collapse).

Polarity confirmation:
  - Acceptance grep: `grep -v '^//' tests/build/no-new-function-in-sw-chunk.test.ts | grep -c 'new Function'` returns 3 (≥2 required).
  - Acceptance grep: `grep -v '^//' tests/build/dead-code-grep.test.ts | grep -c 'permissions.request'` returns 2 (≥2 required).
  - SKIP_BUILD=1 npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run: 2 passed + 1 failed (the expected RED gate).
  - Full vitest: 180 passed + 3 failed (1 = this task's expected RED + 2 = pre-existing ffmpeg/ffprobe flakes per 04-01-SUMMARY Issues Encountered — owned by Plan 04-03).

References:
  - .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md §"tests/build/no-new-function-in-sw-chunk.test.ts" + §"tests/build/dead-code-grep.test.ts"
  - .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md §Q1
  - Plan 04-02 threat model T-04-02-01 (Elevation of Privilege) + T-04-02-03 (Information Disclosure regression pin)
  - tests/build/no-remote-fonts.test.ts (Plan 01-12 analog scaffold)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 14:39:48 +02:00
parent f72bca5c46
commit 630d40c4f8
2 changed files with 344 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
// tests/build/dead-code-grep.test.ts — Plan 04-02 Wave 0 GREEN-on-arrival
// regression pin (ROADMAP SC #4).
//
// ROADMAP SC #4 ("Dead-code grep returns no live references for
// permissions.request + duplicate offscreen inline string") is verified
// against the current src/ source tree (NOT against dist/). Both targets were
// removed during Phase 1:
//
// - `permissions.request` was the broken runtime-permission flow under the
// pre-D-01 `chrome.tabCapture` architecture; removed in Plan 01-05 as part
// of the SW shrink (audit P0 #5 fix). Under D-01 (`getDisplayMedia` via
// offscreen + `desktopCapture` permission), no runtime permission check is
// meaningful — every user-gesture-initiated getDisplayMedia surfaces its
// own picker UI as the implicit grant. Reintroducing `permissions.request`
// would route start-of-recording back into the never-granted branch and
// silently fail.
//
// - The duplicate offscreen inline string was the build-pipeline-side
// manifestation of the same architecture: pre-01-06 vite.config.ts shipped
// a 174-line inline copy-offscreen plugin that emitted an HTML literal of
// the offscreen document for the `chrome.offscreen.createDocument` path.
// Plan 01-06 collapsed vite.config.ts to 21 lines + relocated the
// offscreen entry into `rollupOptions.input` under the crxjs plugin's
// canonical multi-entry pattern. Reintroducing the inline-string emit
// would defeat the build-pipeline collapse.
//
// Polarity at Wave 0 land:
// - GREEN-on-arrival today (both targets are absent — verified by
// `grep -rn 'permissions.request' src/` returning 0 hits + the vite.config.ts
// review at Plan 04-02 RED time). This test acts as a regression pin: a
// future commit reintroducing either offender breaks CI.
//
// Note on the offscreen-inline-string sub-test: the planner empirically
// determined that no single literal HTML string from the pre-01-06 inline
// plugin is uniquely pinnable post-collapse — the plugin emitted a templated
// HTML body that included only generic <html><head>... markup with no
// SUMMARY-distinguishable sentinel. The audit-side coverage for "duplicate
// offscreen inline string absence" is delegated to:
// - tests/build/no-remote-fonts.test.ts (Plan 01-12) — full dist/ walk for
// `googleapis` etc. would catch any inline-HTML reintroduction with remote
// URLs; AND
// - vite.config.ts review at Plan 04-02 time confirms the file is still 21
// lines + 75 LOC including blanks (no inline plugin block re-added).
// This file documents the delegation in the SUMMARY rather than asserting a
// brittle sentinel that future minor formatting changes would invalidate.
//
// Implementation mirrors tests/build/no-remote-fonts.test.ts (recursive walk +
// substring count + describe-block scaffold), but scopes the walk to `src/`
// directly (no build step needed; this is a pure source-tree audit).
//
// References:
// - .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md §"Phase
// Requirements" mapping (ROADMAP SC #4 ↔ this test)
// - .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md (this test
// scaffold; analog at tests/build/no-remote-fonts.test.ts)
// - .planning/phases/01-stabilize-video-pipeline/01-05-SUMMARY.md (Plan
// 01-05 SW shrink, where permissions.request was removed)
// - .planning/phases/01-stabilize-video-pipeline/01-06-SUMMARY.md (Plan
// 01-06 build-pipeline collapse, where the inline-string plugin was
// deleted)
// - Plan 04-02 threat model T-04-02-03 (Information Disclosure via
// leftover deleted-permission literal masking architecture)
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
import { describe, expect, it } from 'vitest';
interface DeadCodeEntry {
readonly needle: string;
readonly searchPaths: ReadonlyArray<string>;
readonly rationale: string;
}
/** Forbidden dead-code substrings — any occurrence in src/ is a regression. */
const FORBIDDEN_DEAD_CODE: ReadonlyArray<DeadCodeEntry> = [
{
needle: 'permissions.request',
searchPaths: ['src'],
rationale:
'Removed in Phase 1 Plan 01-05 SW shrink (audit P0 #5 fix). Under D-01 ' +
'(getDisplayMedia via offscreen + desktopCapture manifest permission), no ' +
'runtime permission check is meaningful. Reintroducing permissions.request ' +
'would route start-of-recording back into the never-granted branch.',
},
];
interface ForbiddenMatch {
readonly filePath: string;
readonly count: number;
}
function listAllFilesRecursive(root: string): ReadonlyArray<string> {
const accumulator: string[] = [];
const stack: string[] = [root];
while (stack.length > 0) {
const dir = stack.pop()!;
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = resolvePath(dir, entry.name);
// Filter-pipeline form: skip symlinks via short-circuit (no `continue` per
// CLAUDE.md global instructions; we read `if (...) {} else if (...) { stack.push }`).
if (entry.isSymbolicLink()) {
// Symlinks within src/ should not exist in this project; defensive skip.
} else if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile()) {
accumulator.push(fullPath);
}
}
}
return accumulator.sort();
}
function isTextLikeFile(filePath: string): boolean {
const binaryExtensions = new Set([
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp',
'.woff', '.woff2', '.ttf', '.otf',
]);
const dotIdx = filePath.lastIndexOf('.');
const ext = dotIdx >= 0 ? filePath.substring(dotIdx).toLowerCase() : '';
return !binaryExtensions.has(ext);
}
function countOccurrencesInFile(filePath: string, needle: string): number {
if (!isTextLikeFile(filePath)) return 0;
const stat = statSync(filePath);
if (stat.size === 0) return 0;
const text = readFileSync(filePath, 'utf8');
let count = 0;
let from = 0;
for (;;) {
const idx = text.indexOf(needle, from);
if (idx < 0) break;
count += 1;
from = idx + needle.length;
}
return count;
}
function findMatchesInPaths(needle: string, searchPaths: ReadonlyArray<string>): ReadonlyArray<ForbiddenMatch> {
const accumulator: ForbiddenMatch[] = [];
const cwd = process.cwd();
for (const searchPath of searchPaths) {
const fullPath = resolvePath(cwd, searchPath);
const stat = statSync(fullPath);
const files = stat.isDirectory() ? listAllFilesRecursive(fullPath) : [fullPath];
for (const filePath of files) {
const count = countOccurrencesInFile(filePath, needle);
if (count > 0) {
accumulator.push({ filePath, count });
}
}
}
return accumulator;
}
describe('dead-code regression pin (ROADMAP SC #4 — Plan 04-02 T-04-02-03)', () => {
for (const entry of FORBIDDEN_DEAD_CODE) {
const pathsDescription = entry.searchPaths.join(' + ');
it(`${pathsDescription} does not contain '${entry.needle}' (${entry.rationale.split('.')[0]})`, () => {
const matches = findMatchesInPaths(entry.needle, entry.searchPaths);
expect(
matches.length,
matches.length === 0
? 'unreachable'
: `${pathsDescription} contains dead-code '${entry.needle}' in ${matches.length} file(s) ` +
`— REGRESSION. ${entry.rationale}\n` +
`Offending files:\n` +
matches
.map((m) => ` - ${m.filePath} (${m.count} occurrence${m.count === 1 ? '' : 's'})`)
.join('\n'),
).toBe(0);
});
}
});