Files
portal/app/vite-plugins/dev-indices/__tests__/manifest.test.ts
T

122 lines
4.2 KiB
TypeScript

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);
});
});