mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 21:19:18 +00:00
* feat(worker-pool): shared sliding pool + bounded semaphore + PGLite-clamp wrapper T1 + T2 of the v0.41.16.0 workers cathedral. New src/core/worker-pool.ts is the canonical primitive every --workers N bulk command in this wave (and future bulk commands) builds on. Atomic-claim invariant enforced by scripts/check-worker-pool-atomicity.sh (wired into bun run verify). BudgetExhausted bypass + AbortSignal composition baked into the helper so budget caps are a structural ceiling under concurrency, not a per-caller convention. The new resolveWorkersWithClamp wrapper composes existing autoConcurrency with PGLite-clamp + per-(command, requested) stderr dedup. Deliberately NOT a modification to shared autoConcurrency (silent today, used by sync + import); embed.ts keeps GBRAIN_EMBED_CONCURRENCY || 20 default per codex #13. 23 + 12 + 9 = 44 hermetic tests pin every contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: structural + dim-check regression suites for v0.41.16.0 wave - test/embed-helper-migration.test.ts (T3): asserts embed.ts's two sliding-pool sites are migrated to runSlidingPool, pre-migration shapes (let nextIdx = 0, Promise.all(Array.from(...))) are gone, GBRAIN_EMBED_CONCURRENCY || 20 default preserved, failureLabel threads page.slug. Per codex #16/#17 these are invariant assertions, not byte-equality on progress event ORDERING. - test/embedding-dim-check-facts.test.ts (T6): readFactsEmbeddingDim covers vector(N) + halfvec(N), halfvec-before-vector regex ordering pinned (codex #19), buildFactsAlterRecipe emits DROP INDEX + ALTER USING + CREATE INDEX (codex #18, not bare REINDEX), FactsEmbeddingDimMismatchError tagged class shape, assertFactsEmbeddingDimMatchesConfig PGLite skip + Postgres absent- column skip, doctor check + insert-cast wiring assertions. - test/extract-conversation-facts-workers.test.ts (T5): helper exports (extractConversationFactsLockId, PER_PAGE_LOCK_TTL_MINUTES), structural wiring (runSlidingPool, resolveWorkersWithClamp, withRefreshingLock, LockUnavailableError, delete-orphans-first before segment loop, preflight before pool, exit 3 when lock_skipped > 0), Minion handler round-trip. - test/extract-workers.test.ts (T7): --workers wiring on all 3 inner fs-walk loops (extractForSlugs, extractLinksFromDir, extractTimelineFromDir) + CLI parse + opts threading through runExtractCore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rebump v0.41.16.0 → v0.41.17.0 (queue collision with PR #1510) PR #1510 (garrytan/dynamic-regex-conversation-formats) claimed v0.41.16.0 on master in parallel. Advancing this wave to v0.41.17.0 so both can land cleanly. Pure mechanical version bump: - VERSION + package.json → 0.41.17.0 - CHANGELOG.md header + "To take advantage of v0.41.17.0" block - TODOS.md section header + v0.41.18+ forward references - CLAUDE.md inline version tags - Regenerated llms-full.txt / llms.txt No code changes. The actual workers cathedral feature set is unchanged from the two prior commits in this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): search-image-column probes column dim at runtime CI shard 5 failed on `searchVector column routing (v0.27.1)` with: error: expected 1280 dimensions, not 1536 The test had a hardcoded `fakeText1536` helper that seeded chunks at 1536-d vectors. Master's default embedding model switched from OpenAI text-embedding-3-large (1536) to ZeroEntropy zembed-1 (1280) so a fresh PGLite brain on CI now sizes content_chunks.embedding at 1280; the test's 1536-d INSERT trips pgvector's CheckExpectedDim. Fix: probe `content_chunks.embedding` width via `readContentChunksEmbeddingDim(engine)` in `beforeAll`, store in `TEXT_DIM`, and build `fakeTextDefault(seed)` at that width. The test now passes regardless of which default ships (the model has flipped twice and may flip again). Local dev (1536 from older config) and CI fresh-install (1280 from new default) both pass. Image-side vectors stay at 1024 (matches Voyage multimodal-3 + the column's fixed width on the image side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): bump PGLite hook timeout for shard-4 deep-process files facts-anti-loop.test.ts and ingest-capture.test.ts were timing out in CI shard 4 with "beforeEach/afterEach hook timed out" after the v0.41.16.0 master merge brought migration count to 99. When these files run deep in a shard process that has already created ~20 PGLite engines, the WASM cold-start + 95-migration replay legitimately exceeds bun's 5s default hook timeout (observed 5.6s and 7.3s locally when reproducing). Bun's --timeout=60000 from scripts/test-shard.sh covers TEST timeouts but NOT hook timeouts; those default to 5s and must be set per-hook via the optional 2nd arg to beforeAll/afterAll. Reproduced locally by running the first 21 shard-4 files via head -21 /tmp/shard4-list.txt | xargs bun test → 179 pass, 2 fail (both with hook-timeout error) After fix: → 198 pass, 0 fail (the 4 anti-loop + 15 ingest-capture tests recover) Full shard 4 with fix: 955 pass, 0 fail. Full shard 5 with fix: 1261 pass, 0 fail. Also added a defensive diagnostic to the two put_page tests: if facts_backstop is missing in the response payload, throw with the full payload + isError so future failures surface the actual handler error instead of a bare "expected {...} got undefined" assertion. No-op when the test passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
4.1 KiB
TypeScript
96 lines
4.1 KiB
TypeScript
/**
|
||
* Structural test for `gbrain extract --workers N` wiring (v0.41.15.0, T7).
|
||
*
|
||
* Per codex #16/#17 the high-value assertion for a CPU-bound migration
|
||
* is that the helper is wired in, not byte-equality. extract is CPU-
|
||
* bound (markdown parse + regex), so the speedup is moderate; the
|
||
* primary contract is "API surface exists + threads correctly +
|
||
* existing serial behavior preserved when --workers is omitted."
|
||
*/
|
||
|
||
import { describe, test, expect } from 'bun:test';
|
||
import { readFileSync } from 'fs';
|
||
import { resolve } from 'path';
|
||
|
||
const REPO_ROOT = resolve(import.meta.dir, '..');
|
||
const EXTRACT_SRC = readFileSync(
|
||
resolve(REPO_ROOT, 'src/commands/extract.ts'),
|
||
'utf-8',
|
||
);
|
||
|
||
describe('extract.ts → workers wiring (T7)', () => {
|
||
test('imports runSlidingPool from worker-pool helper', () => {
|
||
expect(EXTRACT_SRC).toMatch(
|
||
/import\s*\{\s*runSlidingPool\s*\}\s*from\s*['"]\.\.\/core\/worker-pool\.ts['"]/,
|
||
);
|
||
});
|
||
|
||
test('imports parseWorkers + resolveWorkersWithClamp', () => {
|
||
expect(EXTRACT_SRC).toMatch(
|
||
/parseWorkers,\s*resolveWorkersWithClamp/,
|
||
);
|
||
});
|
||
|
||
test('ExtractOpts type carries optional workers field', () => {
|
||
expect(EXTRACT_SRC).toMatch(/workers\?:\s*number/);
|
||
});
|
||
|
||
test('runExtractCore resolves workers via the PGLite-clamp wrapper', () => {
|
||
expect(EXTRACT_SRC).toMatch(/resolveWorkersWithClamp\(\s*engine,\s*opts\.workers/);
|
||
});
|
||
|
||
test('CLI runExtract parses --workers via parseWorkers (loud-fail on invalid)', () => {
|
||
// The parsed value must come from parseWorkers (validates >=1
|
||
// integer) AND must thread into runExtractCore opts.
|
||
expect(EXTRACT_SRC).toMatch(/parseWorkers\(args\[/);
|
||
expect(EXTRACT_SRC).toMatch(/workers,?\s*\}\);/);
|
||
});
|
||
|
||
test('all three inner loops accept the workers parameter', () => {
|
||
// extractForSlugs, extractLinksFromDir, extractTimelineFromDir all
|
||
// receive workers (default 1 for back-compat).
|
||
expect(EXTRACT_SRC).toMatch(/extractForSlugs[\s\S]*?workers:\s*number/);
|
||
expect(EXTRACT_SRC).toMatch(/extractLinksFromDir[\s\S]*?workers:\s*number/);
|
||
expect(EXTRACT_SRC).toMatch(/extractTimelineFromDir[\s\S]*?workers:\s*number/);
|
||
});
|
||
|
||
test('all three inner loops call runSlidingPool', () => {
|
||
// 3 inner loops × 1 runSlidingPool call each = 3 occurrences total
|
||
// (extract.ts has no other runSlidingPool callers).
|
||
const calls = EXTRACT_SRC.match(/runSlidingPool\(/g) ?? [];
|
||
expect(calls.length).toBe(3);
|
||
});
|
||
|
||
test('legacy `for (let i = 0; i < files.length; i++)` per-file loops are gone', () => {
|
||
// The pre-T7 serial loops would fight the worker-pool semantics.
|
||
// After migration only one such loop may remain (acceptable: the
|
||
// dir-walker itself which isn't per-file work). We assert <= 1.
|
||
const serialLoops = EXTRACT_SRC.match(/for\s*\(\s*let\s+i\s*=\s*0;\s*i\s*<\s*files\.length/g) ?? [];
|
||
expect(serialLoops.length).toBeLessThanOrEqual(1);
|
||
});
|
||
|
||
test('extractForSlugs (the CLI/cycle path) is migrated to the pool', () => {
|
||
// Two legacy `for (const slug of slugs)` loops survive in
|
||
// extractLinksForSlugs + extractTimelineForSlugs — the sync-integration
|
||
// hooks. Those are out of T7 scope (called from sync.ts post-sync, not
|
||
// from the user-facing `gbrain extract` CLI). T7 covers extractForSlugs
|
||
// + extractLinksFromDir + extractTimelineFromDir which carry --workers
|
||
// from the CLI surface.
|
||
const legacyCount = (EXTRACT_SRC.match(/for\s*\(\s*const\s+slug\s+of\s+slugs\)/g) ?? []).length;
|
||
expect(legacyCount).toBeLessThanOrEqual(2);
|
||
// The migrated extractForSlugs uses runSlidingPool over `slugs`, not
|
||
// a for-of loop. Confirm by checking that one of the 3 runSlidingPool
|
||
// call sites operates on `slugs`.
|
||
expect(EXTRACT_SRC).toMatch(/runSlidingPool\(\s*\{\s*items:\s*slugs/);
|
||
});
|
||
|
||
test('CLI threads workers into runExtractCore call', () => {
|
||
// The opts-object passed to runExtractCore must include the workers
|
||
// field; without this the parsed CLI flag would silently drop on
|
||
// the FS-source happy path.
|
||
expect(EXTRACT_SRC).toMatch(
|
||
/runExtractCore\(engine,\s*\{[\s\S]*?workers,[\s\S]*?\}\)/,
|
||
);
|
||
});
|
||
});
|