import { describe, it, expect, beforeAll } from 'vitest'; import { execFileSync } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; /** * Story 6.8 AC-3 — a failing item makes the exit status non-zero, so an agent can gate on it. * * ⛔ This had **zero** automated coverage: `grep -rn "exitCode\|process.exit" --include="*.spec.ts"` matched nothing in * the whole repository, and the story's task read "`failed` → non-zero (AC-3)" — conflating the tested boolean with the * untested exit code. The pure function's spec proves `failed` is computed correctly; only running the command proves * anything about the process. * * It runs the BUILT cli, because the exit code is a property of the process and nothing else. */ const CLI = join(process.cwd(), 'apps/cli/dist/main.js'); /** The exit code, and stdout, for `hexasync env` in a given directory. */ function env(cwd: string, ...args: string[]): { code: number; out: string } { try { const out = execFileSync('node', [CLI, 'env', ...args], { cwd, encoding: 'utf8', stdio: 'pipe', }); return { code: 0, out }; } catch (error) { const e = error as { status?: number; stdout?: string }; return { code: e.status ?? 1, out: String(e.stdout ?? '') }; } } /** A workspace with an installed surface at a given bundle, and a manifest at another. */ function workspace(surfaceBundle?: string, manifestBundle?: number): string { const dir = mkdtempSync(join(tmpdir(), 'env-exit-')); mkdirSync(join(dir, 'partials'), { recursive: true }); writeFileSync(join(dir, 'partials', 'main.yaml'), 'id: x\n'); if (surfaceBundle !== undefined) { mkdirSync(join(dir, '.hexasync', 'intellisense', 'docs'), { recursive: true, }); writeFileSync( join(dir, '.hexasync', 'intellisense', 'docs', 'AI-INDEX.md'), `# index\n\n**Asset bundle ${surfaceBundle} · extension 1.0.**\n`, ); } if (manifestBundle !== undefined) { mkdirSync(join(dir, '.hexasync', 'intellisense'), { recursive: true }); writeFileSync( join(dir, '.hexasync', 'intellisense', '.install-manifest.json'), JSON.stringify({ generator: 't', assetBundleVersion: manifestBundle, installedAt: 'now', files: [], }), ); } return dir; } describe('hexasync env — the exit code (AC-3)', () => { beforeAll(() => { // A stale `dist/` has misled reviewers in this repo before, so say plainly when it is missing. expect(existsSync(CLI), `${CLI} is not built — run \`yarn build\``).toBe( true, ); }); it('exits 0 when everything checks out', () => { const dir = workspace('51', 51); try { const { code, out } = env(dir); expect(out).toMatch(/Everything checks out/); expect(code).toBe(0); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('⛔ exits 1 on a STALE surface, and names both numbers', () => { const dir = workspace('44', 51); try { const { code, out } = env(dir); expect(code).toBe(1); expect(out).toMatch(/STALE/); expect(out).toContain('44'); expect(out).toContain('51'); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('exits 1 when the surface is not installed, and says what to run', () => { const dir = workspace(undefined, undefined); try { const { code, out } = env(dir); expect(code).toBe(1); expect(out).toMatch(/Install IntelliSense/); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('⛔ exits 0 with NO project, because n/a is not a failure (AC-4)', () => { /** * The distinction the story rests on: an exit code that fires on "I could not evaluate this" trains every caller to * ignore it, which is the same as not having one. So a bare directory must still run, still report, and still exit 0. */ const dir = mkdtempSync(join(tmpdir(), 'env-noproj-')); try { const { code, out } = env(dir); // Nothing installed here either, so the surface item fails — that is the 1. What must NOT contribute is the // absent project. expect(out).toMatch(/A HexaSync project is open/); expect(out).toMatch(/could not be evaluated/); expect(code).toBe(1); // With the surface present and no project, nothing fails at all. const withSurface = mkdtempSync(join(tmpdir(), 'env-surface-')); try { mkdirSync(join(withSurface, '.hexasync', 'intellisense', 'docs'), { recursive: true, }); writeFileSync( join(withSurface, '.hexasync', 'intellisense', 'docs', 'AI-INDEX.md'), '# index\n\n**Asset bundle 51 · extension 1.0.**\n', ); const bare = env(withSurface); expect(bare.out).toMatch(/could not be evaluated/); expect(bare.code).toBe(0); } finally { rmSync(withSurface, { recursive: true, force: true }); } } finally { rmSync(dir, { recursive: true, force: true }); } }); it('--json emits stable per-item ids for an agent to gate on', () => { const dir = workspace('44', 51); try { const { code, out } = env(dir, '--json'); const report = JSON.parse(out) as { failed: boolean; items: { id: string; outcome: string }[]; }; expect(code).toBe(1); expect(report.failed).toBe(true); expect(report.items.map((item) => item.id)).toEqual([ 'project', 'surface', 'surface-current', 'manifest', ]); expect( report.items.find((item) => item.id === 'surface-current')!.outcome, ).toBe('fail'); } finally { rmSync(dir, { recursive: true, force: true }); } }); });