/** * Recurrence gate for openspec/changes/unified-management-no-ssh/proposal.md: **modules never hand-build SSH, * and every raw-exec escape hatch is justified in writing.** * * The rules themselves live in `./module-script-scan`, because this is not the * only place they run — `.netapp` packaging applies the same scan at publish * time, which is the enforcement point that also covers a module built outside * this repo's CI. One definition, two callers. * * This file is the in-repo half: every production module script, on every * `bun test`. */ import { describe, expect, test } from 'bun:test'; import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { JAILED_CLI_SPAWN_DEBT, JAILED_CLI_SPAWN_RULE, REMOTE_PRIMITIVE_FILES, capabilityPackageSourceFiles, formatViolations, moduleScriptFiles, scanCapabilityPackageSource, scanModuleDirectory, scanModuleScriptSource, } from './module-script-scan'; /** Walk up from this test to the repo root (the dir holding both modules/ and apps/). */ function repoRoot(): string { let dir = import.meta.dir; for (let i = 0; i < 8; i++) { if (existsSync(join(dir, 'modules')) && existsSync(join(dir, 'apps'))) return dir; dir = resolve(dir, '..'); } throw new Error('could not locate repo root (no ancestor with modules/ + apps/)'); } function moduleDirs(): string[] { const modulesRoot = join(repoRoot(), 'modules'); return readdirSync(modulesRoot) .map((m) => join(modulesRoot, m)) .filter((d) => statSync(d).isDirectory() && existsSync(join(d, 'scripts'))); } describe('recurrence gate: modules never hand-build SSH', () => { const dirs = moduleDirs(); test('scans a non-trivial set of module scripts (sanity — the scan actually ran)', () => { const scanned = dirs.flatMap((d) => moduleScriptFiles(join(d, 'scripts'))); expect(scanned.length).toBeGreaterThan(10); }); test('no production module script hand-builds SSH, and every runAppCommand* is justified', () => { const violations = dirs.flatMap((d) => scanModuleDirectory(d)); expect(violations, `Module script policy violations:\n${formatViolations(violations)}`).toEqual( [], ); }); }); describe('recurrence gate: the jailed-CLI-spawn debt list is honest', () => { // The debt list in module-script-scan exempts real, tracked defects from the // jailed-CLI-spawn rule so the rule can land green before hook-owned-state // converts them. Three ways this list could silently rot, and this is the // guard for each: // a site is converted -> the file stops matching, the entry must go // a NEW site appears -> the pinned count moves, the entry must be revisited // a file is deleted -> the entry names a ghost and must go test('every entry names an existing file carrying exactly its pinned spawn count', () => { for (const entry of JAILED_CLI_SPAWN_DEBT) { const f = join(repoRoot(), 'modules', entry.module, entry.file); expect( existsSync(f), `${entry.module}/${entry.file} no longer exists — delete its debt entry`, ).toBe(true); const hits = scanModuleScriptSource(entry.file, readFileSync(f, 'utf-8')).filter( (v) => v.rule === JAILED_CLI_SPAWN_RULE, ); expect( hits.length, `${entry.module}/${entry.file} carries ${hits.length} jailed-CLI spawns, the debt entry pins ${entry.matches} (${entry.reason}) — convert or update the entry`, ).toBe(entry.matches); } }); test('the debt list is non-trivial (the guard actually reached the entries)', () => { // Was >5 while the list still carried the hook-owned-state 5.5 batch. The // conversions retire entries faster than new debt arrives (ce-y0we), so // the floor is now "at least one tracked site" — enough to prove the test // reached the list without pinning the retirement schedule. expect(JAILED_CLI_SPAWN_DEBT.length).toBeGreaterThan(0); }); }); describe('recurrence gate: @celilo/capabilities itself never hand-builds SSH', () => { // The workspace source — where the package is authored, and the only copy a // commit in this repo can fix. Each module's bundled copy is an npm snapshot // that refreshes on the next publish, so the gate reads the source of truth. const capabilitiesSrc = join(repoRoot(), 'packages', 'capabilities', 'src'); test('scans the package source except the remote-primitive file, and the exclusion engaged', () => { // Reach probe, not reasoning: the primitive file must exist and be absent // from the scanned set, or the narrowing below is proving nothing. const primitivePresent = REMOTE_PRIMITIVE_FILES.every((f) => existsSync(join(capabilitiesSrc, f)), ); expect(primitivePresent).toBe(true); const scannedFiles = capabilityPackageSourceFiles(capabilitiesSrc); for (const primitive of REMOTE_PRIMITIVE_FILES) { expect(scannedFiles).not.toContain(primitive); } expect(scannedFiles.length).toBeGreaterThan(10); expect(scannedFiles).toContain('public-web.ts'); }); test('no file outside the remote-primitive seam hand-builds SSH', () => { const violations = scanCapabilityPackageSource(capabilitiesSrc); expect( violations, `Capability package policy violations:\n${formatViolations(violations)}`, ).toEqual([]); }); });