mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(doctor): doctor-categories foundation — BRAIN/SKILL/OPS/META sets + drift guard
Categorizes every doctor check name into exactly one of four categories. Exported
constants + categorizeCheck(name) helper are the single source of truth for the
v0.41.20.0 brain_checks_score + category_scores + --scope=brain wave. Drift guard
test parses doctor.ts source for both inline {name: 'foo'} and helper
const name = 'foo' patterns; CI fires if any check name lacks a category.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(doctor): brain_checks_score + category_scores + --scope=brain skip-computation
Extends Check with optional category and DoctorReport with brain_checks_score +
category_scores (additive — schema_version stays at 2; back-compat health_score
math byte-identical). buildChecks gains --scope=brain with explicit early-skip
gates around the SKILL check group (resolver_health + skill_conformance +
skill_brain_first + whoknows_health). Sub-second doctor on a brain with thousands
of skills. computeDoctorReport tags every check via categorizeCheck() at compute
time. Human output leads with the brain figure and renders the weighted
BrainHealth.brain_score alongside.
Test seam fix in test/doctor-home-dir-in-worktree.test.ts: the pre-existing
fragile JSON parser walked back from "checks" to find the envelope's outer
brace; v0.41.20.0's new nested category_scores object broke that heuristic.
Anchored on the canonical {"schema_version" envelope prefix instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(status): gbrain status — single-screen brain health dashboard + get_status_snapshot MCP op
NEW gbrain status command (src/commands/status.ts) composes 6 sections:
sync (per-source last_sync_at + staleness via buildSyncStatusReport),
cycle (TWO rows: last autopilot-cycle + last autopilot-* of any kind —
reflects v0.36.4.0 health-aware autopilot's targeted handler routing;
totals read from result.report.totals per the canonical handler shape),
locks (gbrain_cycle_locks active rows), workers (readSupervisorEvents +
summarizeCrashes), queue (LIVE counts NO time-window — old stuck jobs
are exactly what status surfaces), autopilot (PID liveness via kill -0).
Stable --json envelope (schema_version: 1). Exit codes 0=ok / 1=snapshot
failed / 2=usage. --section filter.
Thin-client mode routes Sync + Cycle through NEW get_status_snapshot MCP
op (admin scope, NOT localOnly; payload deliberately omits Locks /
Workers / Queue / Autopilot so feature creep can't quietly widen the
admin-scoped data exposure). Local-only sections render "local-only —
N/A on remote brain" honestly instead of pretending the local install's
empty state is the remote brain's.
CLI dispatch: pre-engine-bind branch for thin-client (no PGLite needed)
+ engine-connected dispatch case for local mode. CLI-only architecture
per codex MAJOR-4 (status owns its own thin-client branch inside
runStatus, not routed through op dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version to v0.41.20.0 + CHANGELOG + TODOS + llms regen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(doctor-categories): categorize batch_retry_health from v0.41.19.0 Supavisor wave
The drift guard correctly caught a new check introduced by master's v0.41.19.0
Supavisor Retry Cathedral (PR #1537). batch_retry_health surfaces batch-write
retry events from the new src/core/audit/batch-retry-audit.ts module — OPS
category (infrastructure liveness).
This is exactly why the drift guard exists: any future check added to doctor.ts
without a category entry fails CI immediately instead of silently degrading
to 'meta'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
6.5 KiB
TypeScript
167 lines
6.5 KiB
TypeScript
/**
|
|
* Tests for the `home_dir_in_worktree` doctor check (v0.35.8.0).
|
|
*
|
|
* Hermetic — drives the file system + GBRAIN_HOME + HOME envs directly via
|
|
* `withEnv`, then invokes `runDoctor(null, ['--fast', '--json'])` and parses
|
|
* the resulting JSON `checks` array. Skips the DB phase (engine=null + --fast).
|
|
*
|
|
* Covers F4 edge cases nailed in plan-eng-review:
|
|
* - .git as DIRECTORY (main repo) — warns
|
|
* - .git as FILE (linked worktree) — warns
|
|
* - walk terminates at $HOME — no false positive past it
|
|
* - GBRAIN_HOME override outside any worktree — ok
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { withEnv } from './helpers/with-env.ts';
|
|
import { runDoctor } from '../src/commands/doctor.ts';
|
|
|
|
let scratch: string;
|
|
|
|
beforeEach(() => {
|
|
scratch = join(tmpdir(), `gbrain-doctor-hw-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(scratch, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
try { rmSync(scratch, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
});
|
|
|
|
/** Run the local doctor (no DB; null engine + --fast) under a stubbed HOME +
|
|
* GBRAIN_HOME, capture stdout AND prevent runDoctor's `process.exit(N)` from
|
|
* killing the test runner. Returns the check matching `name`. */
|
|
async function getCheck(name: string, env: Record<string, string | undefined>) {
|
|
const captured: string[] = [];
|
|
// Patch console.log directly — Bun's console.log doesn't route through the
|
|
// current process.stdout.write reference (it appears to cache the binding
|
|
// at module load), so monkey-patching write() doesn't catch it. console.log
|
|
// is the canonical doctor JSON-output channel.
|
|
const origLog = console.log;
|
|
console.log = (...args: unknown[]) => {
|
|
captured.push(args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ') + '\n');
|
|
};
|
|
const origExit = process.exit;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(process as any).exit = (code?: number) => {
|
|
// Throw a tagged error so the test's try-block sees it; runDoctor's
|
|
// own try/catch doesn't catch this because it's outside its scope.
|
|
throw new Error(`__doctor_exit__:${code ?? 0}`);
|
|
};
|
|
try {
|
|
await withEnv(env, async () => {
|
|
try {
|
|
await runDoctor(null, ['--fast', '--json']);
|
|
} catch (e) {
|
|
// Swallow the synthetic __doctor_exit__ sentinel; rethrow other errors.
|
|
if (!(e instanceof Error) || !e.message.startsWith('__doctor_exit__:')) throw e;
|
|
}
|
|
});
|
|
} finally {
|
|
console.log = origLog;
|
|
process.exit = origExit;
|
|
}
|
|
const text = captured.join('');
|
|
// The doctor's JSON envelope is the LAST line that starts with
|
|
// `{"schema_version"`. v0.41.19.0 added nested objects to the envelope
|
|
// (category_scores), so a "find the `{` before `\"checks\"`" heuristic
|
|
// no longer works — it walks back to category_scores's `{` instead of
|
|
// the outer one. Anchor on the canonical envelope prefix instead.
|
|
const lines = text.split('\n');
|
|
let jsonStr = '';
|
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
const trimmed = lines[i].trim();
|
|
if (trimmed.startsWith('{"schema_version"')) {
|
|
jsonStr = trimmed;
|
|
break;
|
|
}
|
|
}
|
|
let parsed: { checks: { name: string; status: string; message: string }[] };
|
|
try {
|
|
parsed = JSON.parse(jsonStr);
|
|
} catch {
|
|
throw new Error(`Could not parse doctor JSON; saw: ${text.slice(-500)}`);
|
|
}
|
|
return parsed.checks.find(c => c.name === name);
|
|
}
|
|
|
|
describe('home_dir_in_worktree doctor check', () => {
|
|
test('gbrain home outside any worktree → ok', async () => {
|
|
// scratch/.gbrain — no parent has a .git, scratch IS our fake $HOME
|
|
const home = scratch;
|
|
const gbrainParent = home;
|
|
const check = await getCheck('home_dir_in_worktree', {
|
|
HOME: home,
|
|
GBRAIN_HOME: gbrainParent,
|
|
});
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe('ok');
|
|
});
|
|
|
|
test('gbrain home inside dir-style .git worktree → warn', async () => {
|
|
// scratch/home/myrepo/.git/ (directory)
|
|
// scratch/home/myrepo/.gbrain/ ← gbrain home is inside the worktree
|
|
const home = join(scratch, 'home');
|
|
const repo = join(home, 'myrepo');
|
|
mkdirSync(join(repo, '.git'), { recursive: true });
|
|
mkdirSync(repo, { recursive: true });
|
|
const check = await getCheck('home_dir_in_worktree', {
|
|
HOME: home,
|
|
GBRAIN_HOME: repo,
|
|
});
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe('warn');
|
|
expect(check!.message).toContain('myrepo');
|
|
});
|
|
|
|
test('gbrain home inside .git-AS-FILE linked worktree → warn (F4)', async () => {
|
|
// Linked worktrees use a `.git` FILE (not a directory) containing
|
|
// `gitdir: /path/to/main/.git/worktrees/<name>`. Doctor MUST recognize
|
|
// both shapes — this is the Conductor + git-worktrees topology our
|
|
// dev environment runs in.
|
|
const home = join(scratch, 'home');
|
|
const repo = join(home, 'linked-wt');
|
|
mkdirSync(repo, { recursive: true });
|
|
writeFileSync(join(repo, '.git'), 'gitdir: /some/other/path/.git/worktrees/linked-wt\n');
|
|
const check = await getCheck('home_dir_in_worktree', {
|
|
HOME: home,
|
|
GBRAIN_HOME: repo,
|
|
});
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe('warn');
|
|
expect(check!.message).toContain('linked-wt');
|
|
});
|
|
|
|
test('walk terminates at $HOME — .git ABOVE $HOME does NOT trigger warn (F4)', async () => {
|
|
// scratch/.git/ (ABOVE the fake $HOME — should be ignored)
|
|
// scratch/home/ (fake $HOME)
|
|
// scratch/home/.gbrain/ (no worktree below $HOME)
|
|
mkdirSync(join(scratch, '.git'), { recursive: true });
|
|
const home = join(scratch, 'home');
|
|
mkdirSync(home, { recursive: true });
|
|
const check = await getCheck('home_dir_in_worktree', {
|
|
HOME: home,
|
|
GBRAIN_HOME: home,
|
|
});
|
|
expect(check).toBeDefined();
|
|
// OK because the .git is above $HOME, outside our walk scope.
|
|
expect(check!.status).toBe('ok');
|
|
});
|
|
|
|
test('GBRAIN_HOME override pointing outside any worktree → ok', async () => {
|
|
// Real $HOME might be inside a worktree, but the user pointed
|
|
// GBRAIN_HOME at a clean location. Doctor should report ok.
|
|
const home = scratch;
|
|
const safe = join(scratch, 'safe-elsewhere');
|
|
mkdirSync(safe, { recursive: true });
|
|
const check = await getCheck('home_dir_in_worktree', {
|
|
HOME: home,
|
|
GBRAIN_HOME: safe,
|
|
});
|
|
expect(check).toBeDefined();
|
|
expect(check!.status).toBe('ok');
|
|
});
|
|
});
|