import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createManifest, readManifest, writeManifest, manifestPath, heuristicRuntimeAssetDests, DEFAULT_SCOPE_LINE, MANIFEST_VERSION, } from './install-manifest.js'; // ─── helpers ───────────────────────────────────────────────────────────────── let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-manifest-test-')); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); // ─── createManifest ─────────────────────────────────────────────────────────── describe('createManifest', () => { it('creates a valid manifest with version 1', () => { const m = createManifest('0.0.24', 2); expect(m.version).toBe(MANIFEST_VERSION); expect(m.cliVersion).toBe('0.0.24'); expect(m.frameworkVersion).toBe(2); }); it('sets installedAt to an ISO-8601 date string', () => { const before = new Date(); const m = createManifest('0.0.24', 2); const after = new Date(); const ts = new Date(m.installedAt); expect(ts.getTime()).toBeGreaterThanOrEqual(before.getTime()); expect(ts.getTime()).toBeLessThanOrEqual(after.getTime()); }); it('starts with empty mutation arrays', () => { const m = createManifest('0.0.24', 2); expect(m.mutations.directories).toHaveLength(0); expect(m.mutations.npmGlobalPackages).toHaveLength(0); expect(m.mutations.npmrcLines).toHaveLength(0); expect(m.mutations.shellProfileEdits).toHaveLength(0); expect(m.mutations.runtimeAssetCopies).toHaveLength(0); }); it('merges partial mutations', () => { const m = createManifest('0.0.24', 2, { npmGlobalPackages: ['@mosaicstack/mosaic'], }); expect(m.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']); expect(m.mutations.directories).toHaveLength(0); }); }); // ─── manifestPath ───────────────────────────────────────────────────────────── describe('manifestPath', () => { it('returns mosaicHome/.install-manifest.json', () => { const p = manifestPath('/home/user/.config/mosaic'); expect(p).toBe('/home/user/.config/mosaic/.install-manifest.json'); }); }); // ─── writeManifest / readManifest round-trip ───────────────────────────────── describe('writeManifest + readManifest', () => { it('round-trips a manifest through disk', () => { const m = createManifest('0.0.24', 2, { npmGlobalPackages: ['@mosaicstack/mosaic'], npmrcLines: [DEFAULT_SCOPE_LINE], }); writeManifest(tmpDir, m); const loaded = readManifest(tmpDir); expect(loaded).toBeDefined(); expect(loaded!.version).toBe(1); expect(loaded!.cliVersion).toBe('0.0.24'); expect(loaded!.mutations.npmGlobalPackages).toEqual(['@mosaicstack/mosaic']); expect(loaded!.mutations.npmrcLines).toEqual([DEFAULT_SCOPE_LINE]); }); it('preserves runtimeAssetCopies with backup path', () => { const m = createManifest('0.0.24', 2, { runtimeAssetCopies: [ { source: '/src/settings.json', dest: '/home/user/.claude/settings.json', backup: '/home/user/.claude/settings.json.mosaic-bak-20260405120000', }, ], }); writeManifest(tmpDir, m); const loaded = readManifest(tmpDir); const copies = loaded!.mutations.runtimeAssetCopies; expect(copies).toHaveLength(1); expect(copies[0]!.backup).toBe('/home/user/.claude/settings.json.mosaic-bak-20260405120000'); }); }); // ─── readManifest — missing / invalid ──────────────────────────────────────── describe('readManifest error cases', () => { it('returns undefined when the file does not exist', () => { expect(readManifest('/nonexistent/path')).toBeUndefined(); }); it('returns undefined when the file contains invalid JSON', () => { const { writeFileSync } = require('node:fs'); writeFileSync(join(tmpDir, '.install-manifest.json'), 'not json', 'utf8'); expect(readManifest(tmpDir)).toBeUndefined(); }); it('returns undefined when version field is wrong', () => { const { writeFileSync } = require('node:fs'); writeFileSync( join(tmpDir, '.install-manifest.json'), JSON.stringify({ version: 99, installedAt: new Date().toISOString(), cliVersion: '1', frameworkVersion: 1, mutations: {}, }), 'utf8', ); expect(readManifest(tmpDir)).toBeUndefined(); }); }); // ─── heuristicRuntimeAssetDests ────────────────────────────────────────────── describe('heuristicRuntimeAssetDests', () => { it('returns a non-empty list of absolute paths', () => { const dests = heuristicRuntimeAssetDests('/home/user'); expect(dests.length).toBeGreaterThan(0); for (const d of dests) { expect(d).toMatch(/^\/home\/user\//); } }); it('includes the claude settings.json path', () => { const dests = heuristicRuntimeAssetDests('/home/user'); expect(dests).toContain('/home/user/.claude/settings.json'); }); }); // ─── DEFAULT_SCOPE_LINE ─────────────────────────────────────────────────────── describe('DEFAULT_SCOPE_LINE', () => { it('contains the mosaicstack registry URL', () => { expect(DEFAULT_SCOPE_LINE).toContain('mosaicstack'); expect(DEFAULT_SCOPE_LINE).toContain('@mosaicstack:registry='); }); });