/** * Tests for cross-module-read materialization + atomic write-back. * * Read side: verify that materializeCrossModuleRoot mirrors every * OTHER deployed module's terraform/ dir + writes an index.json. * Write side: verify applyCrossModuleWriteRoot atomically rotates * staged dirs into live storage, and rolls back on mid-loop failure. */ import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { closeDb, getDb } from '../db/client'; import { runMigrations } from '../db/migrate'; import { modules } from '../db/schema'; import type { ModuleManifest } from '../manifest/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { type CrossModuleIndex, applyCrossModuleWriteRoot, materializeCrossModuleRoot, moduleHasCrossModuleRead, } from './cross-module-read'; function buildManifest(caps: Array<{ name: string; version: string }>): ModuleManifest { return { celilo_contract: '1.0', id: 'm', name: 'm', version: '0.0.1', requires: { capabilities: caps }, } as ModuleManifest; } describe('moduleHasCrossModuleRead', () => { it('returns false for manifests with no capabilities', () => { expect(moduleHasCrossModuleRead(buildManifest([]))).toBe(false); }); it('returns false for manifests with other capabilities', () => { expect(moduleHasCrossModuleRead(buildManifest([{ name: 'public_web', version: '^3.0' }]))).toBe( false, ); }); it('returns true when cross_module_read is declared in requires', () => { expect( moduleHasCrossModuleRead(buildManifest([{ name: 'cross_module_read', version: '^1.0' }])), ).toBe(true); }); it('returns true when cross_module_read is declared in optional', () => { const manifest = { ...buildManifest([]), optional: { capabilities: [{ name: 'cross_module_read', version: '^1.0' }] }, } as ModuleManifest; expect(moduleHasCrossModuleRead(manifest)).toBe(true); }); }); describe('materializeCrossModuleRoot', () => { let dir: string; beforeEach(async () => { dir = mkdtempSync(join(tmpdir(), 'celilo-xmr-test-')); process.env.CELILO_DB_PATH = join(dir, 'celilo.db'); process.env.CELILO_DATA_DIR = dir; await runMigrations(process.env.CELILO_DB_PATH); }); afterEach(() => { closeDb(); resetTestDbPath(); delete process.env.CELILO_DATA_DIR; try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); function plantDeployedModule(id: string, version: string, tfstateContent: string): void { const db = getDb(); db.insert(modules) .values({ id, name: id, version, sourcePath: join(dir, 'sources', id), manifestData: { id, name: id, version, celilo_contract: '1.0', requires: {} } as Record< string, unknown >, }) .run(); const tfDir = join(dir, 'modules', id, 'generated', 'terraform'); mkdirSync(tfDir, { recursive: true }); writeFileSync(join(tfDir, 'terraform.tfstate'), tfstateContent); } it("mirrors every other deployed module's terraform/ + writes index.json", () => { plantDeployedModule('homebridge', '0.1.0', '{"state":"homebridge"}'); plantDeployedModule('caddy', '0.2.0', '{"state":"caddy"}'); plantDeployedModule('celilo-mgmt', '0.3.0', '{"state":"mgmt"}'); const root = join(dir, 'cross-module-root'); materializeCrossModuleRoot(root, 'celilo-mgmt'); // index.json lists the OTHER modules, not celilo-mgmt const index: CrossModuleIndex = JSON.parse(readFileSync(join(root, 'index.json'), 'utf-8')); expect(index.schemaVersion).toBe('1.0'); expect(index.modules.map((m) => m.id).sort()).toEqual(['caddy', 'homebridge']); // Mirrored TF state is reachable + matches the live content. const mirroredHomebridge = readFileSync( join(root, 'modules', 'homebridge', 'terraform', 'terraform.tfstate'), 'utf-8', ); expect(mirroredHomebridge).toBe('{"state":"homebridge"}'); expect(existsSync(join(root, 'modules', 'celilo-mgmt'))).toBe(false); }); it('mirrors state + lock file but EXCLUDES the .terraform provider cache (ISS-0015)', () => { plantDeployedModule('caddy', '0.2.0', '{"state":"caddy"}'); // Plant a realistic terraform dir: state + lock file (must travel) plus a // .terraform/providers/ binary (must NOT — re-fetchable, ~19 MB each). const tfDir = join(dir, 'modules', 'caddy', 'generated', 'terraform'); writeFileSync(join(tfDir, '.terraform.lock.hcl'), 'provider "telmate/proxmox" {}'); const providerDir = join(tfDir, '.terraform', 'providers', 'registry.terraform.io', 'telmate'); mkdirSync(providerDir, { recursive: true }); writeFileSync(join(providerDir, 'terraform-provider-proxmox'), 'BINARY'.repeat(1000)); const root = join(dir, 'cross-module-root'); materializeCrossModuleRoot(root, 'celilo-mgmt'); const mirrored = join(root, 'modules', 'caddy', 'terraform'); expect(existsSync(join(mirrored, 'terraform.tfstate'))).toBe(true); expect(existsSync(join(mirrored, '.terraform.lock.hcl'))).toBe(true); // The provider cache must not be in the envelope. expect(existsSync(join(mirrored, '.terraform'))).toBe(false); }); it('skips modules with no terraform.tfstate (not deployed)', () => { plantDeployedModule('caddy', '0.2.0', '{"state":"caddy"}'); // imported-but-not-deployed: row exists but no tf state on disk const db = getDb(); db.insert(modules) .values({ id: 'half-installed', name: 'half-installed', version: '0.0.1', sourcePath: join(dir, 'sources', 'half-installed'), manifestData: {} as Record, }) .run(); const root = join(dir, 'cross-module-root'); materializeCrossModuleRoot(root, 'celilo-mgmt'); const index: CrossModuleIndex = JSON.parse(readFileSync(join(root, 'index.json'), 'utf-8')); expect(index.modules.map((m) => m.id)).toEqual(['caddy']); }); it('produces an empty index when no other modules are deployed', () => { plantDeployedModule('celilo-mgmt', '0.3.0', '{}'); const root = join(dir, 'cross-module-root'); materializeCrossModuleRoot(root, 'celilo-mgmt'); const index: CrossModuleIndex = JSON.parse(readFileSync(join(root, 'index.json'), 'utf-8')); expect(index.modules).toHaveLength(0); }); }); describe('applyCrossModuleWriteRoot', () => { let dir: string; beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'celilo-xmw-test-')); process.env.CELILO_DATA_DIR = dir; }); afterEach(() => { delete process.env.CELILO_DATA_DIR; try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); function stage(stagingRoot: string, moduleId: string, fileName: string, content: string): void { const tfDir = join(stagingRoot, 'modules', moduleId, 'terraform'); mkdirSync(tfDir, { recursive: true }); writeFileSync(join(tfDir, fileName), content); } function plantLive(moduleId: string, fileName: string, content: string): void { const tfDir = join(dir, 'modules', moduleId, 'generated', 'terraform'); mkdirSync(tfDir, { recursive: true }); writeFileSync(join(tfDir, fileName), content); } function readLive(moduleId: string, fileName: string): string | null { const p = join(dir, 'modules', moduleId, 'generated', 'terraform', fileName); if (!existsSync(p)) return null; return readFileSync(p, 'utf-8'); } it('replaces live tf state with staged content', () => { plantLive('caddy', 'terraform.tfstate', 'old-state'); const stagingRoot = join(dir, 'staging'); mkdirSync(stagingRoot, { recursive: true }); stage(stagingRoot, 'caddy', 'terraform.tfstate', 'new-state'); const result = applyCrossModuleWriteRoot(stagingRoot); expect(result.applied).toEqual(['caddy']); expect(result.skipped).toEqual([]); expect(readLive('caddy', 'terraform.tfstate')).toBe('new-state'); // The .cross-module-restore-old sibling should be cleaned up. expect( existsSync(join(dir, 'modules', 'caddy', 'generated', 'terraform.cross-module-restore-old')), ).toBe(false); }); it('creates a fresh live dir when none existed before', () => { const stagingRoot = join(dir, 'staging'); mkdirSync(stagingRoot, { recursive: true }); stage(stagingRoot, 'new-module', 'terraform.tfstate', 'fresh'); const result = applyCrossModuleWriteRoot(stagingRoot); expect(result.applied).toEqual(['new-module']); expect(readLive('new-module', 'terraform.tfstate')).toBe('fresh'); }); it('reports skipped modules whose staged subtree has no terraform/ dir', () => { const stagingRoot = join(dir, 'staging'); mkdirSync(join(stagingRoot, 'modules', 'orphan'), { recursive: true }); const result = applyCrossModuleWriteRoot(stagingRoot); expect(result.skipped).toEqual(['orphan']); expect(result.applied).toEqual([]); }); it('is a no-op when staging root has no modules/ dir', () => { const stagingRoot = join(dir, 'empty-staging'); mkdirSync(stagingRoot, { recursive: true }); const result = applyCrossModuleWriteRoot(stagingRoot); expect(result.applied).toEqual([]); expect(result.skipped).toEqual([]); }); });