mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(code-intel): readiness signal on code-def/refs/callers/callees (#1780 Gap 1) New src/core/code-graph-readiness.ts: resolveCodeReadiness() returns a typed status (not_built | indexing | ready | unknown) + ready boolean so callers can tell "graph not built / still indexing" apart from "genuinely no match" when count===0. EXISTS-based (cheap), chunk-grain, resolver-version-matching pending predicate, fail-open. Wired into the 4 CLI envelopes (+ human hint) and the 4 MCP op handlers. def/refs are 2-state brain-wide; callers/callees 3-state scoped. * feat(db-lock): automatic same-host dead-pid cycle-lock takeover (#1780 Gap 3) tryAcquireDbLock now reclaims a held, not-TTL-expired lock when the same-host holder is provably dead (process.kill ESRCH) past a 60s grace, via guarded DELETE + one normal-upsert retry returning the normal handle. New shared injectable classifyHolderLiveness/isHolderDeadLocally (EPERM treated as ALIVE — never steals a live lock). runBreakLock's safe path consumes the shared predicate, fixing its prior EPERM-as-dead bug. Cross-host stays TTL-only. * feat(init): validate the embedding key at gbrain init (#1780 Gap 2) New src/core/init-embed-check.ts: config-only diagnoseEmbedding (missing key, all providers) + best-effort 1-token live test-embed (invalid/expired key, 5s timeout, never blocks). Loud warning to stderr, init still exits 0; skipped by --no-embedding / --skip-embed-check / GBRAIN_INIT_SKIP_EMBED_CHECK=1. Builds the effective env (process.env + file-plane keys + --key) via buildGatewayConfig, extracted to src/core/ai/build-gateway-config.ts (cli.ts re-exports) so the check sees the same keys + provider base URLs as runtime. embedding_check added to --json. * chore: bump version and changelog (v0.42.14.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document the #1780 zero-config gaps for v0.42.14.0 CLAUDE.md Key Files: add src/core/code-graph-readiness.ts, init-embed-check.ts, ai/build-gateway-config.ts, and the db-lock auto-takeover + code-* readiness field behaviors. Regenerate llms-full.txt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
/**
|
|
* #1780 Gap 1 — code-graph readiness signal.
|
|
*
|
|
* Verifies the typed readiness contract that lets code-* callers tell
|
|
* "graph not built / still indexing" apart from "genuinely no match" when
|
|
* count === 0:
|
|
* - empty brain → not_built (both grains)
|
|
* - code synced, edges not resolved → symbol grain ready, edge grain indexing
|
|
* - edges resolved → edge grain ready
|
|
* - count > 0 → ready short-circuit (no query)
|
|
* - source scoping (scoped miss → not_built; allSources → brain-wide)
|
|
* - DB error → unknown, fail-open (CRITICAL regression)
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
import { importCodeFile } from '../src/core/import-file.ts';
|
|
import { resolveCodeReadiness, readinessHint } from '../src/core/code-graph-readiness.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clean slate per test: remove all chunks + pages so empty-brain cases hold.
|
|
await engine.executeRaw('DELETE FROM content_chunks');
|
|
await engine.executeRaw('DELETE FROM pages');
|
|
});
|
|
|
|
const SAMPLE = `export function alpha(x: number): number {
|
|
return beta(x) + 1;
|
|
}
|
|
|
|
export function beta(y: number): number {
|
|
return y * 2;
|
|
}
|
|
`;
|
|
|
|
describe('resolveCodeReadiness — empty brain', () => {
|
|
test('symbol grain → not_built when no code exists', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'symbol', count: 0 });
|
|
expect(r.status).toBe('not_built');
|
|
expect(r.ready).toBe(false);
|
|
expect(r.has_code).toBe(false);
|
|
});
|
|
|
|
test('edge grain → not_built when no code exists', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'edge', count: 0 });
|
|
expect(r.status).toBe('not_built');
|
|
expect(r.ready).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolveCodeReadiness — code synced, edges unresolved', () => {
|
|
beforeEach(async () => {
|
|
// importCodeFile writes code chunks with edges_backfilled_at = NULL
|
|
// (resolve phase hasn't run), exactly the "graph still building" state.
|
|
await importCodeFile(engine, 'src/sample.ts', SAMPLE, { noEmbed: true });
|
|
});
|
|
|
|
test('symbol grain → ready (symbol metadata is at chunk time)', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'symbol', count: 0 });
|
|
expect(r.status).toBe('ready');
|
|
expect(r.ready).toBe(true);
|
|
expect(r.has_code).toBe(true);
|
|
});
|
|
|
|
test('edge grain → indexing (edges pending resolution)', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'edge', count: 0 });
|
|
expect(r.status).toBe('indexing');
|
|
expect(r.ready).toBe(false);
|
|
expect(r.pending_edges).toBe(true);
|
|
});
|
|
|
|
test('edge grain → ready once edges_backfilled_at is stamped fresh', async () => {
|
|
// Mirror what the resolve_symbol_edges phase does: stamp every code chunk.
|
|
await engine.executeRaw('UPDATE content_chunks SET edges_backfilled_at = NOW()');
|
|
const r = await resolveCodeReadiness(engine, { kind: 'edge', count: 0 });
|
|
expect(r.status).toBe('ready');
|
|
expect(r.ready).toBe(true);
|
|
expect(r.pending_edges).toBe(false);
|
|
});
|
|
|
|
test('count > 0 short-circuits to ready with no probe', async () => {
|
|
// Even with pending edges, a non-empty result is trivially ready.
|
|
const r = await resolveCodeReadiness(engine, { kind: 'edge', count: 3 });
|
|
expect(r.status).toBe('ready');
|
|
expect(r.ready).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('resolveCodeReadiness — source scoping', () => {
|
|
beforeEach(async () => {
|
|
await importCodeFile(engine, 'src/sample.ts', SAMPLE, { noEmbed: true });
|
|
});
|
|
|
|
test('scoped to a source with no code → not_built', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'symbol', count: 0, sourceId: 'no-such-source' });
|
|
expect(r.status).toBe('not_built');
|
|
});
|
|
|
|
test('scoped to the default source (where code lives) → ready (symbol)', async () => {
|
|
const r = await resolveCodeReadiness(engine, { kind: 'symbol', count: 0, sourceId: 'default' });
|
|
expect(r.status).toBe('ready');
|
|
});
|
|
|
|
test('allSources ignores a non-matching sourceId and goes brain-wide', async () => {
|
|
const r = await resolveCodeReadiness(engine, {
|
|
kind: 'symbol', count: 0, sourceId: 'no-such-source', allSources: true,
|
|
});
|
|
expect(r.status).toBe('ready');
|
|
});
|
|
});
|
|
|
|
describe('resolveCodeReadiness — fail-open (CRITICAL regression)', () => {
|
|
test('DB error → status unknown, ready false, never throws', async () => {
|
|
const broken = {
|
|
kind: 'pglite',
|
|
executeRaw: async () => { throw new Error('boom'); },
|
|
} as unknown as BrainEngine;
|
|
const r = await resolveCodeReadiness(broken, { kind: 'edge', count: 0 });
|
|
expect(r.status).toBe('unknown');
|
|
expect(r.ready).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('readinessHint', () => {
|
|
test('not_built / indexing / unknown produce a hint; ready does not', () => {
|
|
expect(readinessHint({ status: 'not_built', ready: false, has_code: false, pending_edges: false })).toContain('not built');
|
|
expect(readinessHint({ status: 'indexing', ready: false, has_code: true, pending_edges: true })).toContain('still building');
|
|
expect(readinessHint({ status: 'unknown', ready: false, has_code: false, pending_edges: false })).toContain('unavailable');
|
|
expect(readinessHint({ status: 'ready', ready: true, has_code: true, pending_edges: false })).toBeNull();
|
|
});
|
|
});
|