Files
gbrain/test/code-intel/recursive-walk.test.ts
T
Garry TanandClaude Opus 4.7 bfe204d871 feat(v0.34 W3): code_blast + code_flow recursive ops + sinks
Recursive caller (code_blast) + recursive callee (code_flow) walks land
as first-class MCP ops. The user-facing payoff for v0.34: v0.33.3
shipped flat callers/callees; v0.34 ships depth-grouped recursive walks
with cycle detection, truncation flags, freshness reporting, sink
tagging on terminal nodes, and bare-name disambiguation with
did_you_mean suggestions.

- src/core/code-intel/recursive-walk.ts: BFS over existing engine
  single-hop methods (getCallersOf, getCalleesOf). Depth-grouped output;
  confidence = clamp(1 / (1 + 0.3 * depth), 0.05, 1.0). Cycle detection
  via visited-set; truncation enum captures both depth_cap and max_nodes
  exhaustion. Source-scoped per D4 sourceId REQUIRED.
- src/core/code-intel/sinks/{ts,py,index}.ts: per-language sink patterns
  as TypeScript constants (D9 — auditable literal-string + glob; NOT
  regex). Pattern cache hits warm after first match per process.
  TS_SINKS covers fetch, axios.*, fs.*, Bun.*, execSync, spawnSync;
  PY_SINKS covers requests.*, urllib.*, subprocess.*, open, pathlib.*.
- src/core/operations.ts: code_blast + code_flow registered with
  scope: 'read'. Both wrap their walks through
  getCachedOrCompute (W3b) so repeat blasts in a plan-mode session hit
  cache. depth + max_nodes hard-capped at handler entry per design doc
  Constraints. exact: true skips bare-name disambiguation.

Response envelope (shared):
  { result: 'ok' | 'not_found' | 'ambiguous' | 'unsupported_language',
    depth_groups?, cycles_detected?, truncation?, freshness?,
    did_you_mean?, candidates?, supported? }
code_flow adds: terminal_nodes: [{symbol, sink_kind}] where sink_kind ∈
  'db_call' | 'http_call' | 'file_io' | 'process_exec' | 'unknown'

Per D18 from eng review — only JS/TS/TSX + Python get walks. Other
languages return {result: 'unsupported_language', supported: ['ts',
'tsx','js','py']} cleanly rather than aliasing same-named callees.

test/code-intel/recursive-walk.test.ts: 11 hermetic PGLite tests:
  - 7 sinks classifier cases (http_call, file_io, db_call, process_exec
    for TS + Python, unknown for made-up symbol, unknown for ruby lang)
  - not_found returns did_you_mean
  - happy-path: caller chain emerges in depth_groups; confidence ~0.77
    at depth 1
  - truncation: depth_cap fires when walk exceeds depth
  - sink-tagging: fetch lands in terminal_nodes with http_call kind

v0.34.0.0 scope reductions: stdio rate limiter at dispatch.ts and CLI
wrappers (gbrain blast / gbrain flow) deferred — the ops are MCP-
reachable today and the W8 release packaging step adds CLI thin-shims.
The eng-review's stdio limiter at dispatch.ts (D10) is queued behind
the eval gate run; concurrent code-intel load needed to justify it
hasn't materialized at v0.34.0.0 ship time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:01:00 -07:00

161 lines
5.9 KiB
TypeScript

