50-config-audit-sc2.sh: re-seeds snapshot-default-output in-clone (UPDATE_SNAPSHOT=1), then runs the SC2 gate = full 'find tests -name *.test.mjs' MINUS the 6-file machine-locked v5.0.0 byte-stability surface. 730 tests across 46 files pass standalone. templates/validate-plugin.generic.sh: ported, parameterized structure validator for the two test-less plugins (okr, human-friendly-style) — STRUCTURE OK on valid plugin.json + non-empty surface + parseable frontmatter; STRUCTURE FAIL otherwise. Reuses the claude-design/tests/validate-plugin.sh pattern. Brief-correction (operator-ratified 2026-06-17): plan F4 named only json-backcompat + raw-backcompat (2 files). The verified machine-locked surface is 6 — the v5.0.0 fixtures embed the original absolute path AND the claude_md/plugin_hygiene scanners key off a plugins/ ancestor, so findings drift by path (behavioral, not a string rewrite). Dropped clean-room SC2 coverage = config-audit's humanizer/posture-humanizer/scan-orchestrator-humanizer prose-snapshot surface, recorded in the script header + plugin-map.json (not silent). The 3 other v5.0.0-referencing tests (posture, scoring-humanizer, scenario-read-test) are path-independent and stay in the gate. config-audit backlog (out of migration scope): normalize the v5.0.0 fixtures + path-agnostic scanners, then re-include. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3.9 KiB
JavaScript
80 lines
3.9 KiB
JavaScript
// Step 7 test — the SC2 resolver + generic validator are their own test surface:
|
|
// 1. The config-audit SC2 gate PASSes a standalone extract (re-seed + back-compat exclusion).
|
|
// 2. The gate excludes json-backcompat + raw-backcompat by NAME and re-seeds (not excludes) the snapshot.
|
|
// 3. The generic validator PASSes okr.
|
|
// 4. The generic validator FAILs a plugin whose plugin.json was deleted.
|
|
// Pattern: 40-validate-standalone.test.mjs (spawn the script, assert exit code + stdout).
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, cpSync, readFileSync } from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const REPO = path.resolve(here, '..', '..', '..'); // migration -> marketplace-polyrepo-migration -> docs -> repo root
|
|
const SC2 = path.join(here, '50-config-audit-sc2.sh');
|
|
const GENERIC = path.join(here, 'templates', 'validate-plugin.generic.sh');
|
|
const WORK = process.env.WORK || '/tmp/polyrepo-migration';
|
|
|
|
function run(cmd, args, timeout = 600000) {
|
|
// Strip the parent test-runner context so the script's own `node --test` runs un-nested
|
|
// (otherwise Node emits its IPC subtest format and the test-count label is suppressed).
|
|
const env = { ...process.env, WORK };
|
|
delete env.NODE_TEST_CONTEXT;
|
|
return spawnSync(cmd, args, { encoding: 'utf8', env, timeout });
|
|
}
|
|
|
|
// The verified machine-locked v5.0.0 byte-stability surface (operator-ratified 2026-06-17):
|
|
// plan F4 named 2 files; the real surface is these 6. The other v5.0.0-referencing tests
|
|
// (posture, scoring-humanizer, scenario-read-test) are path-independent and stay IN the gate.
|
|
const MACHINE_LOCKED = [
|
|
'json-backcompat', 'raw-backcompat', 'cli-humanizer',
|
|
'posture-humanizer', 'scan-orchestrator-humanizer', 'lint-default-output',
|
|
];
|
|
|
|
test('config-audit SC2 gate PASSes a standalone extract (re-seed + machine-locked exclusion)', () => {
|
|
const r = run('bash', [SC2]);
|
|
assert.equal(r.status, 0, `expected SC2 PASS exit 0:\n${r.stdout}\n${r.stderr}`);
|
|
assert.match(r.stdout, /config-audit: SC2 PASS/);
|
|
assert.match(r.stdout, /minus the 6-file v5\.0\.0 byte-stability surface/);
|
|
});
|
|
|
|
test('the SC2 gate excludes the full machine-locked surface by name and re-seeds the snapshot', () => {
|
|
const src = readFileSync(SC2, 'utf8');
|
|
for (const name of MACHINE_LOCKED) {
|
|
assert.ok(src.includes(name), `EXCLUDE_RE must name the machine-locked test: ${name}`);
|
|
}
|
|
assert.match(src, /grep -vE "\$EXCLUDE_RE"/,
|
|
'the gate must filter the enumerated test list through EXCLUDE_RE');
|
|
assert.match(src, /UPDATE_SNAPSHOT=1/,
|
|
'the rebasable snapshot must be re-seeded in-clone, never excluded');
|
|
});
|
|
|
|
test('generic validator PASSes okr', () => {
|
|
const tmp = mkdtempSync(path.join(os.tmpdir(), 'okr-ok-'));
|
|
try {
|
|
const root = path.join(tmp, 'okr');
|
|
cpSync(path.join(REPO, 'plugins', 'okr'), root, { recursive: true });
|
|
const r = run('bash', [GENERIC, 'okr', root], 60000);
|
|
assert.equal(r.status, 0, `expected okr STRUCTURE OK exit 0:\n${r.stdout}\n${r.stderr}`);
|
|
assert.match(r.stdout, /okr: STRUCTURE OK/);
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('generic validator FAILs a plugin with a deleted plugin.json', () => {
|
|
const tmp = mkdtempSync(path.join(os.tmpdir(), 'okr-broken-'));
|
|
try {
|
|
const root = path.join(tmp, 'okr');
|
|
cpSync(path.join(REPO, 'plugins', 'okr'), root, { recursive: true });
|
|
rmSync(path.join(root, '.claude-plugin', 'plugin.json'), { force: true });
|
|
const r = run('bash', [GENERIC, 'okr', root], 60000);
|
|
assert.notEqual(r.status, 0, `expected STRUCTURE FAIL (non-zero), got 0:\n${r.stdout}`);
|
|
assert.match(r.stdout, /okr: STRUCTURE FAIL/);
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|