mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
The detector is open source. The rules it ships were already public in this repo's git history and in every npm tarball of the JS engine, so a closed binary bought nothing it could keep; the moat is the service (the catalog, the labs, the review pipeline), not the check functions. Keeping them behind a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time download, a second release to order ahead of every engine release, and a serde layer that had to serve two encodings. Deleted - crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs and the shim modules under src/checks and src/browser. - crates/foundation/src/boundary.rs and the postcard dependency. - DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the check:detector-release script, the detector gate and IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs. - scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the vendoring path for the closed browser bundle). - scripts/build-browser-detector.js and the build:browser script (a stub since the JS engine left the tree). - xtask's detector-archive subcommand and its public-repo lookup. Came back - crates/core is now the rule logic itself: every check_* / scan_*, the browser adapters, the visual-contrast decisions. It re-exports foundation as before, so no consumer changed. Its vectors dispatcher is the union of both id tables again, and tests/vectors.rs replays the frozen vectors straight through it. - crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds the in-page bundle from browser-bundle/ plus the wasm core, writes dist/, refreshes the tracked crates/live/assets/detect-antipatterns- browser.js, and writes extension/detector/. bun run build:extension runs it instead of downloading. - crates/live/assets/detect-antipatterns-browser.js is tracked again; live mode embeds it and serves it as /detect.js. - Serde is back to plain derives: no is_human_readable branch in js::json_number, derived Serialize for Rgba and BrowserFinding with their skip_serializing_if attributes. - profile.release has lto = "fat" again; rust-toolchain.toml is plain stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs lose continue-on-error and can be required. Verified - cargo build --workspace --all-targets: clean, no warnings. - cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone with the boundary). - cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok. - cargo xtask bundle && cargo xtask bundle --check: reproducible; the regenerated bundle is committed (it differs from the archived one, which was built with a pinned rustc and lto = false). - cargo build --release -p impeccable: no linker warnings, 12.5 MB (the same source at lto = false is 13.1 MB). - oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens. - bun run build, bun run build:extension, web-ext lint (0 errors, 8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail. - impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms median of 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
/**
|
|
* Guard tests for scripts/release.mjs, the tagging/publishing script for the
|
|
* three independently versioned components. Until now it had zero coverage
|
|
* while owning every refusal that protects a public release: dirty tree,
|
|
* unpushed HEAD, existing tag, disagreeing manifests, missing changelog
|
|
* entry, missing artifacts.
|
|
*
|
|
* The script resolves repoRoot from its own file location and runs top-level
|
|
* code on import, so these tests copy it into a disposable git repo (with a
|
|
* local bare `origin`) and spawn it exactly as a maintainer would. Every run
|
|
* uses --dry-run, which skips all mutating steps (tag, push, gh release,
|
|
* builds) but exercises every guard on the way there.
|
|
*/
|
|
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const RELEASE_SCRIPT = path.join(REPO_ROOT, 'scripts', 'release.mjs');
|
|
|
|
const CHANGELOG = `---
|
|
---
|
|
<article>
|
|
<div class="changelog-version-header"><span class="cf-version">v1.2.3</span></div>
|
|
<ul class="cf-items">
|
|
<li><strong>Loader contract pinned.</strong> Uses <code>plugin.json</code> checks & a <a href="https://example.com/docs">guide</a>.</li>
|
|
<li><strong>Faster runner.</strong> Batched invocations cut wall time.</li>
|
|
</ul>
|
|
</article>
|
|
<article>
|
|
<div class="changelog-version-header"><span class="cf-version">CLI v9.9.9</span></div>
|
|
<ul class="cf-items">
|
|
<li><strong>New detect flags.</strong> Adds <code>--fast</code>.</li>
|
|
</ul>
|
|
</article>
|
|
`;
|
|
|
|
function git(cwd, ...args) {
|
|
return execFileSync('git', args, { cwd, encoding: 'utf-8' }).trim();
|
|
}
|
|
|
|
function runRelease(cwd, ...args) {
|
|
try {
|
|
const stdout = execFileSync(process.execPath, ['scripts/release.mjs', ...args, '--dry-run'], {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
timeout: 60000,
|
|
// The D4 engine release-order guard would otherwise probe the network for
|
|
// published engine assets; these guards predate it and only exercise the
|
|
// version/changelog/artifact checks, so take its documented escape hatch.
|
|
env: { ...process.env, IMPECCABLE_SKIP_ENGINE_CHECK: '1' },
|
|
});
|
|
return { code: 0, stdout, stderr: '' };
|
|
} catch (err) {
|
|
return { code: err.status ?? 1, stdout: err.stdout ?? '', stderr: err.stderr ?? '' };
|
|
}
|
|
}
|
|
|
|
describe('release.mjs guards', () => {
|
|
let root;
|
|
let workDir;
|
|
let bareDir;
|
|
let baselineSha;
|
|
|
|
const write = (rel, contents) => {
|
|
const abs = path.join(workDir, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, contents);
|
|
};
|
|
|
|
before(() => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-release-'));
|
|
bareDir = path.join(root, 'origin.git');
|
|
workDir = path.join(root, 'work');
|
|
execFileSync('git', ['init', '--bare', bareDir]);
|
|
fs.mkdirSync(workDir);
|
|
git(workDir, 'init', '-b', 'main');
|
|
git(workDir, 'config', 'user.email', 'test@example.com');
|
|
git(workDir, 'config', 'user.name', 'Release Test');
|
|
|
|
fs.mkdirSync(path.join(workDir, 'scripts'));
|
|
fs.copyFileSync(RELEASE_SCRIPT, path.join(workDir, 'scripts', 'release.mjs'));
|
|
// release.mjs imports ./check-engine-release.mjs and ./fetch-engine.mjs
|
|
// (and check-engine-release.mjs imports fetch-engine.mjs), so stage them
|
|
// too or the dry runs fail to resolve the modules instead of exercising
|
|
// the guard.
|
|
for (const dep of ['check-engine-release.mjs', 'fetch-engine.mjs']) {
|
|
fs.copyFileSync(path.join(REPO_ROOT, 'scripts', dep), path.join(workDir, 'scripts', dep));
|
|
}
|
|
write('.claude-plugin/plugin.json', JSON.stringify({ name: 'impeccable', version: '1.2.3' }));
|
|
write('.claude-plugin/marketplace.json', JSON.stringify({ plugins: [{ name: 'impeccable', version: '1.2.3' }] }));
|
|
write('package.json', JSON.stringify({
|
|
name: 'impeccable',
|
|
version: '9.9.9',
|
|
optionalDependencies: { '@impeccable/cli-darwin-arm64': '0.1.0', '@impeccable/cli-linux-x64': '0.1.0' },
|
|
}));
|
|
write('ENGINE_VERSION', '0.1.0\n');
|
|
write('extension/manifest.json', JSON.stringify({ version: '2.0.0' }));
|
|
write('site/pages/changelog.astro', CHANGELOG);
|
|
write('dist/universal.zip', 'zip');
|
|
write('dist/extension.zip', 'zip');
|
|
write('dist/extension-firefox.zip', 'zip');
|
|
|
|
git(workDir, 'add', '-A');
|
|
git(workDir, 'commit', '-m', 'fixture');
|
|
git(workDir, 'remote', 'add', 'origin', bareDir);
|
|
git(workDir, 'push', '-u', 'origin', 'main');
|
|
baselineSha = git(workDir, 'rev-parse', 'HEAD');
|
|
});
|
|
|
|
after(() => {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Undo whatever the previous scenario staged, on both ends: local tree
|
|
// and tags back to the baseline commit, and origin force-reset too, since
|
|
// several scenarios push commits or tags that would poison later ones.
|
|
git(workDir, 'checkout', '--', '.');
|
|
git(workDir, 'clean', '-fd');
|
|
git(workDir, 'reset', '--hard', baselineSha);
|
|
git(workDir, 'push', '--force', 'origin', 'main');
|
|
for (const tag of git(workDir, 'tag').split('\n').filter(Boolean)) {
|
|
git(workDir, 'tag', '-d', tag);
|
|
}
|
|
// --refs excludes the peeled `^{}` lines annotated tags produce, which
|
|
// are not deletable refs and would abort the cleanup.
|
|
for (const line of git(workDir, 'ls-remote', '--refs', '--tags', 'origin').split('\n').filter(Boolean)) {
|
|
const ref = line.split('\t')[1];
|
|
if (ref) git(workDir, 'push', 'origin', `:${ref}`);
|
|
}
|
|
});
|
|
|
|
it('dry-runs a clean engine release: tags only, CI publishes', () => {
|
|
const { code, stdout } = runRelease(workDir, 'engine');
|
|
assert.equal(code, 0, stdout);
|
|
assert.match(stdout, /Engine 0\.1\.0/);
|
|
assert.match(stdout, /2 platform package pins agree/);
|
|
assert.match(stdout, /\[dry-run\] git tag -a engine-v0\.1\.0/);
|
|
assert.match(stdout, /\[dry-run\] git push origin engine-v0\.1\.0/);
|
|
assert.doesNotMatch(stdout, /gh release create/);
|
|
assert.match(stdout, /release-engine workflow/);
|
|
});
|
|
|
|
it('engine: refuses when package.json platform pins disagree with ENGINE_VERSION', () => {
|
|
write('ENGINE_VERSION', '0.2.0\n');
|
|
git(workDir, 'commit', '-am', 'bump engine');
|
|
git(workDir, 'push', 'origin', 'main');
|
|
const { code, stderr } = runRelease(workDir, 'engine');
|
|
assert.notEqual(code, 0);
|
|
assert.match(stderr, /pins @impeccable\/cli-darwin-arm64@0\.1\.0.*expected 0\.2\.0/);
|
|
});
|
|
|
|
it('engine: refuses when the tag already exists on origin', () => {
|
|
git(workDir, 'tag', 'engine-v0.1.0');
|
|
git(workDir, 'push', 'origin', 'engine-v0.1.0');
|
|
git(workDir, 'tag', '-d', 'engine-v0.1.0');
|
|
const { code, stderr } = runRelease(workDir, 'engine');
|
|
assert.notEqual(code, 0);
|
|
assert.match(stderr, /engine-v0\.1\.0 already exists on origin/);
|
|
});
|
|
|
|
it('dry-runs a clean skill release end to end', () => {
|
|
const { code, stdout } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 0, stdout);
|
|
assert.match(stdout, /Skill 1\.2\.3/);
|
|
assert.match(stdout, /tag is free/);
|
|
assert.match(stdout, /\[dry-run\] git tag -a skill-v1\.2\.3/);
|
|
assert.match(stdout, /\[dry-run\] gh release create skill-v1\.2\.3/);
|
|
});
|
|
|
|
it('converts the changelog entry to markdown release notes', () => {
|
|
const { code, stdout } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 0, stdout);
|
|
assert.match(stdout, /- \*\*Loader contract pinned\.\*\* Uses `plugin\.json` checks & a \[guide\]\(https:\/\/example\.com\/docs\)\./);
|
|
assert.match(stdout, /- \*\*Faster runner\.\*\*/);
|
|
});
|
|
|
|
it('renders a tweet within the 280-char limit with the release URL', () => {
|
|
const { code, stdout } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 0, stdout);
|
|
const tweetMatch = stdout.match(/--- Tweet \((\d+)\/280 chars\)[^\n]*---\n([\s\S]*?)\n--- end tweet ---/);
|
|
assert.ok(tweetMatch, `no tweet block in output:\n${stdout}`);
|
|
assert.ok(Number(tweetMatch[1]) <= 280);
|
|
assert.match(tweetMatch[2], /Impeccable v1\.2\.3 is out\./);
|
|
assert.match(tweetMatch[2], /releases\/tag\/skill-v1\.2\.3/);
|
|
assert.match(tweetMatch[2], /• Loader contract pinned/);
|
|
});
|
|
|
|
it('matches the prefixed changelog label for the CLI component', () => {
|
|
const { code, stdout } = runRelease(workDir, 'cli');
|
|
assert.equal(code, 0, stdout);
|
|
assert.match(stdout, /CLI 9\.9\.9/);
|
|
assert.match(stdout, /- \*\*New detect flags\.\*\*/);
|
|
});
|
|
|
|
it('refuses an unknown component', () => {
|
|
const { code, stderr } = runRelease(workDir, 'website');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /usage: release\.mjs/);
|
|
});
|
|
|
|
it('refuses a dirty working tree', () => {
|
|
write('README.md', 'uncommitted');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /Working tree is dirty/);
|
|
});
|
|
|
|
it('refuses when HEAD is ahead of origin', () => {
|
|
write('note.txt', 'ahead');
|
|
git(workDir, 'add', '-A');
|
|
git(workDir, 'commit', '-m', 'unpushed');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /Push your commits first/);
|
|
});
|
|
|
|
it('refuses when the tag already exists locally', () => {
|
|
git(workDir, 'tag', 'skill-v1.2.3');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /already exists locally/);
|
|
});
|
|
|
|
it('refuses when the tag already exists on origin', () => {
|
|
git(workDir, 'tag', 'skill-v1.2.3');
|
|
git(workDir, 'push', 'origin', 'skill-v1.2.3');
|
|
git(workDir, 'tag', '-d', 'skill-v1.2.3');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /already exists on origin/);
|
|
});
|
|
|
|
it('refuses when plugin.json and marketplace.json disagree', () => {
|
|
write('.claude-plugin/marketplace.json', JSON.stringify({ plugins: [{ name: 'impeccable', version: '1.0.0' }] }));
|
|
git(workDir, 'add', '-A');
|
|
git(workDir, 'commit', '-m', 'mismatch');
|
|
git(workDir, 'push', 'origin', 'main');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /disagree\. Bump both\./);
|
|
});
|
|
|
|
it('refuses when the changelog entry is missing', () => {
|
|
write('extension/manifest.json', JSON.stringify({ version: '3.0.0' }));
|
|
git(workDir, 'add', '-A');
|
|
git(workDir, 'commit', '-m', 'bump without changelog');
|
|
git(workDir, 'push', 'origin', 'main');
|
|
const { code, stderr } = runRelease(workDir, 'extension');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /No changelog entry found for "Extension v3\.0\.0"/);
|
|
});
|
|
|
|
it('refuses when a release artifact is missing', () => {
|
|
fs.rmSync(path.join(workDir, 'dist/universal.zip'));
|
|
git(workDir, 'add', '-A');
|
|
git(workDir, 'commit', '-m', 'drop artifact');
|
|
git(workDir, 'push', 'origin', 'main');
|
|
const { code, stderr } = runRelease(workDir, 'skill');
|
|
assert.equal(code, 1);
|
|
assert.match(stderr, /Missing artifact: dist\/universal\.zip/);
|
|
});
|
|
});
|