// Index tests (C2-05a): the cache key and its bound, failed reads, mapped-only contents, and the rebuild // time on a 3,000-file fixture. Fixtures are made up and built under the scratch area. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdirSync, symlinkSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { performance } from 'node:perf_hooks'; import { clearIndexCache, indexCacheSize, indexFor } from '../src/index-build.mjs'; import { git, mapping, MARKER } from './helpers.mjs'; import { note, scratchRepo } from './query-fixture.mjs'; const FILES = { 'Book One/Characters/Mara Vell.md': note('character', 'Mara Vell', MARKER), 'Book One/Chapters/Chapter 01.md': '# One\n[[Mara Vell]]\n', 'Inbox Note.md': `${MARKER}\n`, }; // A mapping whose harbor universe has the given display name. Each name gives a new cache key. const named = name => mapping(e => ({ ...e, universes: { ...e.universes, harbor: { ...e.universes.harbor, name } } })); test('the same commit, repo path and mapping return the same index, and only mapped files are in it', async t => { clearIndexCache(); const { repo, commit } = scratchRepo(t, FILES); const a = await indexFor({ repo, commit, mapping: mapping() }); const b = await indexFor({ repo, commit, mapping: mapping() }); assert.equal(a, b); assert.equal(indexCacheSize(), 1); assert.equal(a.commit, commit); assert.ok(a.files.has('Book One/Characters/Mara Vell.md')); assert.ok(!a.files.has('Inbox Note.md')); assert.deepEqual(a.names.get('mara vell'), ['Book One/Characters/Mara Vell.md']); assert.deepEqual(a.linksIn.get('Book One/Characters/Mara Vell.md').map(l => l.path), ['Book One/Chapters/Chapter 01.md']); }); test('a different repo path, mapping or commit gets its own index', async t => { clearIndexCache(); const { root, repo, commit } = scratchRepo(t, FILES); const base = await indexFor({ repo, commit, mapping: mapping() }); const alias = join(root, 'alias'); symlinkSync(repo, alias); assert.notEqual(await indexFor({ repo: alias, commit, mapping: mapping() }), base); assert.notEqual(await indexFor({ repo, commit, mapping: named('Renamed Harbor') }), base); mkdirSync(join(repo, 'Book One/Places'), { recursive: true }); writeFileSync(join(repo, 'Book One/Places/New.md'), note('place', 'New')); git(repo, 'add', '-A'); git(repo, 'commit', '-qm', 'second'); const next = git(repo, 'rev-parse', 'HEAD'); const later = await indexFor({ repo, commit: next, mapping: mapping() }); assert.notEqual(later, base); assert.equal(later.commit, next); assert.ok(later.files.has('Book One/Places/New.md')); }); test('the cache holds at most eight indexes and evicts the least recently used', async t => { clearIndexCache(); const { repo, commit } = scratchRepo(t, FILES); const maps = Array.from({ length: 9 }, (_, i) => named(`Harbor ${i}`)); const first = await indexFor({ repo, commit, mapping: maps[0] }); for (const m of maps.slice(1)) await indexFor({ repo, commit, mapping: m }); assert.equal(indexCacheSize(), 8); assert.notEqual(await indexFor({ repo, commit, mapping: maps[0] }), first); assert.equal(indexCacheSize(), 8); }); test('a failed read is not kept in the cache', async t => { clearIndexCache(); const { repo } = scratchRepo(t, FILES); await assert.rejects(indexFor({ repo, commit: '0'.repeat(40), mapping: mapping() })); assert.equal(indexCacheSize(), 0); }); test('a missing or unchecked mapping is refused before any read', async t => { clearIndexCache(); const { repo, commit } = scratchRepo(t, FILES); await assert.rejects(indexFor({ repo, commit, mapping: undefined }), { code: 'library-mapping-invalid' }); await assert.rejects(indexFor({ repo, commit, mapping: { kind: 'hybrid' } }), { code: 'library-mapping-invalid' }); }); test('a 3,000-file rebuild completes and its time is reported', { timeout: 300_000 }, async t => { clearIndexCache(); const folders = ['Characters', 'Places', 'Research']; const files = {}; for (let i = 0; i < 3000; i++) { files[`Book One/${folders[i % 3]}/Note ${i}.md`] = note('character', `Note ${i}`, `Body ${i} [[Note ${(i + 1) % 3000}]]`); } const { repo, commit } = scratchRepo(t, files); const started = performance.now(); const index = await indexFor({ repo, commit, mapping: mapping() }); const ms = performance.now() - started; t.diagnostic(`3,000-file index rebuild: ${ms.toFixed(0)} ms`); assert.equal(index.paths.length, 3000); });