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>
427 lines
18 KiB
TypeScript
427 lines
18 KiB
TypeScript
/**
|
|
* v0.19.0 Layer 5 — tree-sitter code chunker tests.
|
|
*
|
|
* Covers: detectCodeLanguage across all 29 file extensions, chunkCodeText
|
|
* on TS/Python/Go/Rust/Java + small-sibling merging + tokenizer accuracy
|
|
* + language fallback for unsupported extensions.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { chunkCodeText, detectCodeLanguage, CHUNKER_VERSION } from '../../src/core/chunkers/code.ts';
|
|
|
|
describe('CHUNKER_VERSION', () => {
|
|
test('v0.20.0 Cathedral II Layer 12 bumped to 4', () => {
|
|
expect(CHUNKER_VERSION).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('detectCodeLanguage', () => {
|
|
test('recognizes all 30 supported extensions', () => {
|
|
const cases: Record<string, string> = {
|
|
'foo.ts': 'typescript', 'foo.tsx': 'tsx', 'foo.mts': 'typescript', 'foo.cts': 'typescript',
|
|
'foo.js': 'javascript', 'foo.jsx': 'javascript', 'foo.mjs': 'javascript', 'foo.cjs': 'javascript',
|
|
'foo.py': 'python', 'foo.rb': 'ruby', 'foo.go': 'go',
|
|
'foo.rs': 'rust', 'foo.java': 'java', 'foo.cs': 'c_sharp',
|
|
'foo.cpp': 'cpp', 'foo.cc': 'cpp', 'foo.hpp': 'cpp',
|
|
'foo.c': 'c', 'foo.h': 'c',
|
|
'foo.php': 'php', 'foo.swift': 'swift', 'foo.kt': 'kotlin',
|
|
'foo.scala': 'scala', 'foo.lua': 'lua', 'foo.ex': 'elixir',
|
|
'foo.elm': 'elm', 'foo.ml': 'ocaml', 'foo.dart': 'dart',
|
|
'foo.zig': 'zig', 'foo.sol': 'solidity', 'foo.sh': 'bash',
|
|
'foo.css': 'css', 'foo.html': 'html', 'foo.vue': 'vue',
|
|
'foo.json': 'json', 'foo.yaml': 'yaml', 'foo.toml': 'toml',
|
|
// v0.41 D2 wave: SQL via DerekStride/tree-sitter-sql.
|
|
'foo.sql': 'sql', 'migrations/001_init.sql': 'sql',
|
|
};
|
|
for (const [path, expected] of Object.entries(cases)) {
|
|
expect(detectCodeLanguage(path)).toBe(expected as any);
|
|
}
|
|
});
|
|
|
|
test('returns null for unsupported extensions', () => {
|
|
expect(detectCodeLanguage('foo.md')).toBeNull();
|
|
expect(detectCodeLanguage('foo.txt')).toBeNull();
|
|
expect(detectCodeLanguage('README')).toBeNull();
|
|
});
|
|
|
|
test('is case-insensitive', () => {
|
|
expect(detectCodeLanguage('Main.GO')).toBe('go');
|
|
expect(detectCodeLanguage('App.TSX')).toBe('tsx');
|
|
expect(detectCodeLanguage('Schema.SQL')).toBe('sql');
|
|
});
|
|
});
|
|
|
|
// v0.41 D2 wave (#1173) — SQL via DerekStride/tree-sitter-sql.
|
|
// Step 0 inspection 2026-05-24 verified the grammar wraps every top-level
|
|
// statement in `program > statement > <kind>`. Tests assert the chunker
|
|
// dives through the wrapper and extracts the target name from DDL kinds.
|
|
describe('chunkCodeText — SQL', () => {
|
|
test('CREATE TABLE extracts table name as symbolName', async () => {
|
|
const sql = `CREATE TABLE this_table_name_is_long_enough_to_avoid_merging (
|
|
id SERIAL PRIMARY KEY,
|
|
email TEXT NOT NULL UNIQUE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP,
|
|
deleted_at TIMESTAMP,
|
|
metadata JSONB
|
|
);`;
|
|
// Use a small chunkSizeTokens so the single statement isn't merged
|
|
// with siblings (test fixture has only one, so no merging anyway).
|
|
const result = await chunkCodeText(sql, 'migrations/users.sql', { chunkSizeTokens: 50 });
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
const c = result[0]!;
|
|
expect(c.metadata.language).toBe('sql');
|
|
expect(c.metadata.symbolName).toBe('this_table_name_is_long_enough_to_avoid_merging');
|
|
expect(c.metadata.symbolType).toBe('table');
|
|
expect(c.text).toContain('[SQL]');
|
|
});
|
|
|
|
test('CREATE FUNCTION with $$ body extracts function name + parses cleanly', async () => {
|
|
const sql = `CREATE OR REPLACE FUNCTION get_user_by_email_long_function_name_here_for_no_merge(p_email TEXT)
|
|
RETURNS users AS $$
|
|
SELECT * FROM users WHERE email = p_email LIMIT 1;
|
|
$$ LANGUAGE SQL;`;
|
|
const result = await chunkCodeText(sql, 'migrations/fn.sql', { chunkSizeTokens: 50 });
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
const c = result.find(c => c.metadata.symbolName === 'get_user_by_email_long_function_name_here_for_no_merge');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolType).toBe('function');
|
|
expect(c!.metadata.language).toBe('sql');
|
|
// Dollar-quoted body must NOT crash the parser (codex F15 regression).
|
|
expect(c!.text).toContain('$$');
|
|
});
|
|
|
|
test('CREATE INDEX extracts index name', async () => {
|
|
const sql = `CREATE INDEX idx_a_b_c_d_e_f_g_long ON users (email, created_at, updated_at, deleted_at);`;
|
|
const result = await chunkCodeText(sql, 'idx.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'index');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('idx_a_b_c_d_e_f_g_long');
|
|
});
|
|
|
|
test('CREATE VIEW extracts view name', async () => {
|
|
const sql = `CREATE VIEW active_users_dashboard_view AS
|
|
SELECT id, email FROM users WHERE deleted_at IS NULL AND active = true;`;
|
|
const result = await chunkCodeText(sql, 'view.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'view');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('active_users_dashboard_view');
|
|
});
|
|
|
|
test('ALTER TABLE extracts table name', async () => {
|
|
const sql = `ALTER TABLE long_table_name_here_so_it_does_not_merge_with_sibling
|
|
ADD COLUMN created_at TIMESTAMP DEFAULT NOW(),
|
|
ADD COLUMN updated_at TIMESTAMP;`;
|
|
const result = await chunkCodeText(sql, 'alter.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'table');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('long_table_name_here_so_it_does_not_merge_with_sibling');
|
|
});
|
|
|
|
test('DML statements emit chunks but with symbolName=null (DDL signal only)', async () => {
|
|
const sql = `INSERT INTO users (email) VALUES ('a@b.com') RETURNING id, email, created_at;`;
|
|
const result = await chunkCodeText(sql, 'dml.sql', { chunkSizeTokens: 50 });
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
// The chunk emits but symbolName stays null — DML doesn't contribute
|
|
// to code-def. symbolType remains the underlying statement kind via
|
|
// normalizeSymbolType's fallback (`insert` here, unmapped → "insert").
|
|
expect(result[0]!.metadata.symbolName).toBeNull();
|
|
expect(result[0]!.metadata.language).toBe('sql');
|
|
});
|
|
|
|
test('mixed DDL + DML emits per-statement chunks, only DDL gets symbolName', async () => {
|
|
const sql = `CREATE TABLE long_mixed_table_name_for_no_merge_with_dml_below_it (id INT PRIMARY KEY, x TEXT, y TEXT, z TEXT);
|
|
INSERT INTO long_mixed_table_name_for_no_merge_with_dml_below_it (id, x) VALUES (1, 'aaaaaaaaaa');
|
|
INSERT INTO long_mixed_table_name_for_no_merge_with_dml_below_it (id, x) VALUES (2, 'bbbbbbbbbb');
|
|
SELECT * FROM long_mixed_table_name_for_no_merge_with_dml_below_it WHERE id = 1 ORDER BY x;`;
|
|
const result = await chunkCodeText(sql, 'mixed.sql', { chunkSizeTokens: 50 });
|
|
// Should have chunks for all 4 statements (DDL+DML each emit).
|
|
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
const namedChunks = result.filter(c => c.metadata.symbolName !== null);
|
|
expect(namedChunks.length).toBeGreaterThanOrEqual(1);
|
|
const ddl = namedChunks.find(c => c.metadata.symbolName === 'long_mixed_table_name_for_no_merge_with_dml_below_it');
|
|
expect(ddl).toBeDefined();
|
|
expect(ddl!.metadata.symbolType).toBe('table');
|
|
});
|
|
|
|
test('header includes "[SQL]" language tag', async () => {
|
|
const sql = 'CREATE TABLE x (id INT);';
|
|
const result = await chunkCodeText(sql, 'x.sql');
|
|
expect(result[0]!.text).toMatch(/^\[SQL\]/);
|
|
});
|
|
|
|
test('does not crash on invalid SQL', async () => {
|
|
// Per Step 0: even "SELECT FROM WHERE" parses to a select node, no
|
|
// throw. This pins that we don't regress to throwing.
|
|
const sql = 'SELECT FROM WHERE';
|
|
const result = await chunkCodeText(sql, 'bad.sql', { chunkSizeTokens: 50 });
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
// The chunk emits; symbol_name stays null.
|
|
expect(result[0]!.metadata.language).toBe('sql');
|
|
});
|
|
|
|
test('CREATE TRIGGER extracts trigger name + symbolType=trigger', async () => {
|
|
const sql = `CREATE TRIGGER long_audit_trigger_for_email_changes_on_users_table
|
|
AFTER UPDATE ON users
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION log_email_change();`;
|
|
const result = await chunkCodeText(sql, 'trig.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'trigger');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('long_audit_trigger_for_email_changes_on_users_table');
|
|
});
|
|
|
|
test('CREATE TYPE extracts enum name + symbolType=type', async () => {
|
|
const sql = `CREATE TYPE long_user_role_enum_avoid_merger_padding AS ENUM ('admin', 'member', 'guest', 'auditor');`;
|
|
const result = await chunkCodeText(sql, 'types.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'type');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('long_user_role_enum_avoid_merger_padding');
|
|
});
|
|
|
|
test('CREATE PROCEDURE extracts name + symbolType=procedure', async () => {
|
|
const sql = `CREATE PROCEDURE long_archive_old_users_procedure_no_merge(days_old INT)
|
|
LANGUAGE SQL AS $$
|
|
UPDATE users SET deleted_at = NOW() WHERE last_login_at < NOW() - INTERVAL '1 day' * days_old;
|
|
$$;`;
|
|
const result = await chunkCodeText(sql, 'proc.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'procedure');
|
|
expect(c).toBeDefined();
|
|
expect(c!.metadata.symbolName).toBe('long_archive_old_users_procedure_no_merge');
|
|
});
|
|
|
|
test('CREATE SCHEMA extracts schema name + symbolType=schema', async () => {
|
|
const sql = `CREATE SCHEMA IF NOT EXISTS analytics_long_schema_name_avoid_merge AUTHORIZATION analytics_owner;`;
|
|
const result = await chunkCodeText(sql, 'sch.sql', { chunkSizeTokens: 50 });
|
|
const c = result.find(c => c.metadata.symbolType === 'schema');
|
|
// Schema may not always be reachable depending on grammar version;
|
|
// accept either correct extraction OR null (test that it doesn't crash).
|
|
if (c) {
|
|
expect(c.metadata.symbolName).toBe('analytics_long_schema_name_avoid_merge');
|
|
}
|
|
// Always: chunk emits, language is sql.
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
expect(result[0]!.metadata.language).toBe('sql');
|
|
});
|
|
|
|
test('header symbolType for SQL chunks reflects inner DDL kind, not "statement"', async () => {
|
|
const sql = `CREATE TABLE structured_header_test_table_long_name (id INT, name TEXT, value TEXT, ts TIMESTAMP);`;
|
|
const result = await chunkCodeText(sql, 'header.sql', { chunkSizeTokens: 50 });
|
|
expect(result[0]!.text).toMatch(/^\[SQL\][^\n]*\btable\b/);
|
|
expect(result[0]!.text).not.toMatch(/\bstatement\b/i);
|
|
});
|
|
|
|
test('empty SQL input returns empty chunk array', async () => {
|
|
const result = await chunkCodeText('', 'empty.sql');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test('SQL-only whitespace returns empty chunk array', async () => {
|
|
const result = await chunkCodeText(' \n\n \t \n', 'whitespace.sql');
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — TypeScript', () => {
|
|
test('extracts top-level functions with correct symbol names', async () => {
|
|
const src = `export function calculateScore(items: number[]): number {
|
|
let sum = 0;
|
|
for (const i of items) { sum += i; }
|
|
if (sum < 0) return 0;
|
|
return sum / items.length;
|
|
}`;
|
|
const result = await chunkCodeText(src, 'calc.ts');
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
expect(result[0]!.metadata.language).toBe('typescript');
|
|
expect(result[0]!.metadata.symbolName).toBe('calculateScore');
|
|
expect(result[0]!.text).toContain('[TypeScript]');
|
|
expect(result[0]!.text).toContain('calc.ts');
|
|
});
|
|
|
|
test('extracts classes with methods', async () => {
|
|
const src = `export class Registry {
|
|
private items: Map<string, number> = new Map();
|
|
register(id: string, val: number): void { this.items.set(id, val); }
|
|
lookup(id: string): number | null { return this.items.get(id) ?? null; }
|
|
}`;
|
|
const result = await chunkCodeText(src, 'reg.ts');
|
|
const classChunk = result.find(c => c.metadata.symbolName === 'Registry');
|
|
expect(classChunk).toBeDefined();
|
|
// `export class Foo` is wrapped in export_statement at the AST level;
|
|
// symbol extraction still finds "Registry" but the type surface shows
|
|
// the wrapper. See normalizeSymbolType() for the mapping.
|
|
expect(classChunk!.metadata.symbolType).toMatch(/class|export/);
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — Python', () => {
|
|
test('extracts class_definition + function_definition', async () => {
|
|
const src = `class Animal:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def speak(self, sound):
|
|
return f"{self.name} says {sound}"
|
|
|
|
def pet_the_dog():
|
|
dog = Animal("Rex")
|
|
return dog.speak("woof woof woof woof woof")
|
|
`;
|
|
const result = await chunkCodeText(src, 'animal.py');
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
const allLanguages = result.map(c => c.metadata.language);
|
|
for (const lang of allLanguages) expect(lang).toBe('python');
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — Rust', () => {
|
|
test('extracts struct_item + impl_item + function_item', async () => {
|
|
const src = `pub struct UserRecord {
|
|
pub id: u64,
|
|
pub name: String,
|
|
pub active: bool,
|
|
pub score: f64,
|
|
}
|
|
|
|
impl UserRecord {
|
|
pub fn new(id: u64, name: String) -> Self {
|
|
Self { id, name, active: true, score: 0.0 }
|
|
}
|
|
|
|
pub fn deactivate(&mut self) {
|
|
self.active = false;
|
|
}
|
|
|
|
pub fn bump_score(&mut self, delta: f64) {
|
|
self.score += delta;
|
|
}
|
|
}
|
|
|
|
pub fn compute_total(records: &[UserRecord]) -> f64 {
|
|
records.iter().filter(|r| r.active).map(|r| r.score).sum()
|
|
}
|
|
`;
|
|
const result = await chunkCodeText(src, 'users.rs');
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(result[0]!.metadata.language).toBe('rust');
|
|
const headers = result.map(c => c.text.split('\n')[0]);
|
|
const hasRustTag = headers.some(h => h.includes('[Rust]'));
|
|
expect(hasRustTag).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — Go', () => {
|
|
test('extracts function + type + method declarations', async () => {
|
|
const src = `package main
|
|
|
|
import "fmt"
|
|
|
|
type Point struct {
|
|
X, Y int
|
|
}
|
|
|
|
func (p Point) Distance(other Point) float64 {
|
|
dx := float64(p.X - other.X)
|
|
dy := float64(p.Y - other.Y)
|
|
return dx*dx + dy*dy
|
|
}
|
|
|
|
func main() {
|
|
p1 := Point{X: 1, Y: 2}
|
|
p2 := Point{X: 5, Y: 6}
|
|
fmt.Println(p1.Distance(p2))
|
|
}
|
|
`;
|
|
const result = await chunkCodeText(src, 'main.go');
|
|
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
const headers = result.map(c => c.text.split('\n')[0]);
|
|
expect(headers.some(h => h.includes('[Go]'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — fallback for unsupported language', () => {
|
|
test('unsupported extension falls through to recursive chunker', async () => {
|
|
const src = 'this is not code. just text. lots of text. '.repeat(50);
|
|
const result = await chunkCodeText(src, 'unknown.xyz');
|
|
expect(result.length).toBeGreaterThan(0);
|
|
expect(result[0]!.metadata.symbolType).toBe('module');
|
|
expect(result[0]!.metadata.symbolName).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — small-sibling merging', () => {
|
|
test('small adjacent chunks are merged when chunkSizeTokens is generous', async () => {
|
|
// With a very large chunkSizeTokens, the merge threshold rises and
|
|
// more chunks qualify as "small" for accumulation. 10 tiny consts
|
|
// at chunkTarget=1000 gives a merge threshold of 150 — each const
|
|
// chunk (with its structured header) is ~20 tokens, so they all
|
|
// accumulate into one merged group up to the 1000-token budget.
|
|
const src = `const A = 1;
|
|
const B = 2;
|
|
const C = 3;
|
|
const D = 4;
|
|
const E = 5;
|
|
const F = 6;
|
|
const G = 7;
|
|
const H = 8;
|
|
const I = 9;
|
|
const J = 10;
|
|
`;
|
|
const result = await chunkCodeText(src, 'constants.ts', { chunkSizeTokens: 1000 });
|
|
expect(result.length).toBeLessThan(10); // at least some merging occurred
|
|
const merged = result.find(c => c.metadata.symbolType === 'merged');
|
|
expect(merged).toBeDefined();
|
|
});
|
|
|
|
test('large chunk stays independent', async () => {
|
|
const src = `export function bigFn() {
|
|
let result = 0;
|
|
for (let i = 0; i < 1000; i++) {
|
|
for (let j = 0; j < 1000; j++) {
|
|
result += Math.sqrt(i * i + j * j) * Math.sin(i) * Math.cos(j);
|
|
}
|
|
}
|
|
if (result > 0) { console.log('positive'); }
|
|
else if (result < 0) { console.log('negative'); }
|
|
else { console.log('zero'); }
|
|
return result;
|
|
}
|
|
`;
|
|
const result = await chunkCodeText(src, 'big.ts', { chunkSizeTokens: 100 });
|
|
const bigChunk = result.find(c => c.metadata.symbolName === 'bigFn');
|
|
expect(bigChunk).toBeDefined();
|
|
});
|
|
|
|
test('merged chunk has correct line range spanning merged siblings', async () => {
|
|
const src = `const X = 1;
|
|
|
|
const Y = 2;
|
|
|
|
const Z = 3;
|
|
`;
|
|
const result = await chunkCodeText(src, 'abc.ts', { chunkSizeTokens: 100 });
|
|
if (result.length === 1 && result[0]!.metadata.symbolType === 'merged') {
|
|
expect(result[0]!.metadata.startLine).toBe(1);
|
|
expect(result[0]!.metadata.endLine).toBeGreaterThanOrEqual(5);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — structured header', () => {
|
|
test('header includes language display name, path, line range, symbol', async () => {
|
|
const src = `export function myFunc() { return 42; }
|
|
`;
|
|
const result = await chunkCodeText(src, 'src/lib/foo.ts');
|
|
const first = result[0]!;
|
|
expect(first.text).toMatch(/^\[TypeScript\] src\/lib\/foo\.ts:\d+-\d+ /);
|
|
expect(first.text).toContain('myFunc');
|
|
});
|
|
});
|
|
|
|
describe('chunkCodeText — empty input', () => {
|
|
test('empty source returns empty array', async () => {
|
|
expect(await chunkCodeText('', 'foo.ts')).toEqual([]);
|
|
expect(await chunkCodeText(' \n ', 'foo.ts')).toEqual([]);
|
|
});
|
|
});
|