Files
gbrain/test/extract-workers.test.ts
T
8ab733471b v0.41.17.0 feat: --workers N on every bulk command + facts dim doctor parity (#1519)
* 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>
2026-05-26 18:29:03 -07:00

96 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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]*?\}\)/,
);
});
});