Files
pbakaus_impeccable/tests/detector-bundle.test.mjs
T
Paul BakausandClaude Fable 5.1 f2c9aeab5b build:extension: ship the wasm-core extension shell and vendor its detector from the detector release
`bun run build:extension` was broken on this branch: it still imported the
deleted JS engine (cli/engine/registry/antipatterns.mjs,
scripts/lib/browser-detector-bundle.js).

The shipped shell now matches the new design. The content script only
snapshots the DOM; an extension-owned offscreen document runs the
WebAssembly rule core over that snapshot, so the scanned page's CSP no
longer matters. That replaces the old approach of injecting a JS rules
bundle into the page. New files: extension/offscreen/offscreen.html, plus
the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in
the manifest.

The manifest version stays at 1.3.3. The shell's own manifest carried
2.0.0; feature branches never bump versions, so the bump is a release step.

The five generated detector pieces (core.js, core_bg.wasm, snapshot.js,
overlay.js, antipatterns.json) are vendored at build time into the
gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs,
which resolves them the same three ways crates/core/build.rs resolves the
native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the
~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified
download of detector-browser-bundle.zip from the detector release.
antipatterns.json is no longer regenerated here.

The zip packaging is unchanged. The Firefox variant still builds so
`web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko
has no chrome.offscreen API. The build prints a one-line warning saying so.

Also here: a referenced-path check that fails the build when the manifest
or the service worker points at a file that is not in extension/, a
resolver unit test wired into the core suite, and the detector rule count
in the READMEs synced to the 61 the vendored registry carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-01 16:20:54 -07:00

186 lines
6.9 KiB
JavaScript

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
BUNDLE_SUBDIR,
DETECTOR_PIECES,
cacheBundleDir,
isCompleteBundleDir,
resolveDetectorBundle,
vendorDetectorBundle,
} from '../scripts/lib/detector-bundle.mjs';
const VERSION = '9.9.9';
let tmp;
before(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'detector-bundle-test-')); });
after(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
let seq = 0;
function scratch(name) {
const dir = path.join(tmp, `${name}-${seq++}`);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/** A directory holding the five pieces, each with recognizable content. */
function writeBundle(dir) {
fs.mkdirSync(dir, { recursive: true });
for (const piece of DETECTOR_PIECES) {
fs.writeFileSync(path.join(dir, piece), piece === 'antipatterns.json' ? '[{"id":"a"}]' : `// ${piece}\n`);
}
return dir;
}
/** A fetch that serves a zip body and its sidecar, recording every URL asked for. */
function fakeRelease({ zip, digest, sidecarStatus = 200, zipStatus = 200 }) {
const urls = [];
const fetchImpl = async (url) => {
urls.push(url);
if (url.endsWith('.sha256')) {
return {
ok: sidecarStatus === 200,
status: sidecarStatus,
arrayBuffer: async () => Buffer.from(`${digest} detector-browser-bundle.zip\n`),
};
}
return { ok: zipStatus === 200, status: zipStatus, arrayBuffer: async () => zip };
};
return { fetchImpl, urls };
}
describe('detector bundle resolution', () => {
it('uses IMPECCABLE_DETECTOR_LIB when its extension-detector/ is complete', async () => {
const lib = scratch('lib');
writeBundle(path.join(lib, BUNDLE_SUBDIR));
const result = await resolveDetectorBundle({
version: VERSION,
env: { IMPECCABLE_DETECTOR_LIB: lib },
fetchImpl: () => assert.fail('should not download'),
});
assert.equal(result.source, 'env');
assert.equal(result.dir, path.join(lib, BUNDLE_SUBDIR));
});
it('names the env var and the missing pieces when the local dir is incomplete', async () => {
const lib = scratch('lib-partial');
const dir = path.join(lib, BUNDLE_SUBDIR);
writeBundle(dir);
fs.rmSync(path.join(dir, 'core_bg.wasm'));
await assert.rejects(
() => resolveDetectorBundle({ version: VERSION, env: { IMPECCABLE_DETECTOR_LIB: lib } }),
(err) => {
assert.match(err.message, /IMPECCABLE_DETECTOR_LIB=/);
assert.match(err.message, /core_bg\.wasm/);
assert.match(err.message, /detector-archive/);
return true;
},
);
});
it('uses the version cache under IMPECCABLE_HOME without downloading', async () => {
const home = scratch('home');
const env = { IMPECCABLE_HOME: home };
writeBundle(cacheBundleDir(VERSION, env));
const result = await resolveDetectorBundle({
version: VERSION,
env,
fetchImpl: () => assert.fail('should not download'),
});
assert.equal(result.source, 'cache');
assert.equal(result.dir, cacheBundleDir(VERSION, env));
});
it('downloads and verifies the release bundle into the cache', async () => {
const home = scratch('home-download');
const env = { IMPECCABLE_HOME: home, IMPECCABLE_DETECTOR_BASE: 'https://example.test/dl' };
const zip = Buffer.from('pretend zip bytes');
const digest = crypto.createHash('sha256').update(zip).digest('hex');
const { fetchImpl, urls } = fakeRelease({ zip, digest });
// The extraction step is the `unzip` CLI in production; inject a stand-in
// that writes the flattened members the real one would.
const unzip = (zipPath, member, destDir) => {
assert.equal(fs.readFileSync(zipPath).toString(), zip.toString());
assert.equal(member, 'extension-src/detector/*');
writeBundle(destDir);
};
const result = await resolveDetectorBundle({ version: VERSION, env, fetchImpl, unzip });
assert.equal(result.source, 'download');
assert.equal(result.dir, cacheBundleDir(VERSION, env));
assert.ok(isCompleteBundleDir(result.dir));
assert.ok(
urls.includes(`https://example.test/dl/detector-v${VERSION}/detector-browser-bundle.zip`),
`asked for: ${urls.join(', ')}`,
);
assert.ok(urls.some((u) => u.endsWith('.sha256')), 'verifies against the sidecar');
});
it('refuses a bundle whose checksum does not match', async () => {
const home = scratch('home-mismatch');
const env = { IMPECCABLE_HOME: home };
const { fetchImpl } = fakeRelease({ zip: Buffer.from('bytes'), digest: 'f'.repeat(64) });
await assert.rejects(
() => resolveDetectorBundle({ version: VERSION, env, fetchImpl, unzip: () => assert.fail('never extracts') }),
/checksum mismatch/,
);
assert.equal(isCompleteBundleDir(cacheBundleDir(VERSION, env)), false);
});
it('refuses to install when the sidecar is unavailable', async () => {
const home = scratch('home-nosidecar');
const env = { IMPECCABLE_HOME: home };
const { fetchImpl } = fakeRelease({ zip: Buffer.from('bytes'), digest: 'x', sidecarStatus: 404 });
await assert.rejects(
() => resolveDetectorBundle({ version: VERSION, env, fetchImpl, unzip: () => assert.fail('never extracts') }),
/unverified detector bundle/,
);
});
it('names every option when nothing resolves and the download fails', async () => {
const home = scratch('home-empty');
const env = { IMPECCABLE_HOME: home };
const fetchImpl = async () => ({ ok: false, status: 404, arrayBuffer: async () => Buffer.alloc(0) });
await assert.rejects(
() => resolveDetectorBundle({ version: VERSION, env, fetchImpl }),
(err) => {
assert.match(err.message, /IMPECCABLE_DETECTOR_LIB/);
assert.match(err.message, /IMPECCABLE_DETECTOR_BASE/);
assert.match(err.message, new RegExp(`detector-v${VERSION}`));
return true;
},
);
});
it('refuses to download when IMPECCABLE_DETECTOR_OFFLINE=1', async () => {
const home = scratch('home-offline');
await assert.rejects(
() => resolveDetectorBundle({
version: VERSION,
env: { IMPECCABLE_HOME: home, IMPECCABLE_DETECTOR_OFFLINE: '1' },
fetchImpl: () => assert.fail('should not download'),
}),
/IMPECCABLE_DETECTOR_OFFLINE=1/,
);
});
it('copies all five pieces into the destination directory', async () => {
const lib = scratch('lib-vendor');
writeBundle(path.join(lib, BUNDLE_SUBDIR));
const dest = path.join(scratch('dest'), 'detector');
const result = await vendorDetectorBundle({
destDir: dest,
version: VERSION,
env: { IMPECCABLE_DETECTOR_LIB: lib },
});
assert.deepEqual(result.files.map((f) => f.name).sort(), [...DETECTOR_PIECES].sort());
assert.ok(isCompleteBundleDir(dest));
assert.ok(result.files.every((f) => f.bytes > 0));
});
});