mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 19:49:14 +00:00
Master landed significant work since this branch was cut (v0.15.x → v0.16.x →
v0.17.0 gbrain dream + runCycle → v0.18.0 multi-source brains → v0.18.1 RLS
hardening). Bumped this branch's version from the claimed 0.18.0 to 0.19.0
because master already owns 0.18.x.
Conflicts resolved:
- VERSION: 0.19.0 (was 0.18.0 on HEAD vs 0.18.1 on master)
- package.json: 0.19.0, kept all 11 eval-facing exports, merged master's
typescript devDep + postinstall script + test script (typecheck added)
- src/core/types.ts: union of both PageType additions. Master had added
`meeting | note`; this branch added `email | slack | calendar-event`
for inbox/chat/calendar ingest. Final enum carries all five.
- CHANGELOG.md: renumbered the BrainBench-extraction entry to 0.19.0 and
placed it above master's 0.18.1 RLS entry. Tweaked copy ("In v0.17 it
lived inside this repo" → "Previously it lived inside this repo") to
stop implying a specific version that never shipped.
- CLAUDE.md: adjusted "BrainBench in a sibling repo" heading from
(v0.18+) → (v0.19+).
- docs/benchmarks/2026-04-18-minions-vs-openclaw-production.md:
resolved modify-vs-delete conflict in favor of delete (the extraction).
- scripts/llms-config.ts: dropped the docs/benchmarks/ entry (directory
no longer exists here; lives in gbrain-evals).
- llms.txt / llms-full.txt: regenerated after the config change.
- bun.lock: accepted master's (master already dropped pdf-parse as a
drive-by; aligned with our removal).
Tests: 2094 pass, 236 skip, 18 fail. Spot-checked failures — build-llms,
dream, orphans tests all pass in isolation. Failures reproduce only under
full-suite parallel load and are pre-existing master flakiness (matches the
graph-quality flake noted in the earlier summary). Not merge-introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
256 lines
9.9 KiB
TypeScript
256 lines
9.9 KiB
TypeScript
import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
|
|
// Mock the embedding module BEFORE importing runEmbed, so runEmbed picks up
|
|
// the mocked embedBatch. We track max concurrent invocations via a counter
|
|
// that increments on entry and decrements when the mock resolves.
|
|
let activeEmbedCalls = 0;
|
|
let maxConcurrentEmbedCalls = 0;
|
|
let totalEmbedCalls = 0;
|
|
|
|
mock.module('../src/core/embedding.ts', () => ({
|
|
embedBatch: async (texts: string[]) => {
|
|
activeEmbedCalls++;
|
|
totalEmbedCalls++;
|
|
if (activeEmbedCalls > maxConcurrentEmbedCalls) {
|
|
maxConcurrentEmbedCalls = activeEmbedCalls;
|
|
}
|
|
// Simulate API latency so concurrent workers actually overlap.
|
|
await new Promise(r => setTimeout(r, 30));
|
|
activeEmbedCalls--;
|
|
return texts.map(() => new Float32Array(1536));
|
|
},
|
|
}));
|
|
|
|
// Import AFTER mocking.
|
|
const { runEmbed } = await import('../src/commands/embed.ts');
|
|
|
|
// Proxy-based mock engine that matches test/import-file.test.ts pattern.
|
|
function mockEngine(overrides: Partial<Record<string, any>> = {}): BrainEngine {
|
|
const calls: { method: string; args: any[] }[] = [];
|
|
const track = (method: string) => (...args: any[]) => {
|
|
calls.push({ method, args });
|
|
if (overrides[method]) return overrides[method](...args);
|
|
return Promise.resolve(null);
|
|
};
|
|
const engine = new Proxy({} as any, {
|
|
get(_, prop: string) {
|
|
if (prop === '_calls') return calls;
|
|
if (overrides[prop]) return overrides[prop];
|
|
return track(prop);
|
|
},
|
|
});
|
|
return engine;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
activeEmbedCalls = 0;
|
|
maxConcurrentEmbedCalls = 0;
|
|
totalEmbedCalls = 0;
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.GBRAIN_EMBED_CONCURRENCY;
|
|
});
|
|
|
|
describe('runEmbed --all (parallel)', () => {
|
|
test('runs embedBatch calls concurrently across pages', async () => {
|
|
const NUM_PAGES = 20;
|
|
const pages = Array.from({ length: NUM_PAGES }, (_, i) => ({ slug: `page-${i}` }));
|
|
// Each page has one chunk without an embedding (stale).
|
|
const chunksBySlug = new Map(
|
|
pages.map(p => [
|
|
p.slug,
|
|
[{ chunk_index: 0, chunk_text: `text for ${p.slug}`, chunk_source: 'compiled_truth', embedded_at: null, token_count: 4 }],
|
|
]),
|
|
);
|
|
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
process.env.GBRAIN_EMBED_CONCURRENCY = '10';
|
|
|
|
await runEmbed(engine, ['--all']);
|
|
|
|
expect(totalEmbedCalls).toBe(NUM_PAGES);
|
|
// Concurrency actually happened.
|
|
expect(maxConcurrentEmbedCalls).toBeGreaterThan(1);
|
|
// And stayed within the configured limit.
|
|
expect(maxConcurrentEmbedCalls).toBeLessThanOrEqual(10);
|
|
});
|
|
|
|
test('respects GBRAIN_EMBED_CONCURRENCY=1 (serial)', async () => {
|
|
const pages = Array.from({ length: 5 }, (_, i) => ({ slug: `page-${i}` }));
|
|
const chunksBySlug = new Map(
|
|
pages.map(p => [
|
|
p.slug,
|
|
[{ chunk_index: 0, chunk_text: `text ${p.slug}`, chunk_source: 'compiled_truth', embedded_at: null, token_count: 4 }],
|
|
]),
|
|
);
|
|
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
process.env.GBRAIN_EMBED_CONCURRENCY = '1';
|
|
|
|
await runEmbed(engine, ['--all']);
|
|
|
|
expect(totalEmbedCalls).toBe(5);
|
|
expect(maxConcurrentEmbedCalls).toBe(1);
|
|
});
|
|
|
|
test('skips pages whose chunks are all already embedded when --stale', async () => {
|
|
const pages = [{ slug: 'fresh' }, { slug: 'stale' }];
|
|
const chunksBySlug = new Map<string, any[]>([
|
|
['fresh', [{ chunk_index: 0, chunk_text: 'hi', chunk_source: 'compiled_truth', embedded_at: '2026-01-01', token_count: 1 }]],
|
|
['stale', [{ chunk_index: 0, chunk_text: 'hi', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 }]],
|
|
]);
|
|
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
process.env.GBRAIN_EMBED_CONCURRENCY = '5';
|
|
|
|
await runEmbed(engine, ['--stale']);
|
|
|
|
// Only the stale page triggers an embedBatch call.
|
|
expect(totalEmbedCalls).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────
|
|
// runEmbedCore dry-run mode (v0.17 regression guard)
|
|
// ────────────────────────────────────────────────────────────────
|
|
|
|
describe('runEmbedCore --dry-run never calls the embedding model', () => {
|
|
test('dry-run --all with stale chunks: no embedBatch calls, accurate would_embed', async () => {
|
|
const { runEmbedCore } = await import('../src/commands/embed.ts');
|
|
const pages = Array.from({ length: 3 }, (_, i) => ({ slug: `page-${i}` }));
|
|
// All 3 pages have 2 stale chunks each (none embedded).
|
|
const chunksBySlug = new Map<string, any[]>(
|
|
pages.map(p => [
|
|
p.slug,
|
|
[
|
|
{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'b', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
],
|
|
]),
|
|
);
|
|
|
|
const upserts: string[] = [];
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async (slug: string) => { upserts.push(slug); },
|
|
});
|
|
|
|
const result = await runEmbedCore(engine, { stale: true, dryRun: true });
|
|
|
|
// No OpenAI calls.
|
|
expect(totalEmbedCalls).toBe(0);
|
|
// No DB writes.
|
|
expect(upserts).toEqual([]);
|
|
// Accurate counts.
|
|
expect(result.dryRun).toBe(true);
|
|
expect(result.embedded).toBe(0);
|
|
expect(result.would_embed).toBe(6); // 3 pages * 2 chunks each
|
|
expect(result.skipped).toBe(0);
|
|
expect(result.total_chunks).toBe(6);
|
|
expect(result.pages_processed).toBe(3);
|
|
});
|
|
|
|
test('dry-run --stale correctly separates stale from already-embedded', async () => {
|
|
const { runEmbedCore } = await import('../src/commands/embed.ts');
|
|
const pages = [{ slug: 'fresh' }, { slug: 'partial' }, { slug: 'all-stale' }];
|
|
const chunksBySlug = new Map<string, any[]>([
|
|
['fresh', [
|
|
{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: '2026-01-01', token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'b', chunk_source: 'compiled_truth', embedded_at: '2026-01-01', token_count: 1 },
|
|
]],
|
|
['partial', [
|
|
{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: '2026-01-01', token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'b', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
]],
|
|
['all-stale', [
|
|
{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'b', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
]],
|
|
]);
|
|
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
const result = await runEmbedCore(engine, { stale: true, dryRun: true });
|
|
|
|
expect(totalEmbedCalls).toBe(0);
|
|
expect(result.dryRun).toBe(true);
|
|
expect(result.would_embed).toBe(3); // 1 from 'partial' + 2 from 'all-stale'
|
|
expect(result.skipped).toBe(3); // 2 from 'fresh' + 1 from 'partial'
|
|
expect(result.total_chunks).toBe(6);
|
|
expect(result.pages_processed).toBe(3);
|
|
});
|
|
|
|
test('dry-run --slugs on a single page counts stale chunks, no API calls', async () => {
|
|
const { runEmbedCore } = await import('../src/commands/embed.ts');
|
|
const chunks = [
|
|
{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'b', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
{ chunk_index: 2, chunk_text: 'c', chunk_source: 'compiled_truth', embedded_at: '2026-01-01', token_count: 1 },
|
|
];
|
|
|
|
const engine = mockEngine({
|
|
getPage: async () => ({ slug: 'my-page', compiled_truth: 'text', timeline: '' }),
|
|
getChunks: async () => chunks,
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
const result = await runEmbedCore(engine, { slugs: ['my-page'], dryRun: true });
|
|
|
|
expect(totalEmbedCalls).toBe(0);
|
|
expect(result.dryRun).toBe(true);
|
|
expect(result.would_embed).toBe(2);
|
|
expect(result.skipped).toBe(1);
|
|
expect(result.total_chunks).toBe(3);
|
|
expect(result.pages_processed).toBe(1);
|
|
});
|
|
|
|
test('non-dry-run path reports accurate embedded count (regression guard)', async () => {
|
|
const { runEmbedCore } = await import('../src/commands/embed.ts');
|
|
const pages = [{ slug: 'a' }, { slug: 'b' }];
|
|
const chunksBySlug = new Map<string, any[]>([
|
|
['a', [{ chunk_index: 0, chunk_text: 'a', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 }]],
|
|
['b', [
|
|
{ chunk_index: 0, chunk_text: 'x', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
{ chunk_index: 1, chunk_text: 'y', chunk_source: 'compiled_truth', embedded_at: null, token_count: 1 },
|
|
]],
|
|
]);
|
|
|
|
const engine = mockEngine({
|
|
listPages: async () => pages,
|
|
getChunks: async (slug: string) => chunksBySlug.get(slug) || [],
|
|
upsertChunks: async () => {},
|
|
});
|
|
|
|
process.env.GBRAIN_EMBED_CONCURRENCY = '2';
|
|
|
|
const result = await runEmbedCore(engine, { stale: true });
|
|
|
|
expect(result.dryRun).toBe(false);
|
|
expect(result.embedded).toBe(3); // 1 from a + 2 from b
|
|
expect(result.would_embed).toBe(0);
|
|
expect(result.pages_processed).toBe(2);
|
|
});
|
|
});
|