* fix(initSchema): narrow pre-schema bootstrap + v24 PGLite no-op Closes a 2-year-old wedge cycle that hit users 10+ times across 6 schema versions (#239, #243, #266, #357, #366, #374, #375, #378, #395, #396). Bug class: gbrain ships an embedded schema blob (PGLITE_SCHEMA_SQL + SCHEMA_SQL) that runs before numbered migrations on every initSchema(). The blob references columns that newer migrations introduce. On any brain older than the migration that adds those columns, the blob crashes before the migration can run. Fix: PGLiteEngine.initSchema() and PostgresEngine.initSchema() now call a new private applyForwardReferenceBootstrap() before the schema blob. The bootstrap probes for missing forward-referenced state and adds only what's needed (sources table + pages.source_id, links.link_source + links.origin_page_id, content_chunks.symbol_name + content_chunks.language). Fresh installs and modern brains both no-op. A CI guard test/schema-bootstrap-coverage.test.ts enforces that the bootstrap covers every forward reference in PGLITE_SCHEMA_SQL. Future migrations that add column-with-index in the schema blob must extend the bootstrap; the test fails loudly otherwise. Migration v24 (rls_backfill_missing_tables) now no-ops on PGLite via sqlFor.pglite: '' since PGLite has no RLS engine and is single-tenant. Closes #395. The plan went through CEO + Eng + Codex review. Codex caught a critical bug in the original "run all migrations early" approach: it would crash on v24 trying to ALTER subagent tables that the schema blob hadn't created yet. The narrow bootstrap shape resolves that. Wave incorporates community PRs #398 (@vinsew), #399 (@jdcastro2), #402 (@schnubb-web). Co-Authored-By: vinsew <yiyangchaishu@gmail.com> Co-Authored-By: Julián David Castro <juliancastro@Mac-mini-de-Julian.local> Co-Authored-By: schnubb-web <info@mia-mai.de> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(test): bump beforeAll timeout on minions-shell-pglite for parallel-load flake Default 5s beforeAll timeout occasionally trips under the parallel test runner when many test files initialize PGLite concurrently. The same pattern is documented as a P0 TODO for v0.21 Code Cathedral tests; this is the one instance the upgrade-hardening wave directly exposed (CPU pressure from new bootstrap test files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.21.1) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: update project documentation for v0.21.1 - CLAUDE.md: PGLite + Postgres engine entries note new applyForwardReferenceBootstrap() in initSchema(), v24 sqlFor.pglite no-op, and the new bootstrap test files (test/bootstrap.test.ts, test/schema-bootstrap-coverage.test.ts, test/e2e/postgres-bootstrap.test.ts). - CHANGELOG.md: voice polish on the v0.21.1 headline (drop stray ## prefixes so the bold two-line headline renders as bold prose, not h2 sub-headers that break the version-entry hierarchy). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: correct version slot from v0.22.5 to v0.21.6 Slot allocation correction. v0.21.6 is the actual landing slot for this wave on the v0.21.x patch line. VERSION, package.json, CHANGELOG.md (header + table + take-advantage section), CLAUDE.md (engine entries, migrate.ts entry, test descriptions) all updated together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: correct version slot to v0.22.7 VERSION, package.json, CHANGELOG.md (header + table + take-advantage section), CLAUDE.md (engine entries, migrate.ts entry, test descriptions) all updated together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate llms.txt + llms-full.txt for v0.22.7 CLAUDE.md changed (engine entries describe the bootstrap, migrate.ts entry describes the v24 PGLite no-op). The build:llms regen-drift guard caught the staleness in CI. Running `bun run build:llms` propagates the same content into the AI-consumable bundles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: change version slot from v0.22.7 to v0.22.6-hotfix.1 PR #483 (fix/mcp-registration-auth) claimed v0.22.7. Moved this wave to v0.22.6-hotfix.1 to avoid the collision. Note: semver-orders BEFORE 0.22.6 (pre-release suffix), so the hotfix tag is informational, not ordering-correct. Acceptable here because the wave's content predates master's 0.22.6 and is being landed as a parallel hotfix slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: change version slot to v0.22.6.1 4-digit hotfix slot under master's v0.22.6. bun + bun:test accept the format; the build-llms regen-drift guard and bootstrap tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: vinsew <yiyangchaishu@gmail.com> Co-authored-by: Julián David Castro <juliancastro@Mac-mini-de-Julian.local> Co-authored-by: schnubb-web <info@mia-mai.de> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
4.7 KiB
TypeScript
117 lines
4.7 KiB
TypeScript
/**
|
|
* E2E Minions Shell Handler — PGLite / --follow inline execution path
|
|
*
|
|
* Closes the T4 gap surfaced during PR #381 eng review. The sibling file
|
|
* test/e2e/minions-shell.test.ts covers the Postgres + persistent-worker-daemon
|
|
* path. This file covers the PGLite path documented in the minion-orchestrator
|
|
* skill: `gbrain jobs submit shell ... --follow` runs inline because
|
|
* `gbrain jobs work` (daemon) is not available on PGLite (exclusive file lock).
|
|
*
|
|
* Mirrors the Postgres test's structure but runs in-memory against PGLiteEngine.
|
|
* No DATABASE_URL required, no Docker — runs in CI unconditionally.
|
|
*
|
|
* Run: bun test test/e2e/minions-shell-pglite.test.ts
|
|
*/
|
|
|
|
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 { MinionWorker } from '../../src/core/minions/worker.ts';
|
|
import { registerBuiltinHandlers } from '../../src/commands/jobs.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
let originalAllowShellJobs: string | undefined;
|
|
|
|
async function waitTerminal(queue: MinionQueue, id: number, timeoutMs = 15000): Promise<string> {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
const j = await queue.getJob(id);
|
|
if (j && ['completed', 'failed', 'dead', 'cancelled'].includes(j.status)) return j.status;
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
}
|
|
const j = await queue.getJob(id);
|
|
throw new Error(`job ${id} did not reach terminal state in ${timeoutMs}ms; last status=${j?.status}`);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
// registerBuiltinHandlers gates shell handler on GBRAIN_ALLOW_SHELL_JOBS=1.
|
|
// Mirror the real --follow path by setting the env var; restore on cleanup
|
|
// so other tests see their original environment.
|
|
originalAllowShellJobs = process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
|
process.env.GBRAIN_ALLOW_SHELL_JOBS = '1';
|
|
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({}); // in-memory PGLite
|
|
await engine.initSchema(); // installs pages, minion_jobs, config, etc.
|
|
}, 30000);
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
if (originalAllowShellJobs === undefined) {
|
|
delete process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
|
} else {
|
|
process.env.GBRAIN_ALLOW_SHELL_JOBS = originalAllowShellJobs;
|
|
}
|
|
});
|
|
|
|
describe('E2E: Minions shell handler on PGLite (--follow inline path)', () => {
|
|
// Mirror the Postgres sibling's per-test reset. The engine is shared across
|
|
// both tests via beforeAll; without this, completed jobs from one test leak
|
|
// into minion_jobs and future test additions hit order-dependency.
|
|
beforeEach(async () => {
|
|
const db = (engine as any).db;
|
|
await db.exec(`DELETE FROM minion_attachments; DELETE FROM minion_inbox; DELETE FROM minion_jobs;`);
|
|
});
|
|
|
|
test('submit → worker registered via registerBuiltinHandlers → shell runs → completes', async () => {
|
|
const queue = new MinionQueue(engine);
|
|
const job = await queue.add(
|
|
'shell',
|
|
{ cmd: 'echo hello', cwd: '/tmp' },
|
|
{},
|
|
{ allowProtectedSubmit: true },
|
|
);
|
|
expect(job.name).toBe('shell');
|
|
expect(job.status).toBe('waiting');
|
|
|
|
// This is the exact dispatch path --follow takes (src/commands/jobs.ts:207).
|
|
// Gates shell on GBRAIN_ALLOW_SHELL_JOBS=1 (set in beforeAll above).
|
|
const worker = new MinionWorker(engine, { pollInterval: 100, lockDuration: 30000 });
|
|
await registerBuiltinHandlers(worker, engine);
|
|
expect(worker.registeredNames).toContain('shell');
|
|
|
|
const runPromise = worker.start();
|
|
try {
|
|
const status = await waitTerminal(queue, job.id, 20000);
|
|
expect(status).toBe('completed');
|
|
const final = await queue.getJob(job.id);
|
|
expect((final!.result as any).exit_code).toBe(0);
|
|
expect((final!.result as any).stdout_tail).toBe('hello\n');
|
|
} finally {
|
|
worker.stop();
|
|
await runPromise;
|
|
}
|
|
}, 30000);
|
|
|
|
test('GBRAIN_ALLOW_SHELL_JOBS unset → shellHandler rejects at execution time', async () => {
|
|
// v0.20.3+: shell handler is always registered (so claimed jobs emit a clear
|
|
// rejection log), but the runtime env guard lives inside the handler itself.
|
|
// Prove the guard rejects when the env var is unset.
|
|
const { shellHandler } = await import('../../src/core/minions/handlers/shell.ts');
|
|
const saved = process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
|
delete process.env.GBRAIN_ALLOW_SHELL_JOBS;
|
|
try {
|
|
const ctx: any = {
|
|
id: 1,
|
|
name: 'shell',
|
|
data: { cmd: 'echo hi', cwd: '/tmp' },
|
|
attempt: 1,
|
|
engine,
|
|
};
|
|
await expect(shellHandler(ctx)).rejects.toThrow(/GBRAIN_ALLOW_SHELL_JOBS=1/);
|
|
} finally {
|
|
process.env.GBRAIN_ALLOW_SHELL_JOBS = saved;
|
|
}
|
|
});
|
|
});
|