mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +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>
113 lines
3.9 KiB
TypeScript
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/);
|
|
});
|
|
});
|