import { beforeEach, describe, expect, it, vi } from 'vitest'; const fsMocks = vi.hoisted(() => ({ mkdirSync: vi.fn(), writeFileSync: vi.fn(), existsSync: vi.fn(() => false), readFileSync: vi.fn(() => ''), appendFileSync: vi.fn(), })); const bastilleMocks = vi.hoisted(() => ({ bastille: vi.fn(), bastilleTimed: vi.fn(), jailExists: vi.fn(), jailRoot: vi.fn(), detectFreeBSDRelease: vi.fn(), })); const packageMocks = vi.hoisted(() => ({ loadPackageList: vi.fn(), mountPkgCacheInJail: vi.fn(), })); const statusMocks = vi.hoisted(() => ({ emitStatus: vi.fn(), })); const childProcessMocks = vi.hoisted(() => ({ execSync: vi.fn(), spawnSync: vi.fn(), })); const tailscaleMocks = vi.hoisted(() => ({ maybeEnableTailscaleInJail: vi.fn(), })); const envMocks = vi.hoisted(() => ({ readEnvFile: vi.fn(), })); vi.mock('child_process', () => childProcessMocks); vi.mock('fs', async () => { const actual = await vi.importActual('fs'); const mocked = { ...actual, ...fsMocks }; return { ...mocked, default: mocked }; }); vi.mock('./bastille-helpers.js', () => bastilleMocks); vi.mock('./packages.js', () => packageMocks); vi.mock('./status.js', () => statusMocks); vi.mock('./tailscale.js', () => tailscaleMocks); vi.mock('../src/env.js', () => envMocks); vi.mock('./platform.js', () => ({ getPlatform: vi.fn(() => 'freebsd'), })); vi.mock('../src/logger.js', () => ({ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), }, })); function mockConfig(overrides: Record = {}) { vi.doMock('../src/config.js', () => ({ CODE_SERVICE_INTERNAL_DOMAIN: 'git.home.arpa', CODE_HOSTING_MODE: 'local', FEATURE_GIT: true, GIT_DEFAULT_REPO_NAME: 'alpha-ai.git', GIT_JAIL_NAME: 'alpha-git', GIT_MIRROR_URLS: [], GIT_JAIL_IP: '10.0.0.6', GIT_STORAGE_ROOT: '/srv/git', PLATFORM_RUNTIME_HOME: '/home/alpha', REMOTE_GIT_URL: '', SUBNET_BASE: '10.0.0', ...overrides, })); } function ok(output = '') { return { ok: true, output }; } describe('setup/git.ts', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); bastilleMocks.bastille.mockReturnValue(ok()); bastilleMocks.bastilleTimed.mockReturnValue(ok()); bastilleMocks.jailExists.mockReturnValue(true); bastilleMocks.jailRoot.mockReturnValue('/jails/alpha-git/root'); bastilleMocks.detectFreeBSDRelease.mockReturnValue('15.0-RELEASE'); packageMocks.loadPackageList.mockReturnValue(['git', 'rsync']); packageMocks.mountPkgCacheInJail.mockImplementation(() => {}); tailscaleMocks.maybeEnableTailscaleInJail.mockImplementation(() => {}); envMocks.readEnvFile.mockReturnValue({}); childProcessMocks.execSync.mockImplementation(() => Buffer.from('')); fsMocks.existsSync.mockReturnValue(false); }); it('skips when git feature is disabled', async () => { mockConfig({ FEATURE_GIT: false }); const mod = await import('./git.js'); await mod.run([]); expect(statusMocks.emitStatus).toHaveBeenCalledWith( 'SETUP_GIT', expect.objectContaining({ STATUS: 'skipped', REASON: 'feature_disabled', }), ); expect(bastilleMocks.bastille).not.toHaveBeenCalled(); }); it('falls back to git init --bare when mirror clone fails', async () => { mockConfig({ REMOTE_GIT_URL: 'https://codeberg.org/Clawdie/pi.git' }); bastilleMocks.bastille.mockImplementation((...args: string[]) => { if ( args[0] === 'cmd' && args[1] === 'alpha-git' && args[2] === 'test' && args[3] === '-d' ) { return { ok: false, output: '' }; } if ( args[0] === 'cmd' && args.includes('git') && args.includes('--version') ) { return ok('git version 2.46.0'); } if (args[0] === 'cmd' && args.includes('rev-parse')) { return ok('true\n'); } return ok(); }); bastilleMocks.bastilleTimed.mockReturnValue({ ok: false, output: 'mirror failed', }); const mod = await import('./git.js'); await mod.run([]); expect(bastilleMocks.bastilleTimed).toHaveBeenCalledWith( expect.any(Number), 'cmd', 'alpha-git', 'git', 'clone', '--mirror', 'https://codeberg.org/Clawdie/pi.git', '/srv/git/alpha-ai.git', ); expect(bastilleMocks.bastille).toHaveBeenCalledWith( 'cmd', 'alpha-git', 'git', 'init', '--bare', '/srv/git/alpha-ai.git', ); expect(statusMocks.emitStatus).toHaveBeenCalledWith( 'SETUP_GIT', expect.objectContaining({ STATUS: 'success' }), ); }); it('clones only unique extra mirror repos', async () => { mockConfig({ REMOTE_GIT_URL: 'https://codeberg.org/Clawdie/pi.git', GIT_MIRROR_URLS: [ 'https://codeberg.org/Clawdie/pi.git', 'git@codeberg.org:Clawdie/docs.git', ], }); bastilleMocks.bastille.mockImplementation((...args: string[]) => { if ( args[0] === 'cmd' && args[1] === 'alpha-git' && args[2] === 'test' && args[3] === '-d' ) { return { ok: false, output: '' }; } if ( args[0] === 'cmd' && args.includes('git') && args.includes('--version') ) { return ok('git version 2.46.0'); } if (args[0] === 'cmd' && args.includes('rev-parse')) { return ok('true\n'); } return ok(); }); const mod = await import('./git.js'); await mod.run([]); const cloneCalls = bastilleMocks.bastilleTimed.mock.calls; expect(cloneCalls).toHaveLength(2); expect(cloneCalls[0].slice(1)).toEqual([ 'cmd', 'alpha-git', 'git', 'clone', '--mirror', 'https://codeberg.org/Clawdie/pi.git', '/srv/git/alpha-ai.git', ]); expect(cloneCalls[1].slice(1)).toEqual([ 'cmd', 'alpha-git', 'git', 'clone', '--mirror', 'git@codeberg.org:Clawdie/docs.git', '/srv/git/docs.git', ]); }); });