mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
8abebda02c
oversize-file-not-read's drift test wrote a 1.4 MB `.js` made of one statement repeated 110k times. Windows Defender's script scan of that freshly written file takes about 54 s on its first read (the viewer's read in the test), so the test always timed out on Windows and its cleanup then hit EPERM on the still open file. A varied 2.4 MB `.js` and a real 2 MB bundle scan in 35 ms and 144 ms, so this is the fixture, not something users hit; the fixture now has a different statement on every line. The mpeg-ts git-path cases build a git repository and index it, the last one twice. At ~100 ms per git process on Windows that is 5–6 s, past vitest's 5 s default; the describe now allows 30 s. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
6.7 KiB
TypeScript
127 lines
6.7 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
import { oversizeStamp, hashContent } from '../src/extraction';
|
|
import { hasDriftedOnDisk, readFileShape } from '../src/ui-server/api/source';
|
|
import { ToolHandler } from '../src/mcp/tools';
|
|
import { validateAnswerFiles } from '../src/mcp/answer-freshness';
|
|
|
|
/**
|
|
* A file over the size limit is stored as skipped without ever being read
|
|
* (#1910): committed video/blob fixtures used to be decoded in full — and
|
|
* hashed — only to be discarded, costing their size in RSS per file.
|
|
*/
|
|
describe('oversize files are stat-gated, never read (#1910)', () => {
|
|
let dir: string;
|
|
afterEach(() => { if (dir) fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
const big = (bytes: number, fill = 0x41) => Buffer.alloc(bytes, fill);
|
|
|
|
it('indexes the neighbours, records the oversize file as skipped with a size-stamp hash, and does not decode it', async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-oversize-'));
|
|
fs.writeFileSync(path.join(dir, 'app.ts'), 'export function alpha() { return beta(); }\nexport function beta() { return 1; }\n');
|
|
// 1 MB + 1: over the limit; invalid UTF-8 (0xFF) so any decode would be visible as replacement chars.
|
|
fs.writeFileSync(path.join(dir, 'blob.ts'), big(1024 * 1024 + 1, 0xff));
|
|
const cg = await CodeGraph.init(dir, { index: true });
|
|
try {
|
|
expect(cg.getNodesByKind('function').map(n => n.name).sort()).toEqual(['alpha', 'beta']);
|
|
const rec = cg.getFiles().find(f => f.path === 'blob.ts');
|
|
expect(rec).toBeDefined();
|
|
expect(rec!.contentHash).toBe(hashContent(oversizeStamp(1024 * 1024 + 1)));
|
|
// Nothing from the blob reached the graph.
|
|
expect(cg.getNodesInFile('blob.ts').filter(n => n.kind !== 'file')).toEqual([]);
|
|
// Change detection agrees with what was stored: nothing pending.
|
|
expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] });
|
|
// A same-size rewrite is not a change (nothing about it is indexed)...
|
|
fs.writeFileSync(path.join(dir, 'blob.ts'), big(1024 * 1024 + 1, 0xfe));
|
|
expect(cg.getChangedFiles().modified).toEqual([]);
|
|
// ...crossing the limit is: the file becomes ordinary source.
|
|
fs.writeFileSync(path.join(dir, 'blob.ts'), 'export const gamma = 3;\n');
|
|
expect(cg.getChangedFiles().modified).toEqual(['blob.ts']);
|
|
await cg.sync();
|
|
expect(cg.getNodesInFile('blob.ts').some(n => n.name === 'gamma')).toBe(true);
|
|
// ...and growing back over it is a change too, stored as the stamp again.
|
|
fs.writeFileSync(path.join(dir, 'blob.ts'), big(2 * 1024 * 1024, 0xff));
|
|
expect(cg.getChangedFiles().modified).toEqual(['blob.ts']);
|
|
await cg.sync();
|
|
expect(cg.getFiles().find(f => f.path === 'blob.ts')!.contentHash).toBe(hashContent(oversizeStamp(2 * 1024 * 1024)));
|
|
expect(cg.getNodesInFile('blob.ts').some(n => n.name === 'gamma')).toBe(false);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a 400 MB sparse fixture indexes in well under a second and without growing the heap by its size', async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-oversize-big-'));
|
|
fs.writeFileSync(path.join(dir, 'ok.ts'), 'export const one = 1;\n');
|
|
// Sparse: occupies no disk, but stat() reports 400 MB — a read would decode all of it.
|
|
const fd = fs.openSync(path.join(dir, 'huge.ts'), 'w');
|
|
fs.ftruncateSync(fd, 400 * 1024 * 1024);
|
|
fs.closeSync(fd);
|
|
// Warm the engine on a sibling project first, so the grammar and worker
|
|
// start-up cost is not mistaken for the file being read.
|
|
const warm = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-oversize-warm-'));
|
|
fs.writeFileSync(path.join(warm, 'w.ts'), 'export const w = 1;\n');
|
|
(await CodeGraph.init(warm, { index: true })).close();
|
|
fs.rmSync(warm, { recursive: true, force: true });
|
|
const before = process.memoryUsage().rss;
|
|
const t0 = Date.now();
|
|
const cg = await CodeGraph.init(dir, { index: true });
|
|
try {
|
|
expect(Date.now() - t0).toBeLessThan(5000);
|
|
// Reading 400 MB would show as at least that much RSS; the stamp shows as none.
|
|
expect(process.memoryUsage().rss - before).toBeLessThan(100 * 1024 * 1024);
|
|
expect(cg.getFiles().map(f => f.path).sort()).toEqual(['huge.ts', 'ok.ts']);
|
|
expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] });
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('an unchanged file over the size limit is not reported as drifted (#1910, #1915 review)', () => {
|
|
let dir: string;
|
|
afterEach(() => { if (dir) fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
it('reads as current in the viewer and in MCP until its size changes', async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-oversize-drift-'));
|
|
fs.writeFileSync(path.join(dir, 'app.ts'), 'export function alpha() { return 1; }\n');
|
|
// 1.4 MB of ordinary text: over the index limit, under the viewer's 8 MB read cap.
|
|
// Each line differs: Windows Defender's script scan of a freshly written `.js`
|
|
// made of one statement repeated 110k times takes close to a minute on the
|
|
// first read, so the viewer's read below blew the test timeout there. Real
|
|
// code of the same size is scanned in milliseconds.
|
|
const line = 'const last = 0;\n';
|
|
const lines: string[] = [];
|
|
for (let size = 0, i = 0; size < 1.4 * 1024 * 1024; i++) {
|
|
lines.push(`const x${i} = ${i};\n`);
|
|
size += lines[i]!.length;
|
|
}
|
|
fs.writeFileSync(path.join(dir, 'big.js'), lines.join(''));
|
|
const cg = await CodeGraph.init(dir, { index: true });
|
|
try {
|
|
const record = cg.getFiles().find((f) => f.path === 'big.js')!;
|
|
expect(record).toBeDefined();
|
|
// Touch it: the stat fast path no longer answers, so the hash has to.
|
|
const later = new Date(Date.now() + 5000);
|
|
fs.utimesSync(path.join(dir, 'big.js'), later, later);
|
|
|
|
expect(readFileShape(dir, 'big.js', record).drift).toBe(false);
|
|
expect(hasDriftedOnDisk(dir, 'big.js', record)).toBe(false);
|
|
expect((new ToolHandler(cg) as any).isFileStaleOnDisk(cg, 'big.js')).toBe(false);
|
|
const answer = [{ path: 'big.js', contentHash: record.contentHash }];
|
|
expect((await validateAnswerFiles(dir, answer)).stale).toEqual([]);
|
|
|
|
// A different size is a different stamp, and that is drift.
|
|
fs.appendFileSync(path.join(dir, 'big.js'), line);
|
|
expect(readFileShape(dir, 'big.js', record).drift).toBe(true);
|
|
expect(hasDriftedOnDisk(dir, 'big.js', record)).toBe(true);
|
|
expect((await validateAnswerFiles(dir, answer)).stale).toEqual(['big.js']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|