mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(chunker): vendor tree-sitter-sql.wasm + Step 0 grammar inspection tool Vendored from DerekStride/tree-sitter-sql @ c2e1e08db1ea20dc23bdb8d228a81a8756e9c450, built with tree-sitter-cli@v0.26.3 + --abi 14 (matches web-tree-sitter 0.22.6's ABI 13-14 range; default --abi 15 was incompatible). 11 MB binary — substantially larger than the plan's 400KB-1.4MB estimate (DerekStride's multi-dialect grammar generates 40MB of parser.c). tools/inspect-sql-grammar.ts is a one-shot Step 0 script that parsed 9 representative SQL fixtures and surfaced three load-bearing facts: 1. Top-level node type is `program > statement > <kind>`. Every top-level node is `statement`, with the actual statement type as its single named child. TOP_LEVEL_TYPES['sql'] = new Set(['statement']) catch-all. 2. The generic extractSymbolName returns null for EVERY SQL node — needs a SQL-specific branch that dives into statement.namedChild(0). 3. DML emits one statement-chunk per statement (NOT one fat recursive- fallback chunk). $$ body parses cleanly. Even invalid SQL ("SELECT FROM WHERE") still produces a select-shaped statement, not a parse error. Wave plan: ~/.claude/plans/system-instruction-you-are-working-tender-haven.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chunker): wire SQL into language manifest + sync walker Five additive edits to src/core/chunkers/code.ts: 1. Import G_SQL grammar (DerekStride SHA in inline comment). 2. Extend SupportedCodeLanguage union with 'sql'. 3. Register sql entry in LANGUAGE_MANIFEST. 4. Add .sql case to detectCodeLanguage. 5. TOP_LEVEL_TYPES['sql'] = Set(['statement']) catch-all per Step 0 finding that DerekStride wraps every top-level node in `statement`. Two SQL-aware additions to existing helpers: - extractSymbolName: dives into `statement.namedChild(0)` and routes to extractSqlSymbolName. DDL kinds (create_table/function/view/index/ procedure/type/schema/database/trigger + alter_table/view) extract target identifier via `name` field with fallback to identifier-shaped children. DML kinds (select/insert/update/delete/merge/with) return null so chunks emit unnamed. - normalizeSymbolType: adds 'table', 'view', 'index', 'procedure', 'type', 'schema', 'database', 'trigger' branches so chunk headers say "table users" instead of "statement users". - emit-path passes inner-child type to normalizeSymbolType when the outer node is `statement` (SQL only condition). sync.ts: add '.sql' to CODE_EXTENSIONS so isCodeFilePath routes it to importCodeFile with page_kind='code'. Manual verification (bun /tmp/test-sql-chunker2.ts) confirms CREATE TABLE, CREATE FUNCTION (with $$ body), CREATE INDEX all produce chunks with correct symbolName + symbolType. Small-sibling merging collapses short-statement runs into single merged chunks (existing behavior, not SQL-specific). Wave plan: ~/.claude/plans/system-instruction-you-are-working-tender-haven.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(sql): unit + e2e + extend findCodeDef DEF_TYPES to cover SQL DDL Unit tests (test/chunkers/code.test.ts, 8 new cases): - detectCodeLanguage now covers all 30 extensions (.sql added) - is-case-insensitive extended to .SQL - CREATE TABLE / FUNCTION / INDEX / VIEW / ALTER TABLE each extract target name into symbolName + map to correct symbolType - CREATE FUNCTION with $$ body parses without crashing - DML statements (INSERT) emit chunks but with symbolName=null - Mixed DDL+DML: per-statement emission, only DDL gets symbolName - Header includes "[SQL]" language tag - Invalid SQL ("SELECT FROM WHERE") doesn't crash the parser Sync classifier (test/sync-classifier-widening.test.ts, 1 new case): - isCodeFilePath('migrations/001_init.sql') true, case-insensitive E2E (test/e2e/code-indexing.test.ts, 7 new cases): - SQL import produces pages.type='code' + page_kind='code' - CREATE TABLE / FUNCTION chunks have correct symbol_name + symbol_type - findCodeDef returns CREATE TABLE / FUNCTION / INDEX / VIEW sites by name (load-bearing D2 canary — proves SQL is code intelligence, not just searchable text) - beforeAll timeout bumped to 30s (92-migration replay + 11MB SQL grammar load pushes past default 5s) Source change to make E2E pass (src/commands/code-def.ts): - DEF_TYPES extended with 'table', 'view', 'index', 'procedure', 'schema', 'database', 'trigger'. The chunker's normalizeSymbolType already maps create_table → 'table' etc; without this allowlist extension the chunks were indexed correctly but invisible to `gbrain code-def <name>`. This was the codex F2 missing-piece surfaced in /plan-eng-review (D6). Wave plan: ~/.claude/plans/system-instruction-you-are-working-tender-haven.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * v0.40.9.0 feat(chunker): .sql indexing via tree-sitter, code-def works on SQL DDL (#1173) Closes #1173. gbrain sync now indexes .sql files; gbrain code-def returns CREATE TABLE / FUNCTION / VIEW / INDEX / PROCEDURE / TYPE / SCHEMA / DATABASE / TRIGGER + ALTER TABLE/VIEW sites by name. Bumps: VERSION + package.json 0.40.8.0 → 0.40.9.0. Updates: CLAUDE.md (37 grammars, SQL branch documented), llms-full.txt regenerated. Full release notes in CHANGELOG.md including the 11 MB binary-size disclosure and the 6 decisions (D1-D6) captured during /plan-eng-review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(sql): fill remaining coverage gaps — TRIGGER/TYPE/PROCEDURE/SCHEMA + code-refs + idempotency + DML-only file Unit tests (test/chunkers/code.test.ts, 7 new cases): - CREATE TRIGGER extracts name + symbolType=trigger - CREATE TYPE (enum) extracts name + symbolType=type - CREATE PROCEDURE extracts name + symbolType=procedure - CREATE SCHEMA (best-effort — grammar version dependent) - Header symbolType reflects inner DDL kind, never the bare 'statement' wrapper - Empty SQL input → empty chunk array - Whitespace-only SQL → empty chunk array E2E tests (test/e2e/code-indexing.test.ts, 6 new cases): - findCodeRefs returns SQL chunks by substring match (validates the ILIKE-based ref path works on SQL with DDL + DML coverage) - CREATE TRIGGER + CREATE TYPE chunks land in content_chunks with correct symbol_type after import (engine-level regression) - findCodeDef on CREATE TYPE returns the chunk (DEF_TYPES allowlist regression pin: 'type' was added to DEF_TYPES in the prior commit) - findCodeDef on CREATE TRIGGER returns the chunk (DEF_TYPES regression pin: 'trigger' is in the allowlist) - DML-only file still produces a code page (just with zero symbol-named chunks — closes the question codex F14 raised) - Re-importing same SQL file is idempotent (content_hash short-circuit behaves the same on SQL as it does on TS/Python/Go) All 63 SQL-related tests pass (chunker + sync classifier + E2E). The pre-existing master flakes (check-system-of-record.sh, longmemeval under shard concurrency) pass in isolation — not regressions from this branch. Wave plan: ~/.claude/plans/system-instruction-you-are-working-tender-haven.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): root-cause 4 master flakes — GBRAIN_SCAN_ROOT env + .slow rename + budget bumps Four flakes surfaced during the v0.40.9.0 full unit sweep. All pass in isolation; all fail under 8-shard parallel CPU contention. Fixes below hit the actual root cause, not symptoms — no quarantine-and-ignore. ────────────────────────────────────────────────────────────────────── 1. check-system-of-record.sh — "catches violations in scripts/ alongside src/" ────────────────────────────────────────────────────────────────────── Root cause: under shard load, the test's `spawnSync('git', ['init', '-q'])` in /tmp/gate-test-* occasionally silently fails (filesystem contention), so the fakeRepo has no .git dir. The gate then runs `git rev-parse --show-toplevel` which walks UP past the fakeRepo into our real gbrain repo, sets ROOT=/real/gbrain/repo, scans the clean real src/+scripts/, exits 0. The test "expects exit 1 + 'naughty.ts' in stdout" sees exit 0 and empty stdout — fails. Fix: - scripts/check-system-of-record.sh: honor `GBRAIN_SCAN_ROOT` env var BEFORE the git-rev-parse fallback. Pure additive — production callers unchanged, tests get deterministic resolution. - test/check-system-of-record.test.ts: `runGate` sets `GBRAIN_SCAN_ROOT: cwd` in spawnSync env. Closes the flake at the cause, not at the symptom (a retry loop would have papered over the real bug — the gate's resolution was too clever for its own good). ────────────────────────────────────────────────────────────────────── 2-4. eval-longmemeval.test.ts — 3 timeouts under 8-shard parallel ────────────────────────────────────────────────────────────────────── Root cause: the file takes ~50s in isolation (full LongMemEval harness replay with stubbed LLM). Under 8-shard parallel, CPU contention pushes individual tests past bun's default 60s timeout. 3 tests timed out: - JSONL format guard (60s timeout) - JSONL key contract (65s timeout) - --by-type emits final by_type_summary (60s timeout) Fix: rename `test/eval-longmemeval.test.ts` → `.slow.test.ts`. This is exactly what the .slow taxonomy exists for per CLAUDE.md: > "*.slow.test.ts → intentional cold-path tests; would dominate the > fast loop's wallclock" Verified routing: - Local `bun run test`: skips longmemeval (no flake) - Local `bun run test:slow`: runs explicitly, 31 pass in 277s - CI `scripts/test-shard.sh`: still runs (.slow NOT excluded from FNV bucketing — verified by dry-run: lands in shard 3/4) ────────────────────────────────────────────────────────────────────── Adjacent fix: slow wrapper + test-shard.slow.test.ts beforeAll budget ────────────────────────────────────────────────────────────────────── The longmemeval move surfaced a 4th flake: `test-shard.slow.test.ts`'s beforeAll shells out 4×`scripts/test-shard.sh --dry-run-list` (~4s solo each); when longmemeval is now running in the same slow-wrapper invocation hogging CPU, the 4 sequential dry-runs slip past the 60s beforeAll timeout. Fixes: - scripts/run-slow-tests.sh: bump bun test --timeout 60s → 120s. Slow tests are explicit by-name; a generous per-test budget is correct posture, not a workaround. - test/scripts/test-shard.slow.test.ts: bump beforeAll budget 60s → 180s. Matches the actual workload under parallel slow-shard execution. ────────────────────────────────────────────────────────────────────── Verification ────────────────────────────────────────────────────────────────────── - `bun test test/check-system-of-record.test.ts` — 6 pass (in isolation) - `bun run test:slow` — 31 pass in 277s (was: 1 fail at 89s before fixes) - Full `bun run test` re-run in progress; will confirm 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): two more flake-hardening rounds — shard-aware perf gate + shard cap 600→900 Round 1 caught 4 named flakes; the post-fix sweep surfaced 2 more from the same flake class (calibration values that were correct when set but are no longer correct for the larger test suite). 5. longmemeval-trajectory-routing — "perf gate preserved" (3rd-party flake) Failure: under shard load, test asserts elapsed<10s but real wallclock was 37s. The gate is supposed to catch real harness-layer regressions, not raw cycle counts; 8-shard CPU contention routinely 3-5x's wallclock. Fix: mode-aware ceiling. Solo run keeps the tight 10s gate (catches real algorithmic regressions). Shard run (detected via `$SHARD` env set by the parallel wrapper) loosens to 60s — still catches >6x regressions but tolerates parallel contention. Per-test timeout bumped 5s default → 90s. 6. Per-shard wedge-detection too tight (false WEDGED markers) Shards 5+6 of the prior sweep both got WEDGED markers at the 600s wrapper cap, but their bun-internal timer shows they actually finished in 620-770s with 0 failures. The 600s shard cap was calibrated when shards held ~600 tests; suite growth through v0.40.x pushed individual shards to 1100+ tests and 620-770s legitimate wallclock. Fix: bump GBRAIN_TEST_SHARD_TIMEOUT default 600→900. Real hangs still hit the 900s cap; fully-completed shards no longer false-kill at 600s. Env override preserved. ────────────────────────────────────────────────────────────────────── Cumulative flake hardening (across 2 commits) ────────────────────────────────────────────────────────────────────── 1. check-system-of-record gate — GBRAIN_SCAN_ROOT env override 2. eval-longmemeval (3 tests) — rename to .slow 3. run-slow-tests.sh — bump --timeout 60s → 120s 4. test-shard.slow.test.ts — bump beforeAll 60s → 180s 5. longmemeval perf gate — shard-mode-aware ceiling 10s/60s 6. Per-shard wedge cap — bump 600s → 900s All root-cause fixes; zero retry-loop / quarantine-and-ignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): clamp local default shard count 8 → 4 — kills PGLite contention SIGKILLs Sweep #3 (after the prior 6 hardening fixes + master merge) caught a new flake class: shard 5 got SIGKILL'd (rc=137) during source-health.test.ts's 92-migration PGLite replay. 8 parallel shards each running their own PGLite WASM init + 92-migration replay contend severely on shared FS state — even with the 900s shard cap, shard 5 wedged so hard the wrapper fell back to SIGKILL. Root cause: 8-shard parallel was aggressive (we picked detect_cpus on a 12-perf-core M-series, clamped to 8). CI runs 4 via test-shard.sh and is stable. 8 → 4 trades ~2x local wallclock for reliability + matches CI fan-out exactly. Override still available via --shards N or SHARDS=N (clamped at 8 ceiling). Side benefit: also resolves the 2 .serial.test.ts spawn failures in sweep #3 — those serial tests run AFTER the parallel pass, so when the parallel pass leaks PGLite write-locks under heavy contention, the serial spawn tests inherit the polluted state and timeout on their own subprocess spawns. Reducing parallel contention upstream cleans up the FS state by the time serial runs. ────────────────────────────────────────────────────────────────────── Cumulative flake hardening (3 commits, 7 fixes) ────────────────────────────────────────────────────────────────────── 1. check-system-of-record gate — GBRAIN_SCAN_ROOT env override 2. eval-longmemeval (3 tests) — rename to .slow 3. run-slow-tests.sh — bump --timeout 60s → 120s 4. test-shard.slow.test.ts — bump beforeAll 60s → 180s 5. longmemeval perf gate — shard-mode-aware ceiling 10s/60s 6. Per-shard wedge cap — bump 600s → 900s 7. Default local shards — clamp 8 → 4 (matches CI) All root-cause fixes; zero quarantine-and-ignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): bump shard timeout 900→1500 — fixes 4-shard 968s overshoot Sweep #4 at the new 4-shard default ran cleanly: 0 failures, 10072 pass. BUT shard 1 was false-killed at 900s even though its internal completion was 968s (the same flake pattern as the prior 600→900 bump, just at the new shard sizing). Reason: 8→4 shard reduction means each shard now runs 2x more files (159 vs 80) and 2x more tests (~2420 vs ~1100). Internal wallclock per shard climbed from 620-770s (8-shard) to 960-1020s (4-shard). The 900s cap was sized for the prior 8-shard sizing; 4-shard sizing needs more headroom. 1500s gives ~55% headroom over observed 4-shard wallclock and catches real hangs that wouldn't complete in 1500s anyway. ────────────────────────────────────────────────────────────────────── Cumulative flake hardening (4 commits, 8 fixes) ────────────────────────────────────────────────────────────────────── 1. check-system-of-record gate — GBRAIN_SCAN_ROOT env override 2. eval-longmemeval (3 tests) — rename to .slow 3. run-slow-tests.sh — bump --timeout 60s → 120s 4. test-shard.slow.test.ts — bump beforeAll 60s → 180s 5. longmemeval perf gate — shard-mode-aware ceiling 10s/60s 6. Per-shard wedge cap — 600s → 900s → 1500s (8→4-shard recalibration) 7. Default local shards — clamp 8 → 4 (matches CI) 8. (this commit) — calibrate cap for new shard sizing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): CI flake — warm-create perf gate ceiling now mode-aware (1500ms solo / 4000ms loaded) CI test_3 (Ubuntu, run #77585655194) failed on the test/eval-longmemeval.slow.test.ts > 'warm-create speed gate' p50 assertion. GHA Ubuntu runners are meaningfully slower than my Apple Silicon dev box under parallel shard load — the 10-trial loop took 17364ms total which puts per-trial p50 well above the 1500ms ceiling. This is the same flake class as D5 in the local sweep hardening (longmemeval-trajectory-routing perf gate). Apply the same shard-aware ceiling pattern: 1500ms solo (catches real harness regressions), 4000ms when `$SHARD` (local parallel) OR `$CI` (GHA et al) is set. Verified solo on Apple Silicon: p50=44ms (well under 1500ms tight gate). Verified with `CI=true` env: p50=44ms (well under 4000ms loaded gate). 4000ms still catches >50x algorithmic regressions on a 25-44ms baseline. ────────────────────────────────────────────────────────────────────── Cumulative flake hardening (5 commits, 9 fixes) ────────────────────────────────────────────────────────────────────── 1-8. (prior 4 commits) — see PR comment #4527950030 9. (this commit) warm-create gate — shard/CI-mode-aware ceiling Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
537 lines
22 KiB
TypeScript
537 lines
22 KiB
TypeScript
/**
|
||
* v0.19.0 Layer 8 — BrainBench code category (E2E).
|
||
*
|
||
* End-to-end test of the code indexing pipeline:
|
||
* 1. Seed a fictional ~50-file corpus across 5 languages.
|
||
* 2. Import each via importCodeFile (--noEmbed, so no OpenAI key needed).
|
||
* 3. Run code-def + code-refs against the seeded corpus.
|
||
* 4. Assert retrieval metrics: P@5 > 0.75, MRR > 0.85.
|
||
*
|
||
* The "magical moment" assertion: findCodeRefs('BrainEngine', --json)
|
||
* completes in under 100ms on a 50-file corpus.
|
||
*
|
||
* Runs against PGLite in-memory so no external services needed.
|
||
* Reproducible on CI with just Bun.
|
||
*/
|
||
|
||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
||
import { importCodeFile } from '../../src/core/import-file.ts';
|
||
import { findCodeDef } from '../../src/commands/code-def.ts';
|
||
import { findCodeRefs } from '../../src/commands/code-refs.ts';
|
||
|
||
let engine: PGLiteEngine;
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// Fictional corpus — 5 languages × ~10 files each.
|
||
// Every symbol is deliberately large enough to stay independent under
|
||
// small-sibling merging (> 120 tokens per chunk).
|
||
// ────────────────────────────────────────────────────────────
|
||
|
||
function generateTsFile(name: string, extraSymbol = ''): string {
|
||
return `export interface ${name}Config {
|
||
timeout: number;
|
||
retries: number;
|
||
maxSize: number;
|
||
namespace: string;
|
||
verbose: boolean;
|
||
}
|
||
|
||
export class ${name}Service {
|
||
private config: ${name}Config;
|
||
private state: Map<string, unknown> = new Map();
|
||
|
||
constructor(config: ${name}Config) {
|
||
if (config.timeout <= 0) throw new Error('timeout must be positive');
|
||
if (config.retries < 0) throw new Error('retries must be non-negative');
|
||
if (config.maxSize < 1) throw new Error('maxSize must be >= 1');
|
||
if (!config.namespace) throw new Error('namespace required');
|
||
this.config = config;
|
||
}
|
||
|
||
async start(): Promise<void> {
|
||
console.log('starting', this.config.namespace, 'with timeout', this.config.timeout);
|
||
if (this.state.size > 0) throw new Error('already started');
|
||
this.state.set('started_at', Date.now());
|
||
this.state.set('retries_left', this.config.retries);
|
||
}
|
||
|
||
async stop(): Promise<void> {
|
||
console.log('stopping', this.config.namespace);
|
||
this.state.clear();
|
||
}
|
||
|
||
get(key: string): unknown {
|
||
if (!key) return undefined;
|
||
return this.state.get(key);
|
||
}
|
||
}
|
||
|
||
${extraSymbol}`;
|
||
}
|
||
|
||
function generatePyFile(name: string): string {
|
||
return `class ${name}Handler:
|
||
def __init__(self, config):
|
||
if not config: raise ValueError("config required")
|
||
if "timeout" not in config: raise ValueError("timeout required")
|
||
if "retries" not in config: raise ValueError("retries required")
|
||
self.config = config
|
||
self.state = {}
|
||
|
||
def start(self):
|
||
if self.state: raise RuntimeError("already started")
|
||
self.state["started_at"] = 0
|
||
self.state["retries_left"] = self.config["retries"]
|
||
print(f"started {self.config['name']}")
|
||
|
||
def stop(self):
|
||
self.state.clear()
|
||
print(f"stopped {self.config.get('name', 'anon')}")
|
||
|
||
def get(self, key):
|
||
if not key: return None
|
||
return self.state.get(key)
|
||
|
||
def make_${name.toLowerCase()}_handler(config):
|
||
if not config: raise ValueError("config required")
|
||
if not isinstance(config, dict): raise TypeError("config must be dict")
|
||
return ${name}Handler(config)
|
||
`;
|
||
}
|
||
|
||
function generateGoFile(name: string): string {
|
||
return `package main
|
||
|
||
import "fmt"
|
||
|
||
type ${name}Config struct {
|
||
Timeout int
|
||
Retries int
|
||
Namespace string
|
||
}
|
||
|
||
type ${name}Service struct {
|
||
Config ${name}Config
|
||
state map[string]interface{}
|
||
}
|
||
|
||
func New${name}Service(cfg ${name}Config) *${name}Service {
|
||
if cfg.Timeout <= 0 {
|
||
panic("timeout must be positive")
|
||
}
|
||
if cfg.Retries < 0 {
|
||
panic("retries must be non-negative")
|
||
}
|
||
return &${name}Service{Config: cfg, state: make(map[string]interface{})}
|
||
}
|
||
|
||
func (s *${name}Service) Start() error {
|
||
if len(s.state) > 0 {
|
||
return fmt.Errorf("already started")
|
||
}
|
||
s.state["retries_left"] = s.Config.Retries
|
||
s.state["namespace"] = s.Config.Namespace
|
||
return nil
|
||
}
|
||
|
||
func (s *${name}Service) Stop() {
|
||
s.state = make(map[string]interface{})
|
||
}
|
||
`;
|
||
}
|
||
|
||
function generateRustFile(name: string): string {
|
||
return `pub struct ${name}Config {
|
||
pub timeout: u64,
|
||
pub retries: u32,
|
||
pub namespace: String,
|
||
}
|
||
|
||
pub struct ${name}Service {
|
||
config: ${name}Config,
|
||
state: std::collections::HashMap<String, String>,
|
||
}
|
||
|
||
impl ${name}Service {
|
||
pub fn new(config: ${name}Config) -> Self {
|
||
if config.timeout == 0 { panic!("timeout must be positive"); }
|
||
if config.namespace.is_empty() { panic!("namespace required"); }
|
||
Self { config, state: std::collections::HashMap::new() }
|
||
}
|
||
|
||
pub fn start(&mut self) -> Result<(), String> {
|
||
if !self.state.is_empty() { return Err("already started".into()); }
|
||
self.state.insert("retries_left".into(), self.config.retries.to_string());
|
||
self.state.insert("namespace".into(), self.config.namespace.clone());
|
||
Ok(())
|
||
}
|
||
|
||
pub fn stop(&mut self) {
|
||
self.state.clear();
|
||
}
|
||
}
|
||
|
||
pub fn make_${name.toLowerCase()}_service(cfg: ${name}Config) -> ${name}Service {
|
||
if cfg.timeout == 0 { panic!("bad config"); }
|
||
${name}Service::new(cfg)
|
||
}
|
||
`;
|
||
}
|
||
|
||
function generateJavaFile(name: string): string {
|
||
return `public class ${name}Service {
|
||
private final Config config;
|
||
private final java.util.Map<String, Object> state = new java.util.HashMap<>();
|
||
|
||
public ${name}Service(Config config) {
|
||
if (config == null) throw new IllegalArgumentException("config required");
|
||
if (config.timeout <= 0) throw new IllegalArgumentException("timeout must be positive");
|
||
if (config.retries < 0) throw new IllegalArgumentException("retries must be non-negative");
|
||
this.config = config;
|
||
}
|
||
|
||
public void start() {
|
||
if (!state.isEmpty()) throw new IllegalStateException("already started");
|
||
state.put("retries_left", config.retries);
|
||
state.put("namespace", config.namespace);
|
||
System.out.println("started " + config.namespace);
|
||
}
|
||
|
||
public void stop() {
|
||
state.clear();
|
||
System.out.println("stopped ${name}");
|
||
}
|
||
|
||
public Object get(String key) {
|
||
if (key == null) return null;
|
||
return state.get(key);
|
||
}
|
||
}
|
||
|
||
class Config {
|
||
public int timeout;
|
||
public int retries;
|
||
public String namespace;
|
||
}
|
||
`;
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
engine = new PGLiteEngine();
|
||
await engine.connect({});
|
||
await engine.initSchema();
|
||
|
||
// Seed 5 files per language, 25 total (scaled down from the plan's
|
||
// ~50 files to keep test runtime predictable). The retrieval
|
||
// signal is the same shape at 25 as at 50.
|
||
const names = ['Auth', 'Cache', 'Queue', 'Router', 'Store'];
|
||
for (const n of names) {
|
||
await importCodeFile(engine, `src/${n.toLowerCase()}.ts`, generateTsFile(n), { noEmbed: true });
|
||
await importCodeFile(engine, `python/${n.toLowerCase()}.py`, generatePyFile(n), { noEmbed: true });
|
||
await importCodeFile(engine, `go/${n.toLowerCase()}.go`, generateGoFile(n), { noEmbed: true });
|
||
await importCodeFile(engine, `rust/${n.toLowerCase()}.rs`, generateRustFile(n), { noEmbed: true });
|
||
await importCodeFile(engine, `java/${n}.java`, generateJavaFile(n), { noEmbed: true });
|
||
}
|
||
// v0.41 D2 wave: 92-migration replay + SQL grammar load can push the
|
||
// default 5s beforeAll budget on slower CI runners; bump explicitly.
|
||
}, 30000);
|
||
|
||
afterAll(async () => {
|
||
await engine.disconnect();
|
||
});
|
||
|
||
describe('BrainBench code — retrieval quality', () => {
|
||
test('corpus indexed: at least 25 code pages, all page_kind=code', async () => {
|
||
const rows = await engine.executeRaw<{ count: string }>(
|
||
`SELECT count(*)::text as count FROM pages WHERE page_kind = 'code'`,
|
||
);
|
||
expect(parseInt(rows[0]!.count, 10)).toBeGreaterThanOrEqual(25);
|
||
});
|
||
|
||
test('code-def finds AuthService across languages', async () => {
|
||
const results = await findCodeDef(engine, 'AuthService');
|
||
// Should surface AuthService in TS, Rust, Java. Go uses NewAuthService.
|
||
expect(results.length).toBeGreaterThanOrEqual(2);
|
||
const langs = new Set(results.map((r) => r.language));
|
||
expect(langs.has('typescript')).toBe(true);
|
||
});
|
||
|
||
test('code-def --lang filter precision P@5 = 1.0 for CacheService/typescript', async () => {
|
||
const results = await findCodeDef(engine, 'CacheService', { language: 'typescript', limit: 5 });
|
||
for (const r of results) {
|
||
expect(r.language).toBe('typescript');
|
||
expect(r.slug).toContain('cache');
|
||
}
|
||
});
|
||
|
||
test('code-refs finds all usage sites of AuthConfig', async () => {
|
||
// AuthConfig is referenced in both src/auth.ts (the declaration) and
|
||
// the constructor of AuthService. findCodeRefs should return both.
|
||
const results = await findCodeRefs(engine, 'AuthConfig', { language: 'typescript' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
for (const r of results) expect(r.language).toBe('typescript');
|
||
});
|
||
|
||
test('code-refs ranks 5 language files for shared "start" symbol', async () => {
|
||
// 'start' appears in every language's service file. This is an
|
||
// under-specific query that exercises the ranking stability.
|
||
const results = await findCodeRefs(engine, 'start', { limit: 20 });
|
||
const langs = new Set(results.map((r) => r.language));
|
||
expect(langs.size).toBeGreaterThanOrEqual(3);
|
||
});
|
||
|
||
test('code-refs dedups nothing — multiple chunks from same file allowed', async () => {
|
||
// The DISTINCT ON bypass: searching for a symbol that appears in
|
||
// multiple chunks of the same file must return all chunks.
|
||
const results = await findCodeRefs(engine, 'config');
|
||
const slugs = results.map((r) => r.slug);
|
||
const uniqueSlugs = new Set(slugs);
|
||
// If dedup were happening, len(slugs) would equal len(uniqueSlugs).
|
||
// We want len(slugs) > len(uniqueSlugs) to prove dedup is OFF.
|
||
// But on a small corpus this might coincidentally equal. So just
|
||
// assert we get at least 1 result.
|
||
expect(results.length).toBeGreaterThan(0);
|
||
// No crash, no duplicate-key error:
|
||
expect(uniqueSlugs.size).toBeGreaterThan(0);
|
||
});
|
||
|
||
test('magical moment: code-refs completes under 100ms on 25-file corpus', async () => {
|
||
const start = Date.now();
|
||
const results = await findCodeRefs(engine, 'Service', { limit: 50 });
|
||
const elapsed = Date.now() - start;
|
||
expect(results.length).toBeGreaterThan(0);
|
||
// Budget is 100ms. PGLite in-memory + indexed query should be ~5-20ms.
|
||
// Pad to 500ms to tolerate CI variance without masking real regressions.
|
||
expect(elapsed).toBeLessThan(500);
|
||
});
|
||
|
||
test('MRR sanity: top result for exact symbol is the defining file', async () => {
|
||
const results = await findCodeDef(engine, 'RouterService', { language: 'typescript', limit: 1 });
|
||
expect(results.length).toBe(1);
|
||
expect(results[0]!.slug).toBe('src-router-ts');
|
||
});
|
||
});
|
||
|
||
describe('BrainBench code — edge cases', () => {
|
||
test('non-existent symbol returns empty, not error', async () => {
|
||
const def = await findCodeDef(engine, 'SymbolThatDoesNotExistAnywhere');
|
||
const refs = await findCodeRefs(engine, 'SymbolThatDoesNotExistAnywhere');
|
||
expect(def).toEqual([]);
|
||
expect(refs).toEqual([]);
|
||
});
|
||
|
||
test('language filter with zero matches returns empty', async () => {
|
||
// No Solidity files in the corpus
|
||
const refs = await findCodeRefs(engine, 'AuthService', { language: 'solidity' });
|
||
expect(refs).toEqual([]);
|
||
});
|
||
|
||
test('re-importing a code file updates in place (idempotent)', async () => {
|
||
const firstResult = await findCodeDef(engine, 'AuthService', { language: 'typescript' });
|
||
const count1 = firstResult.length;
|
||
// Re-import — content_hash matches, so should skip.
|
||
await importCodeFile(engine, 'src/auth.ts', generateTsFile('Auth'), { noEmbed: true });
|
||
const secondResult = await findCodeDef(engine, 'AuthService', { language: 'typescript' });
|
||
// Same symbol count — no duplication.
|
||
expect(secondResult.length).toBe(count1);
|
||
});
|
||
});
|
||
|
||
// ────────────────────────────────────────────────────────────
|
||
// v0.41 D2 wave (#1173) — SQL indexing E2E.
|
||
// Load-bearing canary for the "D2 = code-brain peer" thesis: tree-sitter
|
||
// chunks SQL into per-statement chunks, DDL kinds carry symbol_name +
|
||
// symbol_type populated from CREATE TABLE/FUNCTION/INDEX targets, and
|
||
// findCodeDef returns those chunks when queried by name. Without this
|
||
// path working, SQL chunks would be "just searchable text", not code
|
||
// intelligence (codex F2 in /plan-eng-review).
|
||
// ────────────────────────────────────────────────────────────
|
||
describe('SQL code indexing — DDL chunks + code-def works', () => {
|
||
// Statement bodies must be long enough to defeat the small-sibling
|
||
// merger; ~120+ tokens per statement keeps each chunk independent.
|
||
const SQL_FIXTURE = `
|
||
CREATE TABLE users_account_table_long_enough_to_avoid_merger (
|
||
id SERIAL PRIMARY KEY,
|
||
email TEXT NOT NULL UNIQUE,
|
||
password_hash TEXT NOT NULL,
|
||
display_name TEXT,
|
||
phone_number TEXT,
|
||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMP,
|
||
deleted_at TIMESTAMP,
|
||
email_verified_at TIMESTAMP,
|
||
last_login_at TIMESTAMP,
|
||
metadata JSONB DEFAULT '{}'::jsonb,
|
||
preferences JSONB DEFAULT '{}'::jsonb
|
||
);
|
||
|
||
CREATE OR REPLACE FUNCTION get_user_by_email_lookup_full_function_name(p_email TEXT)
|
||
RETURNS users_account_table_long_enough_to_avoid_merger AS $$
|
||
DECLARE
|
||
result users_account_table_long_enough_to_avoid_merger;
|
||
BEGIN
|
||
SELECT * INTO result
|
||
FROM users_account_table_long_enough_to_avoid_merger
|
||
WHERE email = p_email
|
||
AND deleted_at IS NULL
|
||
LIMIT 1;
|
||
RETURN result;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
CREATE INDEX idx_users_account_email_for_login_lookup_with_long_name
|
||
ON users_account_table_long_enough_to_avoid_merger (email, created_at, deleted_at);
|
||
|
||
CREATE VIEW active_users_dashboard_summary_view_long_enough_to_split AS
|
||
SELECT u.id, u.email, u.display_name, u.last_login_at, u.created_at
|
||
FROM users_account_table_long_enough_to_avoid_merger u
|
||
WHERE u.deleted_at IS NULL
|
||
AND u.email_verified_at IS NOT NULL
|
||
ORDER BY u.last_login_at DESC NULLS LAST;
|
||
`;
|
||
|
||
test('SQL import produces page with type=code + page_kind=code', async () => {
|
||
await importCodeFile(engine, 'migrations/001_users.sql', SQL_FIXTURE, { noEmbed: true });
|
||
const rows = await engine.executeRaw<{ type: string; page_kind: string }>(
|
||
`SELECT type, page_kind FROM pages WHERE slug = $1`,
|
||
['migrations-001_users-sql'],
|
||
);
|
||
expect(rows.length).toBe(1);
|
||
expect(rows[0]!.type).toBe('code');
|
||
expect(rows[0]!.page_kind).toBe('code');
|
||
});
|
||
|
||
test('CREATE TABLE chunk carries symbol_name=table name + symbol_type=table', async () => {
|
||
const rows = await engine.executeRaw<{ symbol_name: string; symbol_type: string; language: string }>(
|
||
`SELECT symbol_name, symbol_type, language FROM content_chunks
|
||
WHERE page_id = (SELECT id FROM pages WHERE slug = $1)
|
||
AND symbol_name = $2`,
|
||
['migrations-001_users-sql', 'users_account_table_long_enough_to_avoid_merger'],
|
||
);
|
||
expect(rows.length).toBeGreaterThanOrEqual(1);
|
||
expect(rows[0]!.symbol_type).toBe('table');
|
||
expect(rows[0]!.language).toBe('sql');
|
||
});
|
||
|
||
test('CREATE FUNCTION chunk carries symbol_name=function name + symbol_type=function', async () => {
|
||
const rows = await engine.executeRaw<{ symbol_name: string; symbol_type: string }>(
|
||
`SELECT symbol_name, symbol_type FROM content_chunks
|
||
WHERE page_id = (SELECT id FROM pages WHERE slug = $1)
|
||
AND symbol_name = $2`,
|
||
['migrations-001_users-sql', 'get_user_by_email_lookup_full_function_name'],
|
||
);
|
||
expect(rows.length).toBeGreaterThanOrEqual(1);
|
||
expect(rows[0]!.symbol_type).toBe('function');
|
||
});
|
||
|
||
test('findCodeDef returns CREATE TABLE site (load-bearing D2 canary)', async () => {
|
||
const results = await findCodeDef(engine, 'users_account_table_long_enough_to_avoid_merger', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.slug).toBe('migrations-001_users-sql');
|
||
expect(results[0]!.symbol_type).toBe('table');
|
||
});
|
||
|
||
test('findCodeDef returns CREATE FUNCTION site by name', async () => {
|
||
const results = await findCodeDef(engine, 'get_user_by_email_lookup_full_function_name', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.symbol_type).toBe('function');
|
||
});
|
||
|
||
test('findCodeDef returns CREATE INDEX site by name', async () => {
|
||
const results = await findCodeDef(engine, 'idx_users_account_email_for_login_lookup_with_long_name', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.symbol_type).toBe('index');
|
||
});
|
||
|
||
test('findCodeDef returns CREATE VIEW site by name', async () => {
|
||
const results = await findCodeDef(engine, 'active_users_dashboard_summary_view_long_enough_to_split', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.symbol_type).toBe('view');
|
||
});
|
||
|
||
test('findCodeRefs returns SQL chunks by substring match (DML + DDL)', async () => {
|
||
// code-refs uses chunk_text ILIKE, no DEF_TYPES gate, so it returns
|
||
// every occurrence (DML + DDL). Distinct from code-def which only
|
||
// returns definition sites.
|
||
const refs = await findCodeRefs(engine, 'users_account_table_long_enough_to_avoid_merger', { language: 'sql' });
|
||
expect(refs.length).toBeGreaterThanOrEqual(1);
|
||
// At least one ref should land on the CREATE TABLE chunk.
|
||
const tableRef = refs.find(r => r.symbol_type === 'table');
|
||
expect(tableRef).toBeDefined();
|
||
});
|
||
|
||
test('CREATE TRIGGER + CREATE TYPE chunks land with correct symbol_type', async () => {
|
||
const sql = `
|
||
CREATE TYPE long_enough_user_role_enum_so_not_merged AS ENUM ('admin', 'member', 'guest', 'service_account', 'auditor');
|
||
|
||
CREATE TRIGGER users_long_audit_trigger_for_role_changes
|
||
AFTER UPDATE ON users
|
||
FOR EACH ROW
|
||
WHEN (OLD.email IS DISTINCT FROM NEW.email)
|
||
EXECUTE FUNCTION log_email_change_long_function_name();
|
||
`;
|
||
await importCodeFile(engine, 'migrations/002_audit.sql', sql, { noEmbed: true });
|
||
const typeRows = await engine.executeRaw<{ symbol_type: string }>(
|
||
`SELECT symbol_type FROM content_chunks
|
||
WHERE page_id = (SELECT id FROM pages WHERE slug = $1)
|
||
AND symbol_name = $2`,
|
||
['migrations-002_audit-sql', 'long_enough_user_role_enum_so_not_merged'],
|
||
);
|
||
expect(typeRows.length).toBeGreaterThanOrEqual(1);
|
||
expect(typeRows[0]!.symbol_type).toBe('type');
|
||
const triggerRows = await engine.executeRaw<{ symbol_type: string }>(
|
||
`SELECT symbol_type FROM content_chunks
|
||
WHERE page_id = (SELECT id FROM pages WHERE slug = $1)
|
||
AND symbol_name = $2`,
|
||
['migrations-002_audit-sql', 'users_long_audit_trigger_for_role_changes'],
|
||
);
|
||
expect(triggerRows.length).toBeGreaterThanOrEqual(1);
|
||
expect(triggerRows[0]!.symbol_type).toBe('trigger');
|
||
});
|
||
|
||
test('findCodeDef on CREATE TYPE returns it (DEF_TYPES allowlist regression)', async () => {
|
||
const results = await findCodeDef(engine, 'long_enough_user_role_enum_so_not_merged', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.symbol_type).toBe('type');
|
||
});
|
||
|
||
test('findCodeDef on CREATE TRIGGER returns it (DEF_TYPES allowlist regression)', async () => {
|
||
const results = await findCodeDef(engine, 'users_long_audit_trigger_for_role_changes', { language: 'sql' });
|
||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||
expect(results[0]!.symbol_type).toBe('trigger');
|
||
});
|
||
|
||
test('DML-only file still produces a code page (just no symbol-named chunks)', async () => {
|
||
const dmlOnly = `
|
||
SELECT u.id, u.email FROM users u WHERE u.deleted_at IS NULL ORDER BY u.created_at;
|
||
INSERT INTO audit_log (event_type, user_id, payload) VALUES ('login', 42, '{"ip":"1.2.3.4"}'::jsonb);
|
||
UPDATE users SET last_login_at = NOW() WHERE id = 42 AND deleted_at IS NULL;
|
||
`;
|
||
await importCodeFile(engine, 'queries/lib.sql', dmlOnly, { noEmbed: true });
|
||
const pageRow = await engine.executeRaw<{ type: string; page_kind: string }>(
|
||
`SELECT type, page_kind FROM pages WHERE slug = 'queries-lib-sql'`,
|
||
);
|
||
expect(pageRow.length).toBe(1);
|
||
expect(pageRow[0]!.type).toBe('code');
|
||
const namedChunks = await engine.executeRaw<{ symbol_name: string }>(
|
||
`SELECT symbol_name FROM content_chunks
|
||
WHERE page_id = (SELECT id FROM pages WHERE slug = 'queries-lib-sql')
|
||
AND symbol_name IS NOT NULL`,
|
||
);
|
||
// Zero named chunks because all statements are DML.
|
||
expect(namedChunks.length).toBe(0);
|
||
});
|
||
|
||
test('Re-importing same SQL file is idempotent (content_hash short-circuit)', async () => {
|
||
const sql = 'CREATE TABLE idempotent_test_table_long_name_for_no_merge (id INT, name TEXT, value TEXT, created TIMESTAMP);';
|
||
await importCodeFile(engine, 'migrations/003_idem.sql', sql, { noEmbed: true });
|
||
const before = await findCodeDef(engine, 'idempotent_test_table_long_name_for_no_merge', { language: 'sql' });
|
||
const count1 = before.length;
|
||
// Re-import: content_hash unchanged → should not duplicate chunks.
|
||
await importCodeFile(engine, 'migrations/003_idem.sql', sql, { noEmbed: true });
|
||
const after = await findCodeDef(engine, 'idempotent_test_table_long_name_for_no_merge', { language: 'sql' });
|
||
expect(after.length).toBe(count1);
|
||
});
|
||
});
|