Files
eden/tests/reference.test.ts
T

357 lines
26 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { mkdtemp, readFile, writeFile, cp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Database } from 'bun:sqlite';
import { ReferenceCompiler, SqliteReferenceRepository, readReferenceManifest } from '../src/lib/server/reference';
import { fixtureSources } from '../src/lib/server/reference/sources';
import type { ReferenceSourceAdapter } from '../src/lib/server/reference/types';
async function fixtureDirectory(): Promise<{ root: string; sources: string }> {
const root = await mkdtemp(join(tmpdir(), 'eden-reference-'));
const sources = join(root, 'fixtures');
await cp('fixtures/reference', sources, { recursive: true });
return { root, sources };
}
describe('fixture reference compiler', () => {
test('builds normalized schema and read-only lookup, including unknown IDs', async () => {
const { root, sources } = await fixtureDirectory();
try {
const result = await new ReferenceCompiler({ directory: join(root, 'reference'), profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(result.status).toBe('success');
if (result.status !== 'success' || !result.version) throw new Error('fixture build did not publish a version');
const resultVersion = result.version;
const repository = new SqliteReferenceRepository(join(root, 'reference'));
expect(await repository.types([34, 35, 587, 999999])).toEqual([
expect.objectContaining({ id: 34, name: 'Tritanium', categoryId: 4, groupId: 18, volume: 0.01 }),
expect.objectContaining({ id: 35, name: 'Pyerite', categoryId: 4, groupId: 18, volume: 0.01 }),
expect.objectContaining({ id: 587, name: 'Rifter', categoryId: 6, groupId: 25, volume: 2500 }),
]);
expect(await repository.version()).toBe(resultVersion);
repository.close();
const db = new Database(join(root, 'reference', 'databases', `${resultVersion}.sqlite`), { readonly: true });
for (const table of ['categories', 'groups', 'types', 'market_groups', 'blueprints', 'activities', 'materials', 'products', 'dogma_attributes', 'systems', 'regions', 'constellations', 'gates', 'stations']) expect(db.query(`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`).get(table)).toBeTruthy();
expect(db.query('SELECT COUNT(*) AS count FROM materials').get()).toEqual({ count: 2 });
expect(db.query('SELECT x,y,z FROM systems WHERE system_id=30000142').get()).toEqual({ x: -129064861735000000, y: 60755306910000000, z: 117469227060000000 });
db.close();
} finally { await rm(root, { recursive: true, force: true }); }
});
test('unchanged inputs are a no-op and a failed independent source cannot change ACTIVE', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const adapters = fixtureSources(sources);
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: adapters }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
const firstVersion = first.version;
const firstManifest = await readReferenceManifest(directory);
if (!firstManifest.active) throw new Error('missing active fixture manifest');
const hoboleaksCachePath = join(directory, firstManifest.active.sourceInputs.hoboleaks.cacheFile);
const referenceCachePath = join(directory, firstManifest.active.sourceInputs['reference-data'].cacheFile);
const hoboleaksCache = await readFile(hoboleaksCachePath);
const referenceCache = await readFile(referenceCachePath);
const noop = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(noop.status).toBe('noop'); expect(noop.version).toBe(firstVersion);
const failedHoboleaks: ReferenceSourceAdapter = { source: 'hoboleaks', async check() { throw new Error('Hoboleaks unavailable'); }, async load() { throw new Error('not reached'); } };
const failed = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), hoboleaks: failedHoboleaks } }).compile();
expect(failed.status).toBe('noop'); expect(failed.activeVersion).toBe(firstVersion);
const repository = new SqliteReferenceRepository(directory); expect(await repository.version()).toBe(firstVersion); repository.close();
expect(failed.sourceResults.hoboleaks?.status).toBe('failed');
expect(await readFile(hoboleaksCachePath)).toEqual(hoboleaksCache);
expect(await readFile(referenceCachePath)).toEqual(referenceCache);
const invalidReference: ReferenceSourceAdapter = {
source: 'reference-data',
async check() { return { source: 'reference-data', checkedAt: new Date().toISOString(), fingerprint: 'invalid-reference-fingerprint' }; },
async load(check) { return { source: 'reference-data', raw: { categories: {} }, bytes: new TextEncoder().encode('{"categories":{}}'), check }; },
};
const invalid = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), 'reference-data': invalidReference } }).compile();
expect(invalid.status).toBe('failed'); expect(invalid.activeVersion).toBe(firstVersion);
expect(invalid.sourceResults['reference-data']?.status).toBe('failed');
expect(await readFile(referenceCachePath)).toEqual(referenceCache);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('advances one source with the other source failure and records fallback provenance', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const initial = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(initial.status).toBe('success');
if (initial.status !== 'success' || !initial.version) throw new Error('initial build missing version');
const failHoboleaks: ReferenceSourceAdapter = { source: 'hoboleaks', async check() { throw new Error('Hoboleaks unavailable'); }, async load() { throw new Error('not reached'); } };
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const refAdvances = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), hoboleaks: failHoboleaks } }).compile();
expect(refAdvances.status).toBe('success');
if (refAdvances.status !== 'success' || !refAdvances.version) throw new Error('reference source did not advance');
expect(refAdvances.version).not.toBe(initial.version);
expect(refAdvances.sourceResults.hoboleaks?.status).toBe('failed');
expect(refAdvances.sourceResults.hoboleaks?.reused).toBe(true);
const afterReference = await readReferenceManifest(directory);
expect(afterReference.active?.sourceInputs.hoboleaks.fingerprint).toBe(afterReference.previous?.sourceInputs.hoboleaks.fingerprint);
const failReference: ReferenceSourceAdapter = { source: 'reference-data', async check() { throw new Error('Reference Data unavailable'); }, async load() { throw new Error('not reached'); } };
await writeFile(join(sources, 'hoboleaks-industryactivities.json'), `${await readFile(join(sources, 'hoboleaks-industryactivities.json'), 'utf8')}\n`);
const hoboAdvances = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), 'reference-data': failReference } }).compile();
expect(hoboAdvances.status).toBe('success');
expect(hoboAdvances.version).not.toBe(refAdvances.version);
expect(hoboAdvances.sourceResults['reference-data']?.status).toBe('failed');
expect(hoboAdvances.sourceResults['reference-data']?.reused).toBe(true);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('pins a reader across promotion and batches very large type requests', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial build missing version');
const pinned = new SqliteReferenceRepository(directory);
const request = Array.from({ length: 1501 }, (_, index) => index === 0 ? 34 : index === 1 ? 35 : index === 2 ? 587 : 900000 + index);
expect((await pinned.types(request)).map((type) => type.id)).toEqual([34, 35, 587]);
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(second.status).toBe('success');
if (second.status !== 'success' || !second.version) throw new Error('second build missing version');
expect(await pinned.version()).toBe(first.version);
expect((await pinned.types([34]))[0]?.name).toBe('Tritanium');
const fresh = new SqliteReferenceRepository(directory);
expect(await fresh.version()).toBe(second.version);
fresh.close(); pinned.close();
} finally { await rm(root, { recursive: true, force: true }); }
});
test('rejects a tampered active source cache without changing ACTIVE', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial build missing version');
const manifest = await readReferenceManifest(directory);
if (!manifest.active) throw new Error('missing active manifest');
const cachePath = join(directory, manifest.active.sourceInputs['reference-data'].cacheFile);
await writeFile(cachePath, '{"format":1,"source":"reference-data","fingerprint":"tampered"}');
const failed = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(failed.status).toBe('failed'); expect(failed.activeVersion).toBe(first.version);
expect(failed.sourceResults['reference-data']?.error).toContain('cache');
const repository = new SqliteReferenceRepository(directory); expect(await repository.version()).toBe(first.version); repository.close();
} finally { await rm(root, { recursive: true, force: true }); }
});
test('rejects tampered source-cache provenance metadata during reuse', async () => {
const fields: Array<[string, unknown]> = [
['canonicalSha256', 'tampered-canonical'],
['archiveByteLength', 1],
['checkedAt', '2099-01-01T00:00:00.000Z'],
['url', 'file:///tampered'],
['byteLength', 1],
['sourceRevision', 999],
['sourceBuild', 'tampered-build'],
['stale', true],
['profile', 'production'],
['importerVersion', 'tampered-importer'],
];
for (const [field, value] of fields) {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
const manifest = await readReferenceManifest(directory);
if (!manifest.active) throw new Error('missing active fixture manifest');
const cachePath = join(directory, manifest.active.sourceInputs['reference-data'].cacheFile);
const envelope = JSON.parse(await readFile(cachePath, 'utf8')) as Record<string, unknown>;
envelope[field] = value;
await writeFile(cachePath, JSON.stringify(envelope));
const failed = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(failed.status, field).toBe('failed');
expect(failed.activeVersion, field).toBe(first.version);
expect(failed.error, field).toMatch(/cache|metadata|checksum/);
} finally { await rm(root, { recursive: true, force: true }); }
}
});
test('rejects tampered fallback cache metadata and preserves ACTIVE', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const initial = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(initial.status).toBe('success');
if (initial.status !== 'success' || !initial.version) throw new Error('initial fixture build did not publish a version');
const manifest = await readReferenceManifest(directory);
if (!manifest.active) throw new Error('missing active fixture manifest');
const cachePath = join(directory, manifest.active.sourceInputs.hoboleaks.cacheFile);
const envelope = JSON.parse(await readFile(cachePath, 'utf8')) as Record<string, unknown>;
envelope.sourceBuild = 'tampered-build';
await writeFile(cachePath, JSON.stringify(envelope));
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const failedHoboleaks: ReferenceSourceAdapter = { source: 'hoboleaks', async check() { throw new Error('Hoboleaks unavailable'); }, async load() { throw new Error('not reached'); } };
const failed = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), hoboleaks: failedHoboleaks } }).compile();
expect(failed.status).toBe('failed');
expect(failed.activeVersion).toBe(initial.version);
expect(failed.sourceResults.hoboleaks?.reused).not.toBe(true);
expect((await readReferenceManifest(directory)).active?.version).toBe(initial.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('failed validation does not advance source success provenance', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial build missing version');
const before = await readReferenceManifest(directory);
const beforeSuccess = before.sources['reference-data']?.lastSuccessAt;
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const failed = await new ReferenceCompiler({ directory, profile: 'fixture', thresholds: { systemsMinimum: 2 }, sources: fixtureSources(sources) }).compile();
expect(failed.status).toBe('failed'); expect(failed.activeVersion).toBe(first.version);
const after = await readReferenceManifest(directory);
expect(after.sources['reference-data']?.lastSuccessAt).toBe(beforeSuccess);
expect(after.active?.version).toBe(first.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('rollback pins old source caches for a later fallback update', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial build missing version');
const firstManifest = await readReferenceManifest(directory);
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(second.status).toBe('success');
await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).rollback();
const failHoboleaks: ReferenceSourceAdapter = { source: 'hoboleaks', async check() { throw new Error('Hoboleaks unavailable'); }, async load() { throw new Error('not reached'); } };
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const updated = await new ReferenceCompiler({ directory, profile: 'fixture', sources: { ...fixtureSources(sources), hoboleaks: failHoboleaks } }).compile();
expect(updated.status).toBe('success');
const after = await readReferenceManifest(directory);
expect(after.active?.sourceInputs.hoboleaks.fingerprint).toBe(firstManifest.active?.sourceInputs.hoboleaks.fingerprint);
expect(after.active?.version).not.toBe(first.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('anchors reused cache metadata and no-ops after rollback-forward', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
const checkedReference = (checkedAt: string): Record<string, ReferenceSourceAdapter> => {
const base = fixtureSources(sources);
return {
...base,
'reference-data': {
source: 'reference-data',
async check() { return { ...(await base['reference-data'].check()), checkedAt }; },
async load(check) { return base['reference-data'].load(check); },
},
};
};
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: checkedReference('2026-09-19T00:00:01.000Z') }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: checkedReference('2026-09-19T00:00:02.000Z') }).compile();
expect(second.status).toBe('success');
if (second.status !== 'success' || !second.version) throw new Error('second fixture build did not publish a version');
await new ReferenceCompiler({ directory, profile: 'fixture', sources: checkedReference('2026-09-19T00:00:03.000Z') }).rollback();
const forward = await new ReferenceCompiler({ directory, profile: 'fixture', sources: checkedReference('2026-09-19T00:00:04.000Z') }).compile();
expect(forward.status).toBe('success');
if (forward.status !== 'success' || !forward.version) throw new Error('rollback-forward build did not publish a version');
const manifest = await readReferenceManifest(directory);
expect(manifest.active?.sourceInputs['reference-data'].checkedAt).toBe('2026-09-19T00:00:02.000Z');
const db = new Database(join(directory, 'databases', `${forward.version}.sqlite`), { readonly: true });
expect(db.query('SELECT checked_at,source_build FROM source_provenance WHERE source = ?').get('reference-data')).toEqual({ checked_at: '2026-09-19T00:00:02.000Z', source_build: null });
db.close();
const repeat = await new ReferenceCompiler({ directory, profile: 'fixture', sources: checkedReference('2026-09-19T00:00:05.000Z') }).compile();
expect(repeat.status).toBe('noop');
expect(repeat.activeVersion).toBe(forward.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('promotion retains PREVIOUS and rollback restores it', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
const firstVersion = first.version;
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(second.status).toBe('success');
if (second.status !== 'success' || !second.version) throw new Error('second fixture build did not publish a version');
const secondVersion = second.version;
expect(secondVersion).not.toBe(firstVersion);
const rollback = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).rollback();
expect(rollback.activeVersion).toBe(firstVersion); expect(rollback.previousVersion).toBe(secondVersion);
const repository = new SqliteReferenceRepository(directory); expect(await repository.version()).toBe(firstVersion); repository.close();
const rollForward = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(rollForward.status).toBe('success'); expect(rollForward.version).toBe(secondVersion);
const forwardReader = new SqliteReferenceRepository(directory); expect(await forwardReader.version()).toBe(secondVersion); forwardReader.close();
} finally { await rm(root, { recursive: true, force: true }); }
});
test('rejects a malformed PREVIOUS artifact before rollback', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(second.status).toBe('success');
if (second.status !== 'success' || !second.version) throw new Error('second fixture build did not publish a version');
const manifest = await readReferenceManifest(directory);
if (!manifest.previous) throw new Error('fixture build did not retain PREVIOUS');
const previousPath = join(directory, 'databases', `${manifest.previous.version}.sqlite`);
const malformed = new Database(previousPath);
malformed.run('DROP INDEX type_search_normalized_name_idx');
malformed.close();
await expect(new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).rollback()).rejects.toThrow(/incompatible schema version/);
expect((await readReferenceManifest(directory)).active?.version).toBe(second.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('rejects incomplete PREVIOUS count metadata before rollback', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success') throw new Error('initial fixture build did not publish');
await writeFile(join(sources, 'reference-data.json'), `${await readFile(join(sources, 'reference-data.json'), 'utf8')}\n`);
const second = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(second.status).toBe('success');
const manifest = await readReferenceManifest(directory);
if (!manifest.previous) throw new Error('fixture build did not retain PREVIOUS');
const malformed = { ...manifest, previous: { ...manifest.previous, counts: Object.fromEntries(Object.entries(manifest.previous.counts).filter(([key]) => key !== 'stations')) } };
await writeFile(join(directory, 'manifest.json'), `${JSON.stringify(malformed)}\n`);
await expect(new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).rollback()).rejects.toThrow(/missing count metadata/);
expect((await readReferenceManifest(directory)).active?.version).toBe(manifest.active?.version);
} finally { await rm(root, { recursive: true, force: true }); }
});
test('recovers a validated orphan database after manifest publication crash', async () => {
const { root, sources } = await fixtureDirectory();
const directory = join(root, 'reference');
try {
const first = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(first.status).toBe('success');
if (first.status !== 'success' || !first.version) throw new Error('initial fixture build did not publish a version');
await rm(join(directory, 'manifest.json'));
const recovered = await new ReferenceCompiler({ directory, profile: 'fixture', sources: fixtureSources(sources) }).compile();
expect(recovered.status).toBe('success'); expect(recovered.version).toBe(first.version);
const reader = new SqliteReferenceRepository(directory); expect(await reader.version()).toBe(first.version); reader.close();
} finally { await rm(root, { recursive: true, force: true }); }
});
});