Files
gbrain/test/embed-helper-migration.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

109 lines
5.2 KiB
TypeScript

/**
* Structural regression test for the embed.ts → worker-pool migration
* (v0.41.15.0, T3, REGRESSION per IRON RULE).
*
* What this test asserts:
* 1. embed.ts imports `runSlidingPool` from `../core/worker-pool.ts`.
* 2. Both pre-migration sliding-pool sites are GONE:
* - the `let nextIdx = 0; async function worker() {}` shape
* - the `Promise.all(Array.from({ length: numWorkers }, () => worker()))`
* shape (which paired with the above).
* 3. Both migration sites call `runSlidingPool({ items: ..., workers: CONCURRENCY, ... })`.
* 4. The legacy `CONCURRENCY = parseInt(process.env.GBRAIN_EMBED_CONCURRENCY...)`
* default is preserved (codex finding #13 — embed's pre-existing default
* pre-dates autoConcurrency; the migration must NOT route it through
* resolveWorkersWithClamp). This is the load-bearing back-compat for
* every existing brain that relies on the 20-worker embed sweep.
*
* Why structural rather than end-to-end:
* Per codex finding #16/#17, embed.ts byte-equality via stubbed transport
* was overclaimed in the original plan — it can't prove provider retry
* behavior, SDK usage accounting, or progress event ordering preservation.
* The pre-migration sliding pool didn't have any of those guarantees either;
* it just had a small inline `while (nextIdx < pages.length)` loop. The
* honest contract is "the same pages still get embedded by the same workers
* reading from the same queue, just through a helper now." That's a
* structural property — easiest to pin by source grep.
*
* The helper's contracts (atomic claim, abort propagation, failures[]
* shape, BudgetExhausted bypass) are exhaustively tested in
* test/worker-pool.test.ts; embed inherits those by import.
*/
import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'fs';
import { resolve } from 'path';
const REPO_ROOT = resolve(import.meta.dir, '..');
const EMBED_PATH = resolve(REPO_ROOT, 'src/commands/embed.ts');
const EMBED_SOURCE = readFileSync(EMBED_PATH, 'utf-8');
describe('embed.ts → worker-pool migration (T3)', () => {
test('imports runSlidingPool from worker-pool helper', () => {
expect(EMBED_SOURCE).toMatch(
/import\s*\{\s*runSlidingPool\s*\}\s*from\s*['"]\.\.\/core\/worker-pool\.ts['"]/,
);
});
test('calls runSlidingPool at least twice (embedAll + embedAllStale paths)', () => {
const matches = EMBED_SOURCE.match(/runSlidingPool\(/g) ?? [];
expect(matches.length).toBeGreaterThanOrEqual(2);
});
test('pre-migration `let nextIdx = 0; async function worker()` shape is gone', () => {
// The exact shape the migration replaced. Either token alone could
// legitimately appear in a comment or string literal; both together
// on adjacent lines indicates a regression to the inline pool.
const inlinePool =
/let\s+nextIdx\s*=\s*0\s*;\s*\n\s*async\s+function\s+worker\s*\(/;
expect(EMBED_SOURCE).not.toMatch(inlinePool);
});
test('pre-migration `Promise.all(Array.from({ length: numWorkers }, () => worker()))` is gone', () => {
// Migration-specific shape from the original inline pool. The
// generic `Promise.all(...)` pattern is still allowed elsewhere
// (gateway, etc.); only the worker-pool-fanout shape is banned.
const fanout =
/Promise\.all\(Array\.from\(\{\s*length:\s*numWorkers\s*\}/;
expect(EMBED_SOURCE).not.toMatch(fanout);
});
test('preserves GBRAIN_EMBED_CONCURRENCY default of 20 (codex #13)', () => {
// The pre-migration default must survive: env override or 20.
// Routing through resolveWorkersWithClamp would change this behavior
// (autoConcurrency returns 1 for small file counts even on Postgres),
// breaking every existing brain that relies on the 20-worker default.
expect(EMBED_SOURCE).toMatch(
/parseInt\(process\.env\.GBRAIN_EMBED_CONCURRENCY\s*\|\|\s*['"]20['"]/,
);
});
test('runSlidingPool call sites pass `workers: CONCURRENCY`', () => {
// The migrated calls must thread the pre-existing CONCURRENCY value
// through, not invent a new default. Catches the regression where
// a future contributor swaps `workers: CONCURRENCY` for a literal.
// Allow optional commas/whitespace — match both call sites' shape.
const callSites = EMBED_SOURCE.match(
/runSlidingPool\(\s*\{[\s\S]*?workers:\s*CONCURRENCY/g,
);
expect(callSites?.length ?? 0).toBeGreaterThanOrEqual(2);
});
test('embedAllStale path still threads budgetSignal into pool', () => {
// The pre-migration code checked `!budgetSignal.aborted` in the
// worker loop. The migration moves that check into the helper via
// the `signal` option. If a future refactor drops the signal, the
// wall-clock budget cancellation regresses.
expect(EMBED_SOURCE).toMatch(
/runSlidingPool\(\s*\{[\s\S]*?signal:\s*budgetSignal[\s\S]*?\}\)/,
);
});
test('failureLabel projector uses page.slug (memory bound on large brains)', () => {
// Per codex #10 + D7, failures[] must not store full Page objects.
// We can't test the runtime behavior without a full mock engine, but
// we CAN assert the call sites pass the projector explicitly.
expect(EMBED_SOURCE).toMatch(/failureLabel:\s*\(page\)\s*=>\s*page\.slug/);
});
});