mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
Closes the paths #3382 left open (its author said it narrowed the issue rather than closing it): 1. checkCycleFreshness iterates EVERY local_path source, so an install that nightly-dreams one vault via --dir showed a permanent FAIL for every other federated source — and for any source added minutes ago. 'Never completed a full cycle' is now a WARN with the dream/autopilot hint; a source that HAS cycled and then went stale still escalates through the 6h warn / 24h fail thresholds (the regression signal the check exists for). This is the reporter's actual case: the permanent red eroded doctor's signal until real staleness hid inside it. 2. resolveSourceForDir's exact-match lookup had no archived filter and no ORDER BY, so an archived (or duplicate) alias of the same path could shadow the active source; dream's archived guard then refused the stamp and the ACTIVE source stayed unstamped forever. The lookup now excludes archived rows and orders deterministically, matching the canonical-path fallback's posture. The fallback's fail-closed ambiguity handling is deliberately unchanged. 3. #3382's own regression test (ii) was environment-sensitive: it assumed unsetting OPENAI_API_KEY/ANTHROPIC_API_KEY makes the embed phase fail, which is false wherever another embedding provider resolves (the cycle then reports 'clean' and the test flips). It now fails the sync phase against a vanished checkout — deterministic on every machine, same property pinned (a genuinely failing enabled phase must prevent the stamp). New pins fail on unmodified master and pass here: never-cycled→warn (x2, doctor) and the archived-alias shadow (dream --dir stamp). Fixes #2540 Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
151 lines
6.9 KiB
TypeScript
151 lines
6.9 KiB
TypeScript
/**
|
|
* v0.38 — doctor checkCycleFreshness unit test.
|
|
*
|
|
* Mirrors checkSyncFreshness shape: returns Check with status mapping to
|
|
* per-source last_full_cycle_at from sources.config JSONB. Reads what
|
|
* autopilot's per-source dispatch gate writes.
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { checkCycleFreshness } from '../src/commands/doctor.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({ database_url: '' });
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
});
|
|
|
|
const NOW = Date.parse('2026-05-22T12:00:00.000Z');
|
|
const agoH = (h: number) => new Date(NOW - h * 3600_000).toISOString();
|
|
|
|
async function seed(id: string, lastFullCycleAt?: string, opts: { local_path?: string | null } = {}): Promise<void> {
|
|
const config = lastFullCycleAt
|
|
? JSON.stringify({ last_full_cycle_at: lastFullCycleAt })
|
|
: '{}';
|
|
const localPath = opts.local_path === undefined ? `/tmp/${id}` : opts.local_path;
|
|
await engine.executeRaw(
|
|
`INSERT INTO sources (id, name, local_path, config, archived, created_at)
|
|
VALUES ($1, $2, $3, $4::jsonb, false, NOW())
|
|
ON CONFLICT (id) DO UPDATE SET local_path = EXCLUDED.local_path, config = EXCLUDED.config`,
|
|
[id, id, localPath, config],
|
|
);
|
|
}
|
|
|
|
describe('doctor checkCycleFreshness', () => {
|
|
test('empty (no federated sources) returns ok', async () => {
|
|
// resetPgliteState reseeds the default source with no local_path
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('ok');
|
|
expect(result.message).toMatch(/No federated sources/);
|
|
});
|
|
|
|
test('source with last_full_cycle_at 2h ago returns ok (under 6h warn)', async () => {
|
|
await seed('fresh', agoH(2));
|
|
// default source also has no last_full_cycle_at — so we'd get a fail
|
|
// unless default lacks local_path. resetPgliteState seeds default with
|
|
// no local_path, so it's filtered. Confirm.
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('ok');
|
|
});
|
|
|
|
test('source with last_full_cycle_at 10h ago returns warn (>6h, <24h)', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('warned', agoH(10));
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toMatch(/warned/);
|
|
expect(result.message).toMatch(/10h ago/);
|
|
});
|
|
|
|
test('source with last_full_cycle_at 48h ago returns fail (>24h)', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('stale', agoH(48));
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('fail');
|
|
expect(result.message).toMatch(/stale/);
|
|
expect(result.message).toMatch(/gbrain dream --source/);
|
|
});
|
|
|
|
test('source with NO last_full_cycle_at (never cycled) returns warn, not fail (#2540)', async () => {
|
|
// #2540: never-cycled used to FAIL, which turned doctor permanently red
|
|
// on any install that doesn't cycle every local_path source (e.g. one
|
|
// nightly `dream --dir <vault>` plus other federated sources) — and on
|
|
// any source added minutes ago. It surfaces as a warning; only a source
|
|
// that HAS cycled and then went stale escalates to fail.
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('virgin');
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toMatch(/never completed a full cycle/);
|
|
expect(result.message).toMatch(/gbrain dream --source/);
|
|
});
|
|
|
|
test('reporter case (#2540): one cycled vault + never-cycled siblings is warn, not permanent fail', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('nightly-vault', agoH(2)); // the one vault dreamt via --dir
|
|
await seed('federated-a'); // never cycled
|
|
await seed('federated-b'); // never cycled
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toMatch(/federated-a/);
|
|
expect(result.message).toMatch(/federated-b/);
|
|
expect(result.message).not.toMatch(/nightly-vault/);
|
|
});
|
|
|
|
test('a previously-cycled source gone stale still fails even next to never-cycled sources', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('stale', agoH(72)); // real regression signal
|
|
await seed('virgin'); // never cycled — warn-only
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('fail');
|
|
});
|
|
|
|
test('mixed sources: highest severity wins (fail > warn > ok)', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('fresh', agoH(1)); // ok
|
|
await seed('warned', agoH(12)); // warn
|
|
await seed('stale', agoH(72)); // fail
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('fail');
|
|
});
|
|
|
|
test('future last_full_cycle_at returns warn (clock skew)', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
const future = new Date(NOW + 3600_000).toISOString();
|
|
await seed('clock-skewed', future);
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toMatch(/future last_full_cycle_at/);
|
|
});
|
|
|
|
test('unparseable last_full_cycle_at returns warn', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('garbled', 'not-an-iso-date');
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toMatch(/unparseable/);
|
|
});
|
|
|
|
test('local_path NULL sources are filtered (codex P1-4 parity)', async () => {
|
|
await engine.executeRaw(`UPDATE sources SET local_path = NULL WHERE id = 'default'`);
|
|
await seed('db-only', undefined, { local_path: null });
|
|
// No federated sources to check; default is unsynced but filtered.
|
|
const result = await checkCycleFreshness(engine, { nowMs: NOW });
|
|
expect(result.status).toBe('ok');
|
|
expect(result.message).toMatch(/No federated sources/);
|
|
});
|
|
});
|