GitNexus/gitnexus/test/unit/incremental-parse-cache.test.ts

1007 lines
40 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { mkdtemp, rm, readdir, writeFile, readFile } from 'fs/promises';
import { tmpdir } from 'os';
import path from 'path';
import {
emptyParseCache,
PARSE_CACHE_BUCKET_COUNT,
computeChunkHash,
fileContentHash,
packParseCacheChunks,
parseCacheBucketId,
loadParseCache,
loadParseCacheChunk,
persistParseCacheChunk,
saveParseCache,
pruneCache,
slimParseWorkerResultsForCache,
getColdParseRebuildDir,
createColdParseRebuildDir,
type ParseCache,
} from '../../src/storage/parse-cache.js';
import { TEST_PARSE_IDENTITY } from '../helpers/cache-identity.js';
import { writeV8CacheFile } from '../../src/storage/v8-sidecar.js';
import {
getDurableParsedFileDir,
durableChunkHasShards,
loadParsedFilesForPaths,
loadDurableParsedFileIndex,
persistDurableParsedFileShardSync,
pruneAndSaveDurableParsedFileStore,
} from '../../src/storage/parsedfile-store.js';
import type { ParsedFile } from 'gitnexus-shared';
import type { ParseWorkerResult } from '../../src/core/ingestion/workers/parse-worker.js';
const minimalResult = (overrides: Partial<ParseWorkerResult> = {}): ParseWorkerResult => ({
nodes: [],
relationships: [],
symbols: [],
calls: [],
assignments: [],
routes: [],
fetchCalls: [],
fetchWrapperDefs: [],
decoratorRoutes: [],
routerIncludes: [],
routerImports: [],
toolDefs: [],
ormQueries: [],
constructorBindings: [],
fileScopeBindings: [],
parsedFiles: [],
skippedLanguages: {},
fileCount: 0,
...overrides,
});
describe('computeChunkHash', () => {
it('produces a stable hex hash for a fixed set of (filePath, contentHash) entries', () => {
const entries = [
{ filePath: 'a.ts', contentHash: 'h-a' },
{ filePath: 'b.ts', contentHash: 'h-b' },
{ filePath: 'c.ts', contentHash: 'h-c' },
];
const h1 = computeChunkHash(entries, TEST_PARSE_IDENTITY);
const h2 = computeChunkHash(entries, TEST_PARSE_IDENTITY);
expect(h1).toBe(h2);
expect(h1).toMatch(/^[a-f0-9]{64}$/);
});
it('is order-independent (same files in different order → same hash)', () => {
const order1 = [
{ filePath: 'a.ts', contentHash: 'h-a' },
{ filePath: 'b.ts', contentHash: 'h-b' },
];
const order2 = [
{ filePath: 'b.ts', contentHash: 'h-b' },
{ filePath: 'a.ts', contentHash: 'h-a' },
];
expect(computeChunkHash(order1, TEST_PARSE_IDENTITY)).toBe(
computeChunkHash(order2, TEST_PARSE_IDENTITY),
);
});
it('changes when any file content changes', () => {
const before = [
{ filePath: 'a.ts', contentHash: 'h-a' },
{ filePath: 'b.ts', contentHash: 'h-b' },
];
const after = [
{ filePath: 'a.ts', contentHash: 'h-a' },
{ filePath: 'b.ts', contentHash: 'h-b-NEW' }, // b.ts content changed
];
expect(computeChunkHash(before, TEST_PARSE_IDENTITY)).not.toBe(
computeChunkHash(after, TEST_PARSE_IDENTITY),
);
});
it('changes when chunk membership changes (file added or removed)', () => {
const small = [
{ filePath: 'a.ts', contentHash: 'h-a' },
{ filePath: 'b.ts', contentHash: 'h-b' },
];
const bigger = [...small, { filePath: 'c.ts', contentHash: 'h-c' }];
expect(computeChunkHash(small, TEST_PARSE_IDENTITY)).not.toBe(
computeChunkHash(bigger, TEST_PARSE_IDENTITY),
);
});
});
describe('fileContentHash', () => {
it('hashes a string deterministically', () => {
expect(fileContentHash('hello')).toBe(fileContentHash('hello'));
expect(fileContentHash('hello')).not.toBe(fileContentHash('hello!'));
expect(fileContentHash('hello')).toMatch(/^[a-f0-9]{64}$/);
});
it('handles Buffer input identical to its string form', () => {
const s = 'sentinel';
expect(fileContentHash(Buffer.from(s))).toBe(fileContentHash(s));
});
});
describe('packParseCacheChunks (#3088)', () => {
const files = [
{ path: 'src/a.ts', size: 100, language: 'typescript' },
{ path: 'src/b.ts', size: 100, language: 'typescript' },
{ path: 'pkg/c.py', size: 100, language: 'python' },
];
const budget = 2 * 1024 * 1024;
const packKey = (chunk: string[]): string =>
`${files.find((f) => f.path === chunk[0])?.language ?? 'typescript'}\0${parseCacheBucketId(chunk[0])}`;
it('is independent of scan order', () => {
expect(packParseCacheChunks(files, budget)).toEqual(
packParseCacheChunks([...files].reverse(), budget),
);
});
it('add/delete only rewrites packs in the affected (language, bucket)', () => {
const a = packParseCacheChunks(files, budget);
const added = { path: 'AAA.ts', size: 150_000, language: 'typescript' };
const withNew = packParseCacheChunks([...files, added], budget);
const addedKey = packKey([added.path]);
const untouched = (packs: string[][]) =>
packs.filter((c) => packKey(c) !== addedKey).map((c) => c.join('|'));
expect(untouched(withNew).sort()).toEqual(untouched(a).sort());
expect(withNew.some((c) => c.includes(added.path))).toBe(true);
const withoutB = packParseCacheChunks(
files.filter((f) => f.path !== 'src/b.ts'),
budget,
);
const removedKey = packKey(['src/b.ts']);
const leftover = (packs: string[][]) =>
packs.filter((c) => packKey(c) !== removedKey).map((c) => c.join('|'));
expect(leftover(withoutB).sort()).toEqual(leftover(a).sort());
expect(withoutB.every((c) => !c.includes('src/b.ts'))).toBe(true);
});
it('parseCacheBucketId uses the full sha256 digest, not an IEEE-754 prefix', () => {
const path = 'src/foo.ts';
const hex = fileContentHash(path);
const full = Number(BigInt(`0x${hex}`) % BigInt(PARSE_CACHE_BUCKET_COUNT));
const truncated = Number.parseInt(hex.slice(0, 8), 16) % PARSE_CACHE_BUCKET_COUNT;
expect(parseCacheBucketId(path)).toBe(full);
expect(parseCacheBucketId(path)).toBeGreaterThanOrEqual(0);
expect(parseCacheBucketId(path)).toBeLessThan(PARSE_CACHE_BUCKET_COUNT);
expect(full).not.toBe(truncated);
});
});
describe('pruneCache', () => {
it('drops entries whose hashes are not in the used-set', () => {
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map<string, ParseWorkerResult[]>([
['hash-A', [minimalResult()]],
['hash-B', [minimalResult()]],
['hash-C', [minimalResult()]],
]),
usedKeys: new Set<string>(['hash-A']),
};
const removed = pruneCache(cache, cache.usedKeys);
expect(removed).toBe(2);
expect([...cache.entries.keys()].sort()).toEqual(['hash-A']);
});
it('returns 0 when every entry is in use', () => {
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map<string, ParseWorkerResult[]>([
['hash-A', [minimalResult()]],
['hash-B', [minimalResult()]],
]),
usedKeys: new Set<string>(['hash-A', 'hash-B']),
};
expect(pruneCache(cache, cache.usedKeys)).toBe(0);
expect(cache.entries.size).toBe(2);
});
it('drops onDiskKeys entries not in the used-set and counts them', () => {
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map<string, ParseWorkerResult[]>(),
usedKeys: new Set<string>(['disk-A']),
onDiskKeys: new Set<string>(['disk-A', 'disk-B', 'disk-C']),
};
const removed = pruneCache(cache, new Set(['disk-A']));
expect(removed).toBe(2);
expect([...(cache.onDiskKeys ?? [])].sort()).toEqual(['disk-A']);
});
});
describe('cache identity boundaries', () => {
const entries = [{ filePath: 'src/a.ts', contentHash: fileContentHash('export const a = 1;') }];
const key = computeChunkHash(entries, TEST_PARSE_IDENTITY);
const parsedFile: ParsedFile = {
filePath: 'src/a.ts',
moduleScope: '',
scopes: [],
parsedImports: [],
localDefs: [],
referenceSites: [],
};
it('reuses both stores only under the matching base identity', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-paired-identity-'));
try {
const cache = emptyParseCache(TEST_PARSE_IDENTITY);
cache.entries.set(key, [minimalResult({ fileCount: 1 })]);
cache.usedKeys.add(key);
const saved = await saveParseCache(dir, cache);
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, key, 1, 0, [parsedFile]);
await pruneAndSaveDurableParsedFileStore(durable, cache.identity.base, new Set(saved));
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect((await loadParseCacheChunk(loaded, key))?.[0]?.fileCount).toBe(1);
const coverage = await loadDurableParsedFileIndex(durable, loaded.identity.base);
expect(coverage.get(key)).toEqual(new Set(['src/a.ts']));
expect(await durableChunkHasShards(dir, key, coverage.get(key)!)).toBe(true);
expect((await loadParsedFilesForPaths(dir, new Set(['src/a.ts']))).get('src/a.ts')).toEqual(
parsedFile,
);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it.each([
['missing', undefined],
['numeric legacy', 143],
['release-based legacy', '143+1.6.12'],
['foreign', 'c'.repeat(64)],
['malformed', { base: TEST_PARSE_IDENTITY.base }],
])(
'misses both stores with %s identity metadata even when shards are valid',
async (_label, version) => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-foreign-identity-'));
try {
const cache = emptyParseCache(TEST_PARSE_IDENTITY);
cache.entries.set(key, [minimalResult({ fileCount: 1 })]);
cache.usedKeys.add(key);
await saveParseCache(dir, cache);
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, key, 1, 0, [parsedFile]);
await pruneAndSaveDurableParsedFileStore(durable, TEST_PARSE_IDENTITY.base, new Set([key]));
// Establish the readable generation before changing only its metadata.
expect(await loadDurableParsedFileIndex(durable, TEST_PARSE_IDENTITY.base)).toEqual(
new Map([[key, new Set(['src/a.ts'])]]),
);
await writeFile(
path.join(dir, 'parse-cache', 'index.json'),
JSON.stringify({ version, keys: [key] }),
);
await writeFile(
path.join(durable, 'index.json'),
JSON.stringify({ version, entries: { [key]: ['src/a.ts'] } }),
);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.size).toBe(0);
expect(await loadParseCacheChunk(loaded, key)).toBeUndefined();
expect(await loadDurableParsedFileIndex(durable, TEST_PARSE_IDENTITY.base)).toEqual(
new Map(),
);
// A sibling branch listing an old hash cannot admit a foreign shard.
loaded.usedKeys.add(key);
expect(await saveParseCache(dir, loaded)).toEqual([]);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
);
it('puts identical source under different physical keys for different base identities', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-identity-address-'));
try {
const otherIdentity = { ...TEST_PARSE_IDENTITY, base: 'c'.repeat(64) };
const otherKey = computeChunkHash(entries, otherIdentity);
expect(otherKey).not.toBe(key);
const first = emptyParseCache(TEST_PARSE_IDENTITY, { writeStoragePath: dir });
const second = emptyParseCache(otherIdentity, { writeStoragePath: dir });
await persistParseCacheChunk(first, key, [minimalResult({ fileCount: 1 })]);
await persistParseCacheChunk(second, otherKey, [minimalResult({ fileCount: 2 })]);
expect((await readdir(path.join(dir, 'parse-cache'))).sort()).toEqual(
[`${key}.v8`, `${otherKey}.v8`].sort(),
);
expect((await loadParseCacheChunk(first, key))?.[0]?.fileCount).toBe(1);
expect((await loadParseCacheChunk(second, otherKey))?.[0]?.fileCount).toBe(2);
expect(await loadParseCacheChunk(first, otherKey)).toBeUndefined();
expect(await loadParseCacheChunk(second, key)).toBeUndefined();
const durable = getDurableParsedFileDir(dir);
persistDurableParsedFileShardSync(durable, key, 1, 0, [parsedFile]);
persistDurableParsedFileShardSync(durable, otherKey, 1, 0, [parsedFile]);
expect((await readdir(durable)).sort()).toEqual([key, otherKey].sort());
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});
describe('loadParseCache / saveParseCache (round-trip)', () => {
it('round-trips an empty cache', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set(),
};
await saveParseCache(dir, cache);
await expect(fs.access(path.join(dir, 'parse-cache', 'index.json'))).resolves.toBeUndefined();
await expect(fs.access(path.join(dir, 'parse-cache.json'))).rejects.toThrow();
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.identity).toEqual(TEST_PARSE_IDENTITY);
expect(loaded.entries.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns an empty cache when the file is missing', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(0);
expect(loaded.usedKeys.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns an empty cache on version mismatch (next-run regen)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
// Write a cache file with a different version directly
const fs = await import('fs/promises');
await fs.writeFile(
path.join(dir, 'parse-cache.json'),
JSON.stringify({ version: 'foreign-99', entries: { h: [] } }),
'utf-8',
);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(0); // mismatch → empty
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns an empty cache on corrupt JSON', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
await fs.writeFile(path.join(dir, 'parse-cache.json'), '{not-json', 'utf-8');
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('loads a single-file representation bearing the current identity', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
await fs.writeFile(
path.join(dir, 'parse-cache.json'),
JSON.stringify({
version: TEST_PARSE_IDENTITY.base,
entries: {
legacyChunk: [minimalResult({ fileCount: 7 })],
},
}),
'utf-8',
);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(1);
expect(loaded.entries.get('legacyChunk')?.[0]?.fileCount).toBe(7);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('skips corrupt or missing shards while loading the sharded cache index', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const cacheDir = path.join(dir, 'parse-cache');
const goodKey = 'a'.repeat(64);
const missingKey = 'b'.repeat(64);
const badKey = 'c'.repeat(64);
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeFile(
path.join(cacheDir, 'index.json'),
JSON.stringify({
version: TEST_PARSE_IDENTITY.base,
keys: [goodKey, missingKey, badKey],
}),
'utf-8',
);
await writeV8CacheFile(path.join(cacheDir, `${goodKey}.v8`), [
minimalResult({ fileCount: 3 }),
]);
await fs.writeFile(path.join(cacheDir, `${badKey}.v8`), '{not-json', 'utf-8');
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(0);
expect(loaded.onDiskKeys?.size).toBe(3);
const chunk = await loadParseCacheChunk(loaded, goodKey);
expect(chunk?.[0]?.fileCount).toBe(3);
// A shard listed in the index but absent on disk, and a corrupt-JSON
// shard, both resolve to undefined (graceful cache miss) — not a throw.
expect(await loadParseCacheChunk(loaded, missingKey)).toBeUndefined();
expect(await loadParseCacheChunk(loaded, badKey)).toBeUndefined();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('round-trips Map and Set values through the JSON replacer/reviver', async () => {
// ParsedFile.scopes[*].typeBindings is a ReadonlyMap<string, TypeRef>.
// Without the replacer/reviver pair, JSON.stringify collapses Maps to
// {} and downstream code that does .get() / iterates entries crashes
// with "is not iterable". This test pins the round-trip behaviour.
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const innerMap = new Map<string, string>([
['k1', 'v1'],
['k2', 'v2'],
]);
const innerSet = new Set<string>(['s1', 's2']);
// Stash the live Map/Set inside a synthetic ParseWorkerResult — we
// only need the serializer to traverse them. Casting to bypass the
// strict shape isn't a problem here: this test is about JSON
// round-tripping of arbitrary nested Map/Set values, not full
// ParseWorkerResult contents.
const fake = minimalResult({
parsedFiles: [
{
filePath: 't.ts',
// Cast through unknown to satisfy the readonly Scope shape
// while still smuggling a live Map into the serializer's
// traversal path — see comment block above.
scopes: [{ id: 's1', typeBindings: innerMap, extras: innerSet }],
} as unknown as ParseWorkerResult['parsedFiles'][number],
],
});
const chunkKey = 'd'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map<string, ParseWorkerResult[]>([[chunkKey, [fake]]]),
usedKeys: new Set([chunkKey]),
};
await saveParseCache(dir, cache);
const persisted = await fs.readdir(path.join(dir, 'parse-cache'));
expect(persisted).toContain('index.json');
expect(persisted).toContain(`${chunkKey}.v8`);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
const reloaded = (await loadParseCacheChunk(loaded, chunkKey))?.[0];
expect(reloaded).toBeDefined();
const scope = (reloaded as ParseWorkerResult).parsedFiles[0]?.scopes[0] as unknown as {
typeBindings?: unknown;
extras?: unknown;
};
expect(scope.typeBindings).toBeInstanceOf(Map);
expect((scope.typeBindings as Map<string, string>).get('k1')).toBe('v1');
expect((scope.typeBindings as Map<string, string>).size).toBe(2);
expect(scope.extras).toBeInstanceOf(Set);
expect((scope.extras as Set<string>).has('s2')).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('ignores traversal-like and non-hex keys in sharded index.json', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const cacheDir = path.join(dir, 'parse-cache');
await fs.mkdir(cacheDir, { recursive: true });
const safeKey = 'e'.repeat(64);
await fs.writeFile(
path.join(cacheDir, 'index.json'),
JSON.stringify({
version: TEST_PARSE_IDENTITY.base,
keys: ['../evil', '/absolute', 'G'.repeat(64), safeKey],
}),
'utf-8',
);
await writeV8CacheFile(path.join(cacheDir, `${safeKey}.v8`), [
minimalResult({ fileCount: 9 }),
]);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.size).toBe(1);
const chunk = await loadParseCacheChunk(loaded, safeKey);
expect(chunk?.[0]?.fileCount).toBe(9);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('writes one shard file per cache entry (three distinct keys)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const k1 = '1'.repeat(64);
const k2 = '2'.repeat(64);
const k3 = '3'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map<string, ParseWorkerResult[]>([
[k1, [minimalResult({ fileCount: 1 })]],
[k2, [minimalResult({ fileCount: 2 })]],
[k3, [minimalResult({ fileCount: 3 })]],
]),
usedKeys: new Set([k1, k2, k3]),
};
await saveParseCache(dir, cache);
const cacheDir = path.join(dir, 'parse-cache');
const names = await fs.readdir(cacheDir);
expect(names).toContain('index.json');
expect(names.filter((n) => n.endsWith('.v8')).length).toBe(3);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.size).toBe(3);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('returns empty when sharded index version mismatches even if legacy parse-cache.json is valid', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const cacheDir = path.join(dir, 'parse-cache');
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeFile(
path.join(cacheDir, 'index.json'),
JSON.stringify({ version: 'foreign-sharded-1', keys: [] }),
'utf-8',
);
await fs.writeFile(
path.join(dir, 'parse-cache.json'),
JSON.stringify({
version: TEST_PARSE_IDENTITY.base,
entries: { legacyChunk: [minimalResult({ fileCount: 42 })] },
}),
'utf-8',
);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.entries.size).toBe(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('second saveParseCache replaces the first sharded cache', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
const k1 = '4'.repeat(64);
const k2 = '5'.repeat(64);
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[k1, [minimalResult()]]]),
usedKeys: new Set([k1]),
});
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[k2, [minimalResult({ fileCount: 99 })]]]),
usedKeys: new Set([k2]),
});
const names = await fs.readdir(path.join(dir, 'parse-cache'));
expect(names).not.toContain(`${k1}.v8`);
expect(names).toContain(`${k2}.v8`);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.size).toBe(1);
const chunk = await loadParseCacheChunk(loaded, k2);
expect(chunk?.[0]?.fileCount).toBe(99);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('removes legacy parse-cache.json after a successful sharded save', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const fs = await import('fs/promises');
await fs.writeFile(
path.join(dir, 'parse-cache.json'),
JSON.stringify({
version: TEST_PARSE_IDENTITY.base,
entries: { oldLegacy: [minimalResult({ fileCount: 5 })] },
}),
'utf-8',
);
const k = '6'.repeat(64);
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[k, [minimalResult({ fileCount: 6 })]]]),
usedKeys: new Set([k]),
});
await expect(fs.access(path.join(dir, 'parse-cache.json'))).rejects.toThrow();
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
const chunk = await loadParseCacheChunk(loaded, k);
expect(chunk?.[0]?.fileCount).toBe(6);
expect(loaded.onDiskKeys?.has(k)).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('slimParseWorkerResultsForCache drops legacy DAG fields', () => {
const raw = minimalResult({
calls: [{ filePath: 'a.c', calleeName: 'f', line: 1 } as never],
assignments: [
{ filePath: 'a.c', sourceId: 's', receiverText: 'x', propertyName: 'y', line: 1 },
],
constructorBindings: [{ filePath: 'a.c', bindings: [] }],
parsedFiles: [
{
filePath: 'a.c',
moduleScope: 'm',
scopes: [],
parsedImports: [],
localDefs: [],
referenceSites: [],
},
],
scopeExtractionFailures: ['a.c'],
});
const slim = slimParseWorkerResultsForCache([raw])[0];
expect(slim.calls).toEqual([]);
expect(slim.assignments).toEqual([]);
expect(slim.constructorBindings).toEqual([]);
expect(slim.parsedFiles).toEqual([]);
expect(slim.scopeExtractionFailures).toEqual(['a.c']);
expect(slim.fileCount).toBe(raw.fileCount);
});
it('slimParseWorkerResultsForCache preserves nodes (incremental exportedTypeMap depends on them)', () => {
const raw = minimalResult({
nodes: [
{
id: 'Function:a.ts:foo',
label: 'Function',
properties: { name: 'foo', filePath: 'a.ts', isExported: true },
},
] as ParseWorkerResult['nodes'],
});
const slim = slimParseWorkerResultsForCache([raw])[0];
// `nodes` (and `symbols`) must survive slimming — on a warm cache hit they
// are what mergeChunkResults replays to rebuild the ExportedTypeMap.
expect(slim.nodes).toEqual(raw.nodes);
expect(slim.nodes).toHaveLength(1);
});
it('round-trips scope extraction failures through a persisted cache shard', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const key = 'e'.repeat(64);
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[key, [minimalResult({ scopeExtractionFailures: ['src/broken.ts'] })]]]),
usedKeys: new Set([key]),
});
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
const replayed = await loadParseCacheChunk(loaded, key);
expect(replayed?.[0]?.scopeExtractionFailures).toEqual(['src/broken.ts']);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('persistParseCacheChunk writes to disk without retaining in-memory entries', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const key = '7'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set(),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 11 })]);
expect(cache.entries.has(key)).toBe(false);
expect(cache.stagedKeys?.has(key)).toBe(true);
const chunk = await loadParseCacheChunk(cache, key);
expect(chunk?.[0]?.fileCount).toBe(11);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('saveParseCache excludes a usedKeys hash whose shard was never persisted (no phantom index key)', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const realKey = 'a'.repeat(64);
const phantomKey = 'b'.repeat(64); // in usedKeys but has no entry and no on-disk shard
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[realKey, [minimalResult({ fileCount: 3 })]]]),
usedKeys: new Set([realKey, phantomKey]),
};
await saveParseCache(dir, cache);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.has(realKey)).toBe(true);
// The phantom key was never written, so it must not appear in the index.
expect(loaded.onDiskKeys?.has(phantomKey)).toBe(false);
expect((await loadParseCacheChunk(loaded, realKey))?.[0]?.fileCount).toBe(3);
expect(await loadParseCacheChunk(loaded, phantomKey)).toBeUndefined();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('saveParseCache copies a persisted-but-evicted shard (copyFile branch) and round-trips', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const key = 'c'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([key]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
// persist writes the shard to the live dir and evicts it from `entries`,
// so saveParseCache must hit the copyFile branch to carry it forward.
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 42 })]);
expect(cache.entries.has(key)).toBe(false);
await saveParseCache(dir, cache);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect(loaded.onDiskKeys?.has(key)).toBe(true);
expect((await loadParseCacheChunk(loaded, key))?.[0]?.fileCount).toBe(42);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('recreates a memoized shard directory after a long-lived process replaces it', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-'));
try {
const firstKey = 'd'.repeat(64);
const secondKey = 'e'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([firstKey]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, firstKey, [minimalResult({ fileCount: 1 })]);
await rm(path.join(dir, 'parse-cache'), { recursive: true, force: true });
cache.usedKeys = new Set([secondKey]);
await persistParseCacheChunk(cache, secondKey, [minimalResult({ fileCount: 2 })]);
await saveParseCache(dir, cache);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect((await loadParseCacheChunk(loaded, secondKey))?.[0]?.fileCount).toBe(2);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('writes a V8 shard and loads it with Map-preserving semantics', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-v8-'));
try {
const innerMap = new Map<string, string>([
['k1', 'v1'],
['k2', 'v2'],
]);
const innerSet = new Set<string>(['s1', 's2']);
// Exercise opaque structured-clone data without relying on a removed
// worker field (ParsedFiles intentionally live in a separate store).
const fake = {
...minimalResult({ fileCount: 9 }),
serializationProbe: { typeBindings: innerMap, extras: innerSet },
};
const key = 'f'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([key]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, key, [fake]);
const names = await readdir(path.join(dir, 'parse-cache'));
expect(names).toEqual(expect.arrayContaining([`${key}.v8`]));
expect(names.some((n) => n.endsWith('.json') && n !== 'index.json')).toBe(false);
const loaded = await loadParseCacheChunk(cache, key);
expect(loaded?.[0]?.fileCount).toBe(9);
const restored = loaded?.[0];
if (!restored || !('serializationProbe' in restored)) {
throw new Error('Missing serialized probe');
}
const smuggled = restored.serializationProbe;
if (
typeof smuggled !== 'object' ||
smuggled === null ||
!('typeBindings' in smuggled) ||
!('extras' in smuggled)
) {
throw new Error('Invalid serialized probe');
}
expect(smuggled.typeBindings).toBeInstanceOf(Map);
expect(smuggled.extras).toBeInstanceOf(Set);
if (!(smuggled.typeBindings instanceof Map) || !(smuggled.extras instanceof Set)) {
throw new Error('Structured-clone collections were not preserved');
}
expect([...smuggled.typeBindings]).toEqual([
['k1', 'v1'],
['k2', 'v2'],
]);
expect([...smuggled.extras].sort()).toEqual(['s1', 's2']);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('treats a corrupt parse-cache V8 shard as a miss', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-v8-fb-'));
try {
const key = 'a'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([key]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 4 })]);
await writeFile(path.join(dir, 'parse-cache', `${key}.v8`), Buffer.from([1, 2, 3]));
const loaded = await loadParseCacheChunk(cache, key);
expect(loaded).toBeUndefined();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('saveParseCache copies an existing V8 shard', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-v8-copy-'));
try {
const key = 'c'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([key]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 42 })]);
const liveV8 = await readFile(path.join(dir, 'parse-cache', `${key}.v8`));
await saveParseCache(dir, cache);
expect(await readdir(path.join(dir, 'parse-cache'))).toEqual(
expect.arrayContaining([`${key}.v8`, 'index.json']),
);
expect(await readFile(path.join(dir, 'parse-cache', `${key}.v8`))).toEqual(liveV8);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect((await loadParseCacheChunk(loaded, key))?.[0]?.fileCount).toBe(42);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('misses when the V8 shard is absent', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-v8-legacy-'));
try {
const key = 'b'.repeat(64);
const cache: ParseCache = {
identity: TEST_PARSE_IDENTITY,
entries: new Map(),
usedKeys: new Set([key]),
writeStoragePath: dir,
onDiskKeys: new Set(),
};
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 7 })]);
await rm(path.join(dir, 'parse-cache', `${key}.v8`), { force: true });
const loaded = await loadParseCacheChunk(cache, key);
expect(loaded).toBeUndefined();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('persists cold-rebuild shards under staging without touching the live parse-cache dir', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-stage-'));
try {
const liveKey = 'a'.repeat(64);
const stagedKey = 'b'.repeat(64);
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[liveKey, [minimalResult({ fileCount: 1 })]]]),
usedKeys: new Set([liveKey]),
});
const staging = getColdParseRebuildDir(dir);
const cache = await loadParseCache(dir, TEST_PARSE_IDENTITY, staging);
cache.usedKeys = new Set([liveKey, stagedKey]);
await persistParseCacheChunk(cache, stagedKey, [minimalResult({ fileCount: 99 })]);
const liveNames = await readdir(path.join(dir, 'parse-cache'));
expect(liveNames).toContain(`${liveKey}.v8`);
expect(liveNames).not.toContain(`${stagedKey}.v8`);
const stagedNames = await readdir(path.join(staging, 'parse-cache'));
expect(stagedNames).toContain(`${stagedKey}.v8`);
const saved = await saveParseCache(dir, cache);
expect(saved.sort()).toEqual([liveKey, stagedKey].sort());
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect((await loadParseCacheChunk(loaded, liveKey))?.[0]?.fileCount).toBe(1);
expect((await loadParseCacheChunk(loaded, stagedKey))?.[0]?.fileCount).toBe(99);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('prefers a staged shard over a same-hash live shard when publishing', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-pref-'));
try {
const key = 'c'.repeat(64);
await saveParseCache(dir, {
identity: TEST_PARSE_IDENTITY,
entries: new Map([[key, [minimalResult({ fileCount: 1 })]]]),
usedKeys: new Set([key]),
});
const staging = getColdParseRebuildDir(dir);
const cache = await loadParseCache(dir, TEST_PARSE_IDENTITY, staging);
cache.usedKeys.add(key);
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 7 })]);
expect((await loadParseCacheChunk(cache, key))?.[0]?.fileCount).toBe(7);
await saveParseCache(dir, cache);
const loaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, getColdParseRebuildDir(dir));
expect((await loadParseCacheChunk(loaded, key))?.[0]?.fileCount).toBe(7);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('does not fall back to a live shard when its staged repair disappears', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-missing-repair-'));
try {
const key = 'd'.repeat(64);
const original = emptyParseCache(TEST_PARSE_IDENTITY);
original.entries.set(key, [minimalResult({ fileCount: 1 })]);
original.usedKeys.add(key);
await saveParseCache(dir, original);
const staging = getColdParseRebuildDir(dir);
const cache = await loadParseCache(dir, TEST_PARSE_IDENTITY, staging);
expect((await loadParseCacheChunk(cache, key))?.[0]?.fileCount).toBe(1);
await persistParseCacheChunk(cache, key, [minimalResult({ fileCount: 7 })]);
cache.usedKeys.add(key);
await rm(path.join(staging, 'parse-cache', `${key}.v8`));
expect(await loadParseCacheChunk(cache, key)).toBeUndefined();
expect(await readFile(path.join(dir, 'parse-cache', `${key}.v8`))).not.toHaveLength(0);
expect(await saveParseCache(dir, cache)).toEqual([]);
const reloaded = await loadParseCache(dir, TEST_PARSE_IDENTITY, staging);
expect(await loadParseCacheChunk(reloaded, key)).toBeUndefined();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
it('createColdParseRebuildDir isolates run children beneath each locked slot staging root', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'gnx-pc-uniq-'));
try {
const a = await createColdParseRebuildDir(path.join(dir, 'slot-a'));
const nextA = await createColdParseRebuildDir(path.join(dir, 'slot-a'));
const b = await createColdParseRebuildDir(path.join(dir, 'slot-b'));
expect(a).not.toBe(nextA);
expect(a).not.toBe(b);
expect(path.dirname(a)).toBe(getColdParseRebuildDir(path.join(dir, 'slot-a')));
expect(path.dirname(nextA)).toBe(path.dirname(a));
expect(path.dirname(b)).toBe(getColdParseRebuildDir(path.join(dir, 'slot-b')));
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});