GitNexus/gitnexus/test/integration/cfg/parse-cache-mixed.test.ts
Gergő Magyar 72edf40087
perf(store): V8 sidecars plus hardlinked ParsedFile restore (#3099)
* perf(store): add best-effort V8 sidecars beside canonical JSON caches

Warm ParsedFile and parse-cache loads skip JSON.parse when a sidecar is present. JSON remains authoritative: envelope validation plus v8.deserialize decide the hit, and any failure falls back without reparsing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): require generation bind or sidecar drop before cache overwrite

A same-length JSON rewrite could accept a leftover V8 sidecar if both
generation rotation and unlink failed. Refuse the new generation unless
at least one of those invalidations succeeds; skip publishing a sidecar
when only the drop succeeded.

detect_changes --scope all: 7 files, risk low, no affected processes.
tsc --noEmit clean; 115/115 relevant unit tests; cache-related
integration tests pass. parse-impl-env-reads worker-ready timeout is
pre-existing (same 5 failures with this change set stashed). ESLint 0
errors; remaining warnings are pre-existing and not on changed lines.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* refactor(store): share V8 overwrite invalidation across persist paths

The bind-or-drop gate lived in five writers. One helper keeps the
protocol in a single place and lets bind/drop run together on the
async path.

detect_changes --scope all: 3 files, risk low, no affected processes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(store): hardlink durable ParsedFile shards into the run store

Warm restore of parsedfile-cache into parsedfile-store now publishes all four shard files via fs.link, falling back to copy-into-tmp + rename so a leftover dest hardlink can never be written through. JSON remains the canonical cache; V8 sidecars ride the same path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(store): load immutable V8 shards in place, drop JSON fallback

Warm analyze was still paying JSON.parse plus a restore copy. One .v8 envelope per shard and SCHEMA_BUMP 81 make a miss re-extract instead of serving a stale JSON twin.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(store): validate durable V8 warm-cache restores

Reject incomplete or corrupt durable generations and snapshot valid shards before skipping parse workers, preserving ParsedFiles when persistence fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(store): drop unused durable load path

Load ParsedFiles only from the run-store snapshot and share one checksummed payload reader so inspect and deserialize stay consistent.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-30 20:50:20 +00:00

116 lines
4.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
getDurableParsedFileDir,
persistDurableParsedFileShardSync,
durableChunkHasShards,
loadParsedFilesForPaths,
} from '../../../src/storage/parsedfile-store.js';
import type { ParsedFile } from 'gitnexus-shared';
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
// #2082 M2 U5 — the warm/mixed cache seam for statement facts. On a warm (or
// mixed) run the unchanged chunk's ParsedFiles are loaded from the
// durable store instead of re-parsed (#2038); if that load (or intern)
// dropped or aliased the new `bindings`/`statements`
// fields, reaching-defs would silently degrade to `no-facts` for every cached
// file — exactly the field-loss class the #2038 mergeChunkResults lesson
// warns about. This pins the persist → load round-trip at the exact
// seam scope-resolution consumes.
const factCfg: FunctionCfg = {
filePath: 'src/a.ts',
functionStartLine: 1,
functionEndLine: 5,
functionStartColumn: 0,
entryIndex: 0,
exitIndex: 1,
blocks: [
{ index: 0, startLine: 1, endLine: 1, text: '', kind: 'entry', statements: [] },
{
index: 1,
startLine: 5,
endLine: 5,
text: '',
kind: 'exit',
statements: [
{ line: 2, defs: [0], uses: [] },
{ line: 3, defs: [1], uses: [0] },
],
},
],
edges: [{ from: 0, to: 1, kind: 'seq' }],
bindings: [
{ name: 'x', declLine: 2, declColumn: 6, kind: 'let' },
{ name: 'y', declLine: 3, declColumn: 6, kind: 'const' },
],
};
const mkParsedFile = (filePath: string): ParsedFile =>
({
filePath,
moduleScope: '',
scopes: [],
parsedImports: [],
localDefs: [],
referenceSites: [],
cfgSideChannel: [factCfg],
}) as unknown as ParsedFile;
describe('durable ParsedFile store carries M2 statement facts (#2082 U5)', () => {
let tempDir = '';
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'm2-facts-store-'));
});
afterEach(() => {
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
});
it('persist → loadParsedFilesForPaths preserves bindings + statements deep-equal', async () => {
const durableDir = getDurableParsedFileDir(tempDir);
const chunkHash = 'c'.repeat(64);
const files = ['src/a.ts', 'src/b.ts'];
// What a worker writes at flush on a cache MISS (the cold half of a
// mixed-mode run)…
persistDurableParsedFileShardSync(durableDir, chunkHash, 7, 0, files.map(mkParsedFile));
const wanted = new Set(files);
expect(await durableChunkHasShards(tempDir, chunkHash, wanted)).toBe(true);
const loaded = await loadParsedFilesForPaths(tempDir, wanted);
expect(loaded.size).toBe(2);
for (const filePath of files) {
const pf = loaded.get(filePath);
expect(pf).toBeDefined();
const channel = (pf as { cfgSideChannel?: unknown }).cfgSideChannel;
expect(Array.isArray(channel)).toBe(true);
const cfg = (channel as FunctionCfg[])[0];
// deep-equal: the interning reviver may dedup strings/objects but the
// VALUES must be intact — and no aliasing may merge the two files'
// distinct fact arrays into wrong shapes.
expect(cfg.bindings).toEqual(factCfg.bindings);
expect(cfg.blocks.map((b) => b.statements)).toEqual(factCfg.blocks.map((b) => b.statements));
}
});
it('facts survive even when two files share identical binding tables (reviver dedup safety)', async () => {
// The store reviver interns strings and dedups objects keyed on `nodeId`
// presence — BindingEntry/StatementFacts deliberately carry no such field,
// so dedup must never alias-then-mutate across files. Two files with
// byte-identical channels is the worst case.
const durableDir = getDurableParsedFileDir(tempDir);
const chunkHash = 'd'.repeat(64);
persistDurableParsedFileShardSync(durableDir, chunkHash, 7, 0, [
mkParsedFile('src/same1.ts'),
mkParsedFile('src/same2.ts'),
]);
const wanted = new Set(['src/same1.ts', 'src/same2.ts']);
expect(await durableChunkHasShards(tempDir, chunkHash, wanted)).toBe(true);
const loaded = await loadParsedFilesForPaths(tempDir, wanted);
const c1 = (loaded.get('src/same1.ts') as { cfgSideChannel?: FunctionCfg[] }).cfgSideChannel;
const c2 = (loaded.get('src/same2.ts') as { cfgSideChannel?: FunctionCfg[] }).cfgSideChannel;
expect(c1?.[0].bindings).toEqual(factCfg.bindings);
expect(c2?.[0].bindings).toEqual(factCfg.bindings);
});
});