Files
gbrain/test/chunker-timeout.test.ts
T
Garry TanandClaude Opus 4.7 f5c32dff98 test(v0.34): update 4 pre-existing tests for new emit shapes + sourceId contract
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>
2026-05-14 09:39:05 -07:00

125 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
});
});
});