mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
Three test files updated to match the v0.34 contract changes: - test/edge-extractor.test.ts: two assertions on `toSymbol` exact-match were brittle to the W1 receiver-type resolution. `this.go()` / `self.go()` now resolve to `Foo::go` instead of bare `go`. Tests accept either form for back-compat with brains still on pre-W1 extracted edges. - test/source-id-tx-regression.test.ts: the D16 "back-compat cross-source view preserved" test was asserting that ctx.sourceId undefined → cross-source view. v0.34 STEP 0 (D4) closes that path by design — it's the exact cross-source-bleed bug class STEP 0 fixed. Test renamed + assertion updated to reflect: makeCtx() with no override now falls back to 'default' (per the dispatch + cli auto-fill), and cross-source visibility is an explicit caller decision, not an implicit consequence of ctx omission. - test/chunker-timeout.test.ts: the GBRAIN_CHUNKER_TIMEOUT_MS=1 fallback case asserted edges=[] under the calls-only extractor. W2's extractAllEdges emits imports/references from top-level statements even on a partial parse, so the timeout-fallback path can return non-empty edges. Assertion relaxed to "edges is an array" — the contract that matters is "returns cleanly without hanging," not the edges-array shape. Full unit suite (parallel + serial): 6132 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
5.2 KiB
TypeScript
125 lines
5.2 KiB
TypeScript
/**
|
||
* v0.31.2 chunker timeout regression tests.
|
||
*
|
||
* Closes the bug class where a single pathological code file could
|
||
* wedge the entire sync inside tree-sitter WASM at 99% CPU with no
|
||
* I/O and no observable progress. The fix bounds parser.parse() with
|
||
* setTimeoutMicros and falls back to recursive chunks on timeout.
|
||
*
|
||
* Test design (per codex C9): the runtime parser is non-deterministic
|
||
* about how long it takes to parse arbitrary input, so "force timeout
|
||
* via huge input" is machine-speed dependent and flaky on slow CI. We
|
||
* stub the ParserLike seam directly to assert the timeout contract,
|
||
* then verify the env wiring with a 1ms cap that reliably loses.
|
||
*/
|
||
import { describe, test, expect } from 'bun:test';
|
||
import {
|
||
parseWithTimeout,
|
||
ChunkerTimeoutError,
|
||
chunkCodeTextFull,
|
||
} from '../src/core/chunkers/code.ts';
|
||
import { withEnv } from './helpers/with-env.ts';
|
||
|
||
const REAL_TS = `export function add(a: number, b: number): number { return a + b; }
|
||
export class Counter {
|
||
private n = 0;
|
||
increment(): void { this.n++; }
|
||
get value(): number { return this.n; }
|
||
}
|
||
`;
|
||
|
||
describe('parseWithTimeout — pure-function seam', () => {
|
||
test('1. throws ChunkerTimeoutError when parser.parse returns null', () => {
|
||
const stub = {
|
||
_timeoutCalls: 0,
|
||
_parseCalls: 0,
|
||
setTimeoutMicros(_t: number) { this._timeoutCalls++; },
|
||
parse(_s: string) { this._parseCalls++; return null; },
|
||
};
|
||
|
||
expect(() => parseWithTimeout(stub, 'x', 50, 'foo.ts')).toThrow(ChunkerTimeoutError);
|
||
expect(stub._timeoutCalls).toBe(1);
|
||
expect(stub._parseCalls).toBe(1);
|
||
});
|
||
|
||
test('2. throws clear error if setTimeoutMicros API is missing', () => {
|
||
// A future web-tree-sitter that drops the API must NOT silently
|
||
// regress to no-timeout behavior.
|
||
const stub = {
|
||
parse(_s: string) { return { rootNode: null, delete: () => {} }; },
|
||
} as any;
|
||
|
||
expect(() => parseWithTimeout(stub, 'x', 50, 'foo.ts'))
|
||
.toThrow(/setTimeoutMicros/);
|
||
});
|
||
|
||
test('3. returns the tree on success; calls setTimeoutMicros once', () => {
|
||
const stub = {
|
||
_timeout: 0,
|
||
setTimeoutMicros(t: number) { this._timeout = t; },
|
||
parse(_s: string) { return { rootNode: { type: 'program' }, delete: () => {} }; },
|
||
};
|
||
|
||
const tree = parseWithTimeout(stub, 'x', 50, 'foo.ts') as { rootNode: { type: string } };
|
||
expect(tree.rootNode.type).toBe('program');
|
||
expect(stub._timeout).toBe(50_000); // microseconds = ms × 1000
|
||
});
|
||
|
||
test('4. ChunkerTimeoutError carries filePath + timeoutMs for actionable logs', () => {
|
||
const stub = { setTimeoutMicros() {}, parse() { return null; } };
|
||
try {
|
||
parseWithTimeout(stub, 'x', 30_000, 'src/big.ts');
|
||
throw new Error('should have thrown');
|
||
} catch (e: unknown) {
|
||
expect(e).toBeInstanceOf(ChunkerTimeoutError);
|
||
expect((e as ChunkerTimeoutError).filePath).toBe('src/big.ts');
|
||
expect((e as ChunkerTimeoutError).timeoutMs).toBe(30_000);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('chunkCodeTextFull — integration with real parser', () => {
|
||
test('5. default-timeout path on real code completes well under cap', async () => {
|
||
const result = await chunkCodeTextFull(REAL_TS, 'sample.ts');
|
||
expect(result.chunks.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('6. GBRAIN_CHUNKER_TIMEOUT_MS=1 reliably forces fallback', async () => {
|
||
// 1ms is below any plausible real-parser wall-clock. The parser
|
||
// returns null; chunkCodeTextFull catches the ChunkerTimeoutError
|
||
// and falls back to fallbackChunks (recursive text chunker).
|
||
await withEnv({ GBRAIN_CHUNKER_TIMEOUT_MS: '1' }, async () => {
|
||
const result = await chunkCodeTextFull(REAL_TS, 'sample.ts');
|
||
// Recursive fallback still produces chunks; assertion is that the
|
||
// call returned cleanly instead of hanging.
|
||
expect(Array.isArray(result.chunks)).toBe(true);
|
||
// v0.34 W2: extractAllEdges can emit imports/references from a partial
|
||
// parse (top-level statements survive grammar timeout). The original
|
||
// assertion was edges=[] under the calls-only extractor; with W2's
|
||
// imports/references emit, top-level imports show up even on the
|
||
// partial-parse fallback path. The contract that matters is `result`
|
||
// returned cleanly without hanging — edges array shape (empty or not)
|
||
// is engine-side noise.
|
||
expect(Array.isArray(result.edges)).toBe(true);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('cleanup contract under exception', () => {
|
||
test('7. parser.delete() still fires when timeout throws (codex C4)', async () => {
|
||
// chunkCodeTextFull's finally must reap parser+tree even when
|
||
// parseWithTimeout throws ChunkerTimeoutError. We can't directly
|
||
// observe parser.delete() in a real WASM run, but we can test the
|
||
// shape: forcing the env-timeout path through chunkCodeTextFull a
|
||
// few times must not leak (smoke test — Bun GC won't catch a real
|
||
// WASM leak but if cleanup were missing on the throw path, repeated
|
||
// calls would visibly degrade).
|
||
await withEnv({ GBRAIN_CHUNKER_TIMEOUT_MS: '1' }, async () => {
|
||
for (let i = 0; i < 5; i++) {
|
||
const result = await chunkCodeTextFull(REAL_TS, `sample-${i}.ts`);
|
||
expect(Array.isArray(result.chunks)).toBe(true);
|
||
}
|
||
});
|
||
});
|
||
});
|