Files
gbrain/test/wait-for-completion.test.ts
T
Garry TanandClaude Opus 4.7 27762abb7a Merge origin/master into garrytan/gbrain-evals
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>
2026-04-23 10:45:02 -07:00

113 lines
3.9 KiB
TypeScript

/**
* waitForCompletion tests. Uses PGLite in-memory so the poll path exercises
* a real getJob over a real engine.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { MinionQueue } from '../src/core/minions/queue.ts';
import { waitForCompletion, TimeoutError, __testing } from '../src/core/minions/wait-for-completion.ts';
let engine: PGLiteEngine;
let queue: MinionQueue;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({ database_url: '' });
await engine.initSchema();
queue = new MinionQueue(engine);
}, 60_000);
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await engine.executeRaw('DELETE FROM minion_jobs');
});
describe('waitForCompletion terminal states', () => {
test('TERMINAL_STATES covers every terminal MinionJobStatus value', () => {
expect(__testing.TERMINAL_STATES).toEqual(['completed', 'failed', 'dead', 'cancelled']);
});
test('returns immediately when job already completed (fast path)', async () => {
const j = await queue.add('t', {});
const claimed = await queue.claim('tok', 30000, 'default', ['t']);
await queue.completeJob(claimed!.id, 'tok', { ok: true });
const t0 = Date.now();
const res = await waitForCompletion(queue, j.id, { pollMs: 500 });
expect(res.status).toBe('completed');
expect(Date.now() - t0).toBeLessThan(300); // no full poll cycle
});
test('returns when job transitions to failed mid-wait', async () => {
const j = await queue.add('t', {});
const p = waitForCompletion(queue, j.id, { pollMs: 25, timeoutMs: 5000 });
// Transition the job to failed after a brief delay.
setTimeout(async () => {
const claimed = await queue.claim('tok', 30000, 'default', ['t']);
await queue.failJob(claimed!.id, 'tok', 'boom', 'failed');
}, 60);
const res = await p;
expect(res.status).toBe('failed');
});
test('returns when job transitions to cancelled', async () => {
const j = await queue.add('t', {});
const p = waitForCompletion(queue, j.id, { pollMs: 25, timeoutMs: 5000 });
setTimeout(() => { queue.cancelJob(j.id); }, 60);
const res = await p;
expect(res.status).toBe('cancelled');
});
test('throws TimeoutError when job stays non-terminal past timeoutMs', async () => {
const j = await queue.add('t', {});
await expect(
waitForCompletion(queue, j.id, { pollMs: 25, timeoutMs: 100 })
).rejects.toBeInstanceOf(TimeoutError);
});
test('TimeoutError carries the jobId and elapsedMs', async () => {
const j = await queue.add('t', {});
try {
await waitForCompletion(queue, j.id, { pollMs: 25, timeoutMs: 80 });
throw new Error('should have thrown');
} catch (e) {
expect(e).toBeInstanceOf(TimeoutError);
const te = e as TimeoutError;
expect(te.jobId).toBe(j.id);
expect(te.elapsedMs).toBeGreaterThanOrEqual(80);
}
});
test('TimeoutError does NOT cancel the job', async () => {
const j = await queue.add('t', {});
try {
await waitForCompletion(queue, j.id, { pollMs: 25, timeoutMs: 80 });
} catch {}
const still = await queue.getJob(j.id);
expect(still?.status).toBe('waiting');
});
test('AbortSignal exits loop early without throwing', async () => {
const j = await queue.add('t', {});
const ac = new AbortController();
setTimeout(() => ac.abort(), 50);
const res = await waitForCompletion(queue, j.id, {
pollMs: 25,
timeoutMs: 5000,
signal: ac.signal,
});
expect(res.id).toBe(j.id);
// Still waiting — we just stopped polling.
expect(res.status).toBe('waiting');
});
test('throws when job id does not exist', async () => {
await expect(waitForCompletion(queue, 99_999, { pollMs: 10, timeoutMs: 100 }))
.rejects.toThrow(/not found/);
});
});