import { describe, it, expect } from 'vitest'; import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createEmpty, loadManifest, saveManifest, findBySignature, addEntry, markDeleted, } from '../manifest'; import type { ManifestEntry } from '../types'; const sampleEntry = (sig: string, file = 'resources/js/A.vue'): ManifestEntry => ({ file, line: 1, tag: 'v-btn', parentChain: ['A'], signature: sig, text: 'hello', key: null, ref: null, createdAt: '2026-05-12T00:00:00.000Z', }); describe('manifest', () => { it('createEmpty returns a fresh manifest with lastId=0', () => { const m = createEmpty(); expect(m.version).toBe(1); expect(m.lastId).toBe(0); expect(m.entries).toEqual({}); expect(m.deleted).toEqual({}); }); it('addEntry assigns next id and increments lastId', () => { const m = createEmpty(); const id1 = addEntry(m, sampleEntry('sig-1')); const id2 = addEntry(m, sampleEntry('sig-2')); expect(id1).toBe(1); expect(id2).toBe(2); expect(m.lastId).toBe(2); expect(m.entries['1'].signature).toBe('sig-1'); }); it('findBySignature returns matching id', () => { const m = createEmpty(); addEntry(m, sampleEntry('sig-a')); addEntry(m, sampleEntry('sig-b')); expect(findBySignature(m, 'sig-b')).toBe(2); expect(findBySignature(m, 'sig-x')).toBeNull(); }); it('markDeleted moves entry to deleted section, preserves lastId', () => { const m = createEmpty(); addEntry(m, sampleEntry('sig-x')); markDeleted(m, 1); expect(m.entries['1']).toBeUndefined(); expect(m.deleted['1'].lastSignature).toBe('sig-x'); expect(m.lastId).toBe(1); // monotonic, not decremented }); it('deleted ids are not reused by addEntry', () => { const m = createEmpty(); addEntry(m, sampleEntry('sig-x')); markDeleted(m, 1); const id = addEntry(m, sampleEntry('sig-y')); expect(id).toBe(2); }); it('saveManifest then loadManifest roundtrips data', () => { const dir = mkdtempSync(join(tmpdir(), 'dx-')); const path = join(dir, 'm.json'); const m = createEmpty(); addEntry(m, sampleEntry('sig-1')); saveManifest(path, m); const loaded = loadManifest(path); expect(loaded.lastId).toBe(1); expect(loaded.entries['1'].signature).toBe('sig-1'); rmSync(dir, { recursive: true }); }); it('loadManifest returns createEmpty() if file missing', () => { const m = loadManifest('/nonexistent/path-' + Math.random() + '.json'); expect(m.lastId).toBe(0); }); it('saveManifest writes atomically (no partial file on crash)', () => { const dir = mkdtempSync(join(tmpdir(), 'dx-')); const path = join(dir, 'm.json'); const m = createEmpty(); addEntry(m, sampleEntry('sig-x')); saveManifest(path, m); // Atomic write should leave no .tmp residue expect(() => readFileSync(path + '.tmp')).toThrow(); rmSync(dir, { recursive: true }); }); it('loadManifest throws on corrupt JSON with actionable message', () => { const dir = mkdtempSync(join(tmpdir(), 'dx-')); const path = join(dir, 'm.json'); writeFileSync(path, '{not valid json', 'utf8'); expect(() => loadManifest(path)).toThrow(/corrupt JSON/); rmSync(dir, { recursive: true }); }); it('loadManifest throws on unsupported version', () => { const dir = mkdtempSync(join(tmpdir(), 'dx-')); const path = join(dir, 'm.json'); writeFileSync(path, JSON.stringify({ version: 2, lastId: 0, entries: {}, deleted: {} }), 'utf8'); expect(() => loadManifest(path)).toThrow(/version 2 unsupported/); rmSync(dir, { recursive: true }); }); it('markDeleted is a no-op for unknown ids (does not throw)', () => { const m = createEmpty(); expect(() => markDeleted(m, 9999)).not.toThrow(); expect(m.deleted['9999']).toBeUndefined(); expect(m.lastId).toBe(0); }); });