import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadManifest, resolveOwnership, frameworkSubtreeRoots } from './manifest.js'; /** * Bash ↔ TS parity (#791, §6.1). * * The installer (bash) and the config adapter (TS) each resolve path ownership * from framework-manifest.txt. If the two resolvers disagreed on a single path, * an upgrade could protect a file on one code path and wipe it on the other — * exactly the two-copies drift that #631 patched by hand. This test drives the * bash resolver (`tools/_lib/manifest.sh`) as a subprocess and asserts it agrees * with the TS resolver for a broad set of paths spanning every ownership class. */ const FRAMEWORK_ROOT = fileURLToPath(new URL('../../framework', import.meta.url)); const MANIFEST_SH = join(FRAMEWORK_ROOT, 'tools', '_lib', 'manifest.sh'); const hasBash = (() => { try { execFileSync('bash', ['-c', 'true'], { stdio: 'ignore' }); return true; } catch { return false; } })(); function bashResolve(relPath: string): string { return execFileSync('bash', [MANIFEST_SH, 'resolve', relPath], { encoding: 'utf-8', }).trim(); } function bashSubtreeRoots(): string[] { return execFileSync('bash', [MANIFEST_SH, 'subtree-roots'], { encoding: 'utf-8' }) .split('\n') .map((s) => s.trim()) .filter((s) => s.length > 0); } // Paths spanning every ownership class: framework single-files, framework // subtrees, operator declared trees, operator carve-out inside a framework // subtree, local overlays, and deliberately UNANTICIPATED paths (fail-safe). const PROBE_PATHS = [ 'CONSTITUTION.md', 'AGENTS.md', 'STANDARDS.md', 'install.sh', 'framework-manifest.txt', 'guides/E2E-DELIVERY.md', 'tools/git/pr-create.sh', 'tools/_lib/manifest.sh', 'defaults/SOUL.md', 'fleet/README.md', 'fleet/roles/coder.md', 'fleet/roster.schema.json', 'fleet/examples/general.yaml', // operator 'SOUL.md', 'USER.md', 'TOOLS.md', 'SOUL.local.md', 'USER.local.md', 'STANDARDS.local.md', 'agents/coder0.conf', 'policy/custom.md', 'memory/note.md', 'sources/skills/x.md', 'credentials/c.json', 'tools/_lib/credentials.json', 'fleet/roster.yaml', 'fleet/roster.json', 'fleet/agents/coder0.env', 'fleet/run/coder0.hb', 'fleet/backlog/data.db', 'fleet/roles.local/custom.md', // unanticipated → operator (fail-safe) 'harvester/sop.md', 'unknown-operator-dir/x', 'fleet/my-fleet.yaml', 'random-root-file.md', 'tools/some-new-framework-tool.sh', ]; describe.skipIf(!hasBash)('bash ↔ TS manifest parity (§6.1)', () => { it('the bash resolver CLI exists and is executable', () => { expect(existsSync(MANIFEST_SH)).toBe(true); }); it('bash and TS resolve identical ownership for every probe path', () => { const manifest = loadManifest(FRAMEWORK_ROOT); const disagreements: Array<{ path: string; ts: string; bash: string }> = []; for (const p of PROBE_PATHS) { const ts = resolveOwnership(manifest, p); const bash = bashResolve(p); if (ts !== bash) disagreements.push({ path: p, ts, bash }); } expect(disagreements).toEqual([]); }); it('bash and TS agree on the framework subtree roots', () => { const manifest = loadManifest(FRAMEWORK_ROOT); expect(bashSubtreeRoots().sort()).toEqual(frameworkSubtreeRoots(manifest).sort()); }); });