import fs from 'fs'; import path from 'path'; import { afterEach, describe, expect, it } from 'vitest'; import { detectExistingInstall, resolveInstallMode } from './install-mode.js'; import { SERVICE_NAME } from '../src/platform-identity.js'; function makeProjectRoot(): string { const base = path.join(process.cwd(), 'tmp', 'tests'); fs.mkdirSync(base, { recursive: true }); return fs.mkdtempSync(path.join(base, 'clawdie-install-mode-')); } function removeDir(dirPath: string): void { fs.rmSync(dirPath, { recursive: true, force: true }); } describe('detectExistingInstall', () => { const roots: string[] = []; afterEach(() => { while (roots.length > 0) { removeDir(roots.pop()!); } }); it('treats two soft signals as an existing install', () => { const root = makeProjectRoot(); roots.push(root); fs.writeFileSync(path.join(root, '.env'), 'OPENROUTER_API_KEY=test\n'); fs.mkdirSync(path.join(root, 'groups'), { recursive: true }); fs.writeFileSync(path.join(root, 'groups', 'ops.json'), '{}'); const detection = detectExistingInstall(root, { existsSync: (target) => fs.existsSync(target) && target !== `/usr/local/etc/rc.d/${SERVICE_NAME}`, commandExists: () => false, execFileSync: (() => { throw new Error('not called'); }) as typeof import('child_process').execFileSync, }); expect(detection.existing).toBe(true); expect(detection.softSignals).toEqual(['env', 'groups']); expect(detection.strongSignals).toEqual([]); expect(detection.installUuid).toBeNull(); }); it('treats a strong signal as an existing install', () => { const root = makeProjectRoot(); roots.push(root); const detection = detectExistingInstall(root, { existsSync: (target) => target === `/usr/local/etc/rc.d/${SERVICE_NAME}` || fs.existsSync(target), commandExists: () => false, execFileSync: (() => { throw new Error('not called'); }) as typeof import('child_process').execFileSync, }); expect(detection.existing).toBe(true); expect(detection.strongSignals).toContain('service'); expect(detection.installUuid).toBeNull(); }); it('prefers system.env zfs pool/prefix when matching datasets', () => { const root = makeProjectRoot(); roots.push(root); fs.writeFileSync( path.join(root, 'system.env'), ['ZFS_POOL=tank', 'ZFS_PREFIX=alpha-runtime', ''].join('\n'), ); const detection = detectExistingInstall(root, { existsSync: (target) => fs.existsSync(target) && target !== `/usr/local/etc/rc.d/${SERVICE_NAME}`, commandExists: (name) => name === 'zfs', execFileSync: ((cmd: string) => { if (cmd === 'zfs') return 'tank/alpha-runtime\ntank/alpha-runtime/pgdata\n'; throw new Error('unexpected command'); }) as typeof import('child_process').execFileSync, }); expect(detection.existing).toBe(true); expect(detection.strongSignals).toContain('zfs'); expect(detection.rootDataset).toBe('tank/alpha-runtime'); }); it('treats persisted zfs install metadata as a strong signal', () => { const root = makeProjectRoot(); roots.push(root); fs.writeFileSync( path.join(root, 'system.env'), ['ZFS_POOL=tank', 'ZFS_PREFIX=alpha-runtime', ''].join('\n'), ); const detection = detectExistingInstall(root, { existsSync: (target) => fs.existsSync(target) && target !== `/usr/local/etc/rc.d/${SERVICE_NAME}`, commandExists: (name) => name === 'zfs', execFileSync: ((cmd: string, args: string[]) => { if (cmd !== 'zfs') throw new Error('unexpected command'); if (args[0] === 'list') return ''; if (args[0] === 'get') return 'abc123\n'; throw new Error('unexpected zfs args'); }) as typeof import('child_process').execFileSync, }); expect(detection.existing).toBe(true); expect(detection.strongSignals).toContain('zfs-metadata'); expect(detection.installUuid).toBe('abc123'); }); it('returns fresh when no signals are present', () => { const root = makeProjectRoot(); roots.push(root); const detection = detectExistingInstall(root, { existsSync: (target) => fs.existsSync(target) && target !== `/usr/local/etc/rc.d/${SERVICE_NAME}`, commandExists: () => false, execFileSync: (() => { throw new Error('not called'); }) as typeof import('child_process').execFileSync, }); expect(detection.existing).toBe(false); }); }); describe('resolveInstallMode', () => { const freshDetection = { signals: { envFile: false, groupsDir: false, serviceFile: false, zfsDataset: false, zfsMetadata: false, runtimeUser: false, }, strongSignals: [], softSignals: [], existing: false, rootDataset: null, installUuid: null, }; const existingDetection = { signals: { envFile: true, groupsDir: true, serviceFile: false, zfsDataset: true, zfsMetadata: false, runtimeUser: false, }, strongSignals: ['zfs'], softSignals: ['env', 'groups'], existing: true, rootDataset: 'zroot/clawdie-runtime', installUuid: null, }; it('resolves auto to fresh when nothing exists', () => { expect(resolveInstallMode('auto', freshDetection).effective).toBe('fresh'); }); it('resolves auto to upgrade when an install exists', () => { expect(resolveInstallMode('auto', existingDetection).effective).toBe( 'upgrade', ); }); it('rejects fresh against an existing install', () => { expect(() => resolveInstallMode('fresh', existingDetection)).toThrow( /INSTALL_MODE=fresh/, ); }); it('rejects upgrade when no install exists', () => { expect(() => resolveInstallMode('upgrade', freshDetection)).toThrow( /INSTALL_MODE=upgrade/, ); }); it('rejects rescue when no install exists', () => { expect(() => resolveInstallMode('rescue', freshDetection)).toThrow( /INSTALL_MODE=rescue/, ); }); });