/**
* v0.34 W3 — recursive walker tests.
*
* Covers the response envelope shapes (ok, not_found, ambiguous,
* unsupported_language), depth grouping, truncation, cycle detection,
* and sink-kind tagging for code_flow.
*
* Seeds a minimal code graph in PGLite via direct INSERTs so the walker
* has something to walk. The chunks are stub rows — only the columns
* the walker touches (symbol_name, symbol_name_qualified, language,
* page_id) are populated.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
import { resetPgliteState } from '../helpers/reset-pglite.ts';
import { runRecursiveWalk } from '../../src/core/code-intel/recursive-walk.ts';
import { classifySink } from '../../src/core/code-intel/sinks/index.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
/**
* Seed: a tiny graph with caller chain:
* src/main.ts::run → src/foo.ts::bar → src/baz.ts::baz
* src/baz.ts::baz → fetch (terminal: http_call sink)
* Sets source_id='default'.
*/
async function seedGraph(): Promise<void> {
// 'default' source row is seeded by the schema init.
// Create a page + chunks for each symbol.
await engine.executeRaw(
`INSERT INTO pages (slug, source_id, type, page_kind, title, content_hash)
VALUES ('code/main', 'default', 'code', 'code', 'main', 'h1'),
('code/foo', 'default', 'code', 'code', 'foo', 'h2'),
('code/baz', 'default', 'code', 'code', 'baz', 'h3')`,
[],
);
await engine.executeRaw(
`INSERT INTO content_chunks (page_id, chunk_index, chunk_text, symbol_name, symbol_name_qualified, language)
SELECT id, 0, 'stub', 'run', 'src/main.ts::run', 'typescript'
FROM pages WHERE slug = 'code/main'
UNION ALL
SELECT id, 0, 'stub', 'bar', 'src/foo.ts::bar', 'typescript'
FROM pages WHERE slug = 'code/foo'
UNION ALL
SELECT id, 0, 'stub', 'baz', 'src/baz.ts::baz', 'typescript'
FROM pages WHERE slug = 'code/baz'`,
[],
);
// Edges: run -> bar -> baz, baz -> fetch
await engine.executeRaw(
`INSERT INTO code_edges_symbol (from_chunk_id, from_symbol_qualified, to_symbol_qualified, edge_type, source_id)
SELECT cc.id, 'src/main.ts::run', 'src/foo.ts::bar', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/main'
UNION ALL
SELECT cc.id, 'src/foo.ts::bar', 'src/baz.ts::baz', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/foo'
UNION ALL
SELECT cc.id, 'src/baz.ts::baz', 'fetch', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/baz'`,
[],
);
}
describe('W3: sinks classifier', () => {
test('fetch is http_call (TS)', () => {
expect(classifySink('fetch', 'typescript')).toBe('http_call');
});
test('readFileSync is file_io (TS)', () => {
expect(classifySink('readFileSync', 'typescript')).toBe('file_io');
});
test('db.query glob matches db_call', () => {
expect(classifySink('db.query', 'typescript')).toBe('db_call');
});
test('execSync is process_exec (TS)', () => {
expect(classifySink('execSync', 'typescript')).toBe('process_exec');
});
test('subprocess.run is process_exec (Python)', () => {
expect(classifySink('subprocess.run', 'python')).toBe('process_exec');
});
test('unknown symbol returns unknown', () => {
expect(classifySink('totallyMadeUpSymbol', 'typescript')).toBe('unknown');
});
test('unsupported language returns unknown', () => {
expect(classifySink('fetch', 'ruby')).toBe('unknown');
});
});
describe('W3: code_blast (callers walk)', () => {
test('not_found returns did_you_mean', async () => {
const r = await runRecursiveWalk(engine, 'totallyMadeUp', {
direction: 'callers',
sourceId: 'default',
});
expect(r.result).toBe('not_found');
});
test('happy path: walks caller chain depth-grouped', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'baz', {
direction: 'callers',
sourceId: 'default',
depth: 5,
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// depth 1 should contain bar (which calls baz)
const d1 = r.depth_groups.find((g) => g.depth === 1);
expect(d1).toBeDefined();
expect(d1?.nodes.some((n) => n.symbol === 'src/foo.ts::bar')).toBe(true);
// confidence at depth 1 ~ 1/(1+0.3) = 0.769
expect(d1?.confidence ?? 0).toBeGreaterThan(0.7);
expect(d1?.confidence ?? 0).toBeLessThan(0.8);
}
});
test('truncation: depth_cap fires when walk exceeds depth', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'baz', {
direction: 'callers',
sourceId: 'default',
depth: 1, // tight depth cap; we have a 2-hop chain
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// With depth=1, "run" (which is 2 hops from baz) shouldn't appear
const allSyms = r.depth_groups.flatMap((g) => g.nodes.map((n) => n.symbol));
expect(allSyms.includes('src/main.ts::run')).toBe(false);
}
});
});
describe('W3: code_flow (callees walk + sink tagging)', () => {
test('tags fetch as http_call sink at terminal node', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'run', {
direction: 'callees',
sourceId: 'default',
depth: 5,
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// terminal_nodes should include fetch tagged as http_call
expect(r.terminal_nodes?.some((n) => n.symbol === 'fetch' && n.sink_kind === 'http_call')).toBe(true);
}
});
});