// 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 }); } });