mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 00:03:15 +01:00
fix(setup-gbrain): a failing skill check on another agent's folder no longer stops setup on a working brain (#3790) (#3802)
* fix(setup-gbrain): judge doctor health from brain checks only (#3790) Step 5 and gstack-gbrain-detect's gbrain_doctor_ok read gbrain doctor's overall status, so a failing skill check against a skills dir gbrain auto-detected (~/.openclaw/workspace) stopped setup on a working brain. Both now run doctor with --scope=brain and ignore category:skill failures from gbrain versions that predate the flag. gbrain's 'healthy' status is accepted too; only 'ok' and 'warnings' were before. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gbrain-detect): spawn doctor through spawnGbrain, fail closed with a cause Review pass 1 batch: route the doctor spawn through lib/gbrain-exec so the Windows gbrain.cmd shim works, report why doctor printed no JSON, raise --doctor's timeout to 300 s, count skill failures only when doctor says unhealthy, and cover the fail-closed paths and exit codes in tests. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gbrain-detect): a doctor spawn that throws fails closed instead of crashing detect A JSON-null ~/.gbrain/config.json made spawnGbrain throw, which crashed detect before Step 1.5's broken-config remediation could run. Step 5 prose now says skill checks never count, matching the code. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gbrain-detect): ignore doctor stderr so a chatty doctor cannot overflow the spawn buffer stderr over spawnSync's 1 MB maxBuffer made the call fail with ENOBUFS and dropped the stdout report, turning a healthy brain into a Step 5 STOP. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gbrain-detect): let --doctor's report drain before exit process.exit() cut a large verdict off mid-pipe, so Step 5's jq read truncated JSON and stopped on a healthy brain. Set exitCode instead. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(gbrain-detect): say what gbrain_doctor_ok proves under --fast Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: setup-gbrain doctor gate judges brain checks only (#3790) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(gbrain): document gstack-gbrain-detect --doctor and gbrain doctor's status values Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
56a800da98
commit
b2e2c166b0
7 files changed
+292
-24
No files matched your search
@@ -212,7 +212,7 @@ The skill re-collects a PAT (one-time, discarded after), lists every project in
|
||||
|
||||
| Bin | Purpose |
|
||||
|---|---|
|
||||
| `gstack-gbrain-detect` | Emit current state as JSON: gbrain on PATH, version, config engine, doctor status, sync mode |
|
||||
| `gstack-gbrain-detect` | Emit current state as JSON: gbrain on PATH, version, config engine, doctor status, sync mode. `--doctor` runs the full brain-health gate that `/setup-gbrain` Step 5 uses (`gbrain doctor --json --scope=brain`); it prints `brain_ok`, `brain_failures` and `skill_failures`, and exits 0 only when `brain_ok` is true. Failed skill checks (gbrain grading another agent's skills folder) never decide `brain_ok`. |
|
||||
| `gstack-gbrain-install` | Detect-first installer (probes `~/git/gbrain`, `~/gbrain`, then fresh clone). Has `--dry-run` and `--validate-only` flags. PATH-shadow check exits 3 with remediation menu. |
|
||||
| `gstack-gbrain-lib.sh` | Sourced, not executed. Provides `read_secret_to_env VARNAME "prompt" [--echo-redacted "<sed-expr>"]` |
|
||||
| `gstack-gbrain-supabase-verify` | Structural URL check. Rejects direct-connection URLs (`db.*.supabase.co:5432`) with exit 3 |
|
||||
@@ -228,7 +228,7 @@ Gbrain itself ships with these that gstack wraps:
|
||||
|---|---|
|
||||
| `gbrain init --pglite` | Initialize a local PGLite brain |
|
||||
| `gbrain init --non-interactive` | Initialize via env (`GBRAIN_DATABASE_URL` or `DATABASE_URL`). Never pass a URL as argv — it'll leak to shell history. |
|
||||
| `gbrain doctor --json` | Health check. Returns `{status: "ok"|"warnings"|"error", health_score: 0-100, checks: [...]}` |
|
||||
| `gbrain doctor --json` | Health check. Returns `{status: "healthy"|"warnings"|"unhealthy" (older gbrain: "ok"), health_score: 0-100, checks: [...]}`. `--scope=brain` (gbrain v0.41.20+) skips the skill checks. |
|
||||
| `gbrain migrate --to supabase --url ...` | Move a PGLite brain to Supabase (lossless, preserves source as backup) |
|
||||
| `gbrain migrate --to pglite` | Reverse migration |
|
||||
| `gbrain search "query"` | Search the brain |
|
||||
|
||||
+75
-14
@@ -27,6 +27,13 @@
|
||||
* gbrain_local_status. Key order may differ from the bash version's `jq -n`
|
||||
* output — downstream parsers must not depend on key order (none currently do).
|
||||
*
|
||||
* Flags:
|
||||
* --is-ok exit 0 iff the local engine is usable (see below).
|
||||
* --doctor run `gbrain doctor --json --scope=brain` and print
|
||||
* {brain_ok, status, brain_failures, skill_failures, report,
|
||||
* error?};
|
||||
* exit 0 iff brain_ok. Skill checks never decide brain_ok (#3790).
|
||||
*
|
||||
* Env:
|
||||
* GSTACK_HOME — override ~/.gstack for state lookups (used by tests).
|
||||
* HOME — effective user home (drives ~/.gbrain/config.json path).
|
||||
@@ -44,7 +51,7 @@ import {
|
||||
resolveGbrainBin,
|
||||
readGbrainVersion,
|
||||
} from "../lib/gbrain-local-status";
|
||||
import { gbrainConfigDir, isTransactionModePooler } from "../lib/gbrain-exec";
|
||||
import { gbrainConfigDir, isTransactionModePooler, spawnGbrain } from "../lib/gbrain-exec";
|
||||
import { classifyGbrainMcpEntry, classifyGbrainMcpListLine, type GbrainMcpEntry } from "../lib/gbrain-mcp-locality";
|
||||
import { resolveStateRoot } from "../lib/state-root";
|
||||
|
||||
@@ -119,21 +126,66 @@ function detectPoolerMode(): "transaction" | "session" | "unknown" | null {
|
||||
return isTransactionModePooler(parsed.database_url) ? "transaction" : "session";
|
||||
}
|
||||
|
||||
// --- gbrain doctor health (any nonzero exit or non-"ok"/"warnings" status → false) ---
|
||||
// --- gbrain doctor health, judged from brain checks only (#3790) ---
|
||||
//
|
||||
// Uses --fast to avoid hanging on a dead DB. Per the local-status classifier
|
||||
// (which probes DB directly via `gbrain sources list`), gbrain_doctor_ok is a
|
||||
// coarse health summary, not engine-reachability — that's gbrain_local_status.
|
||||
// gbrain doctor's skill checks (category "skill": resolver_health,
|
||||
// skill_conformance, ...) grade whatever skills dir gbrain auto-detects, which
|
||||
// can be another agent's workspace such as ~/.openclaw/workspace. They say
|
||||
// nothing about the brain, so they never decide health here. `--scope=brain`
|
||||
// (gbrain v0.41.20+) skips that group; older gbrain ignores the flag, so
|
||||
// failed checks tagged "skill" are dropped as well. gbrain's status is
|
||||
// "healthy"/"warnings"/"unhealthy"; "ok" is kept for older reports. doctor
|
||||
// exits 1 when any check fails, so stdout is read whatever the exit code.
|
||||
interface DoctorVerdict {
|
||||
brain_ok: boolean;
|
||||
status: string | null;
|
||||
brain_failures: string[];
|
||||
skill_failures: string[];
|
||||
report: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function runDoctor(extraArgs: string[], timeoutMs: number): DoctorVerdict {
|
||||
let r: ReturnType<typeof spawnGbrain>;
|
||||
try {
|
||||
r = spawnGbrain(["doctor", "--json", ...extraArgs, "--scope=brain"], {
|
||||
timeout: timeoutMs,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
} catch (e) {
|
||||
const error = `gbrain doctor could not start: ${e instanceof Error ? e.message : String(e)}`;
|
||||
return { brain_ok: false, status: null, brain_failures: [], skill_failures: [], report: null, error };
|
||||
}
|
||||
let report: { status?: string; checks?: { name?: string; status?: string; category?: string }[] } | null = null;
|
||||
try {
|
||||
report = JSON.parse(r.stdout || "null");
|
||||
} catch {}
|
||||
if (!report || typeof report !== "object") {
|
||||
const cause = r.error ? r.error.message : r.signal ? `killed by ${r.signal}` : `exit ${r.status}`;
|
||||
const error = `gbrain doctor printed no JSON report (${cause})`;
|
||||
return { brain_ok: false, status: null, brain_failures: [], skill_failures: [], report: null, error };
|
||||
}
|
||||
const failed = (Array.isArray(report.checks) ? report.checks : []).filter((c) => c?.status === "fail");
|
||||
const skill_failures = failed.filter((c) => c.category === "skill").map((c) => c.name ?? "unnamed");
|
||||
const brain_failures = failed.filter((c) => c.category !== "skill").map((c) => c.name ?? "unnamed");
|
||||
const status = report.status ?? null;
|
||||
const statusOk = status === "healthy" || status === "ok" || status === "warnings";
|
||||
return {
|
||||
brain_ok: brain_failures.length === 0 && (statusOk || (status === "unhealthy" && skill_failures.length > 0)),
|
||||
status,
|
||||
brain_failures,
|
||||
skill_failures,
|
||||
report,
|
||||
};
|
||||
}
|
||||
|
||||
// Uses --fast to avoid hanging on a dead DB. --fast skips every DB check, so
|
||||
// gbrain_doctor_ok only says doctor ran and its filesystem checks passed; it
|
||||
// proves nothing about the brain's data. Step 5 runs the full --doctor gate.
|
||||
// Engine reachability is gbrain_local_status (it probes `gbrain sources list`).
|
||||
function detectDoctor(onPath: boolean): boolean {
|
||||
if (!onPath) return false;
|
||||
const out = tryExec("gbrain", ["doctor", "--json", "--fast"], 3_000);
|
||||
if (!out) return false;
|
||||
try {
|
||||
const parsed = JSON.parse(out) as { status?: string };
|
||||
return parsed.status === "ok" || parsed.status === "warnings";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return runDoctor(["--fast"], 3_000).brain_ok;
|
||||
}
|
||||
|
||||
// --- artifacts sync mode ---
|
||||
@@ -315,4 +367,13 @@ if (process.argv.includes("--is-ok")) {
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
// --doctor: the full brain-health gate /setup-gbrain Step 5 runs. Prints the
|
||||
// DoctorVerdict as JSON and exits 0 iff brain_ok. exitCode, not process.exit:
|
||||
// exit() cuts off a large report still draining into a pipe.
|
||||
if (process.argv.includes("--doctor")) {
|
||||
const verdict = runDoctor([], 300_000);
|
||||
process.stdout.write(JSON.stringify(verdict, null, 2) + "\n");
|
||||
process.exitCode = verdict.brain_ok ? 0 : 1;
|
||||
} else {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
**`/setup-gbrain` no longer stops on a working brain because gbrain's skill checks failed on another agent's skills folder (#3790).** gbrain doctor also grades whatever skills folder it finds on its own, such as `~/.openclaw/workspace`. When one of those skill checks failed, Step 5 read the whole report as `unhealthy` and stopped, and the agent then offered to edit the other agent's workspace to get past it. `gstack-gbrain-detect` reported `gbrain_doctor_ok: false` for the same reason.
|
||||
|
||||
### Fixed
|
||||
- **Step 5 and `gbrain_doctor_ok` judge the brain from brain checks only** (`bin/gstack-gbrain-detect`, `setup-gbrain`). A new `gstack-gbrain-detect --doctor` runs `gbrain doctor --json --scope=brain`, ignores failed checks tagged `category: "skill"` from gbrain versions that still run them, and prints `brain_ok`, `brain_failures` and `skill_failures`. Step 5 proceeds on `brain_ok`, notes any skill failures, and tells the agent never to edit another agent's workspace to make setup pass. A failed connection, schema_version or pgvector check still stops setup.
|
||||
- **A fully healthy brain passes Step 5.** gbrain reports `healthy`, `warnings` or `unhealthy`; Step 5 used to accept only `ok` or `warnings`, so a brain with no warnings at all stopped too.
|
||||
- **The doctor gate fails closed with a cause.** No JSON, a crash, a timeout or a gbrain that can't start reports `brain_ok: false` and why, instead of crashing detect. A JSON-null `~/.gbrain/config.json` no longer breaks Step 1's detection. Doctor now runs through gbrain's spawn helper, so the Windows `gbrain.cmd` shim works, and a large report reaches Step 5 whole.
|
||||
|
||||
### Tests
|
||||
- `test/setup-gbrain-doctor-gate.test.ts` runs the real Step 5 block and detect against a stub gbrain: skill-only failures with and without `--scope=brain`, brain failures, a fully healthy brain, no JSON, empty output, a large report through a pipe and a JSON-null config. 10 of its first 14 cases failed on main 6bffa20.
|
||||
- `test/gbrain-spawn-windows-shell.test.ts` checks that detect spawns doctor through `spawnGbrain`.
|
||||
@@ -315,12 +315,14 @@ compatible MCP version.
|
||||
For Paths 1, 2a, 2b, 3, switch:
|
||||
|
||||
```bash
|
||||
doctor=$(gbrain doctor --json)
|
||||
status=$(echo "$doctor" | jq -r .status)
|
||||
DOCTOR=$(~/.claude/skills/gstack/bin/gstack-gbrain-detect --doctor)
|
||||
BRAIN_OK=$(echo "$DOCTOR" | jq -r .brain_ok)
|
||||
```
|
||||
|
||||
If status is `ok` or `warnings`, proceed. Anything else → surface the full
|
||||
doctor output and STOP.
|
||||
If `BRAIN_OK` is `true`, proceed; skill checks never count. Note any
|
||||
`skill_failures`: gbrain graded a skills dir it found, maybe another agent's
|
||||
(`~/.openclaw/workspace`). Never edit that workspace to pass. Otherwise show
|
||||
`echo "$DOCTOR" | jq .` and STOP.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -230,12 +230,14 @@ compatible MCP version.
|
||||
For Paths 1, 2a, 2b, 3, switch:
|
||||
|
||||
```bash
|
||||
doctor=$(gbrain doctor --json)
|
||||
status=$(echo "$doctor" | jq -r .status)
|
||||
DOCTOR=$(~/.claude/skills/gstack/bin/gstack-gbrain-detect --doctor)
|
||||
BRAIN_OK=$(echo "$DOCTOR" | jq -r .brain_ok)
|
||||
```
|
||||
|
||||
If status is `ok` or `warnings`, proceed. Anything else → surface the full
|
||||
doctor output and STOP.
|
||||
If `BRAIN_OK` is `true`, proceed; skill checks never count. Note any
|
||||
`skill_failures`: gbrain graded a skills dir it found, maybe another agent's
|
||||
(`~/.openclaw/workspace`). Never edit that workspace to pass. Otherwise show
|
||||
`echo "$DOCTOR" | jq .` and STOP.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@ describe("#1731 gbrain spawns carry the Windows shell flag", () => {
|
||||
});
|
||||
}
|
||||
|
||||
test("bin/gstack-gbrain-detect: gbrain doctor goes through spawnGbrain (no direct openers)", () => {
|
||||
const src = read("bin/gstack-gbrain-detect");
|
||||
expect(src.match(/(spawnSync|spawn|execFileSync|tryExec)\(\s*["']gbrain["']/g)?.length ?? 0).toBe(0);
|
||||
expect(src).toContain("spawnGbrain([\"doctor\"");
|
||||
});
|
||||
|
||||
// Not-yet-seamed file: every direct gbrain spawn must still carry the
|
||||
// #1731 shell flag. (Migrate to gbrainInvocation when next touched.)
|
||||
test("lib/gbrain-local-status.ts: every gbrain spawn has shell:NEEDS_SHELL_ON_WINDOWS", () => {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* /setup-gbrain Step 5 and gstack-gbrain-detect judge brain health from brain
|
||||
* checks only (#3790).
|
||||
*
|
||||
* gbrain doctor's skill checks (category "skill": resolver_health,
|
||||
* skill_conformance, ...) grade whatever skills dir gbrain auto-detects, which
|
||||
* can be another agent's workspace such as ~/.openclaw/workspace. A failure
|
||||
* there made a working brain read "unhealthy", Step 5 stopped, and the agent
|
||||
* proposed editing the other agent's workspace. Real brain failures
|
||||
* (connection, schema_version, pgvector) must still stop Step 5.
|
||||
*
|
||||
* A stub gbrain on PATH serves canned doctor JSON; no real gbrain runs.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const DETECT = path.join(ROOT, 'bin', 'gstack-gbrain-detect');
|
||||
const SKILL_MD = path.join(ROOT, 'setup-gbrain', 'SKILL.md');
|
||||
|
||||
const check = (name: string, category: string, status = 'ok') => ({ name, category, status, message: '' });
|
||||
const BRAIN_OK = [
|
||||
check('connection', 'ops'), check('pgvector', 'ops'), check('schema_version', 'meta'), check('embeddings', 'brain'),
|
||||
];
|
||||
const SKILL_FAIL = check('resolver_health', 'skill', 'fail');
|
||||
const report = (checks: ReturnType<typeof check>[]) => ({
|
||||
schema_version: 2,
|
||||
status: checks.some((c) => c.status === 'fail') ? 'unhealthy' : checks.some((c) => c.status === 'warn') ? 'warnings' : 'healthy',
|
||||
checks,
|
||||
});
|
||||
const withFailure = (name: string) => BRAIN_OK.map((c) => (c.name === name ? { ...c, status: 'fail' } : c));
|
||||
|
||||
type Case = { full: object | string; scoped: object | string };
|
||||
const CASES: Record<string, Case> = {
|
||||
// gbrain >= v0.41.20: --scope=brain drops the skill group.
|
||||
'skill-only failure': {
|
||||
full: report([...BRAIN_OK, SKILL_FAIL]),
|
||||
scoped: report([...BRAIN_OK, check('harness_wiring', 'ops', 'warn')]),
|
||||
},
|
||||
// Older gbrain ignores --scope=brain and still runs the skill group.
|
||||
'skill-only failure, gbrain without --scope': {
|
||||
full: report([...BRAIN_OK, SKILL_FAIL]),
|
||||
scoped: report([...BRAIN_OK, SKILL_FAIL]),
|
||||
},
|
||||
'all checks pass': { full: report(BRAIN_OK), scoped: report(BRAIN_OK) },
|
||||
'connection failure': { full: report([...withFailure('connection'), SKILL_FAIL]), scoped: report(withFailure('connection')) },
|
||||
'schema_version failure': { full: report([...withFailure('schema_version'), SKILL_FAIL]), scoped: report(withFailure('schema_version')) },
|
||||
'pgvector failure': { full: report([...withFailure('pgvector'), SKILL_FAIL]), scoped: report(withFailure('pgvector')) },
|
||||
'pgvector failure, gbrain without --scope': {
|
||||
full: report([...withFailure('pgvector'), SKILL_FAIL]),
|
||||
scoped: report([...withFailure('pgvector'), SKILL_FAIL]),
|
||||
},
|
||||
'doctor prints no JSON': { full: 'Error: engine crashed\n', scoped: 'Error: engine crashed\n' },
|
||||
'doctor prints nothing': { full: '', scoped: '' },
|
||||
'unhealthy status with no failed check': {
|
||||
full: { ...report(BRAIN_OK), status: 'unhealthy' },
|
||||
scoped: { ...report(BRAIN_OK), status: 'unhealthy' },
|
||||
},
|
||||
};
|
||||
const HEALTHY = ['skill-only failure', 'skill-only failure, gbrain without --scope', 'all checks pass'];
|
||||
|
||||
let tmp: string;
|
||||
const SAFE_PATH = '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin';
|
||||
|
||||
beforeAll(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-doctor-gate-'));
|
||||
fs.mkdirSync(path.join(tmp, 'bin'));
|
||||
fs.symlinkSync(process.execPath, path.join(tmp, 'bin', 'bun'));
|
||||
// gbrain doctor exits 1 when any check fails and prints the report anyway.
|
||||
fs.writeFileSync(path.join(tmp, 'bin', 'gbrain'), `#!/bin/sh
|
||||
case "$1" in
|
||||
--version) echo "gbrain 0.60.537.0"; exit 0 ;;
|
||||
doctor) ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
f="$FAKE_DOCTOR_DIR/full.json"
|
||||
for a in "$@"; do [ "$a" = "--scope=brain" ] && f="$FAKE_DOCTOR_DIR/scoped.json"; done
|
||||
cat "$f"
|
||||
grep -q '"status":"unhealthy"' "$f" && exit 1
|
||||
grep -q '^{' "$f" || exit 1
|
||||
exit 0
|
||||
`, { mode: 0o755 });
|
||||
});
|
||||
|
||||
afterAll(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
function caseEnv(name: string) {
|
||||
const dir = path.join(tmp, name.replace(/[^a-z0-9]+/gi, '-'));
|
||||
fs.mkdirSync(path.join(dir, 'home'), { recursive: true });
|
||||
const { full, scoped } = CASES[name]!;
|
||||
fs.writeFileSync(path.join(dir, 'full.json'), typeof full === 'string' ? full : JSON.stringify(full));
|
||||
fs.writeFileSync(path.join(dir, 'scoped.json'), typeof scoped === 'string' ? scoped : JSON.stringify(scoped));
|
||||
return {
|
||||
...process.env,
|
||||
PATH: `${path.join(tmp, 'bin')}:${SAFE_PATH}`,
|
||||
HOME: path.join(dir, 'home'),
|
||||
GSTACK_HOME: path.join(dir, 'state'),
|
||||
GSTACK_DETECT_NO_CACHE: '1',
|
||||
FAKE_DOCTOR_DIR: dir,
|
||||
};
|
||||
}
|
||||
|
||||
function step5Fence(): string {
|
||||
const md = fs.readFileSync(SKILL_MD, 'utf-8');
|
||||
const start = md.indexOf('## Step 5: Verify gbrain doctor');
|
||||
const end = md.indexOf('\n## ', start + 1);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const fence = md.slice(start, end).match(/\`\`\`bash\n([\s\S]*?)\`\`\`/);
|
||||
expect(fence).not.toBeNull();
|
||||
return fence![1]!.replaceAll('~/.claude/skills/gstack/bin/', `${ROOT}/bin/`);
|
||||
}
|
||||
|
||||
describe('/setup-gbrain Step 5 doctor gate', () => {
|
||||
for (const name of Object.keys(CASES)) {
|
||||
const healthy = HEALTHY.includes(name);
|
||||
test(`${name}: Step 5 ${healthy ? 'proceeds' : 'stops'}`, () => {
|
||||
const r = spawnSync('bash', ['-c', `${step5Fence()}\necho "BRAIN_OK=\${BRAIN_OK:-unset} status=\${status:-unset}"`], {
|
||||
env: caseEnv(name), encoding: 'utf-8', timeout: 30_000,
|
||||
});
|
||||
const verdict = r.stdout.trim().split('\n').pop();
|
||||
expect(verdict).toStartWith(`BRAIN_OK=${healthy}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('gstack-gbrain-detect --doctor', () => {
|
||||
test('exits 0 and names skill failures when only skill checks fail', () => {
|
||||
const r = spawnSync(DETECT, ['--doctor'], { env: caseEnv('skill-only failure, gbrain without --scope'), encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(r.status).toBe(0);
|
||||
const v = JSON.parse(r.stdout);
|
||||
expect(v).toMatchObject({ brain_ok: true, brain_failures: [], skill_failures: ['resolver_health'] });
|
||||
});
|
||||
|
||||
test('exits 1 and names the brain failure', () => {
|
||||
const r = spawnSync(DETECT, ['--doctor'], { env: caseEnv('pgvector failure'), encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(r.status).toBe(1);
|
||||
expect(JSON.parse(r.stdout)).toMatchObject({ brain_ok: false, brain_failures: ['pgvector'] });
|
||||
});
|
||||
|
||||
test('prints a large report whole through a pipe', () => {
|
||||
const big = { status: 'healthy', checks: Array.from({ length: 3000 }, (_, i) => ({ ...check(`c${i}`, 'brain'), message: 'x'.repeat(60) })) };
|
||||
const env = { ...caseEnv('all checks pass'), FAKE_DOCTOR_DIR: path.join(tmp, 'big') };
|
||||
fs.mkdirSync(env.FAKE_DOCTOR_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(env.FAKE_DOCTOR_DIR, 'scoped.json'), JSON.stringify(big));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const r = spawnSync('bash', ['-c', `"${DETECT}" --doctor | cat`], { env, encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(JSON.parse(r.stdout).report.checks).toHaveLength(3000);
|
||||
}
|
||||
});
|
||||
|
||||
test('exits 1 with the cause when doctor prints no JSON', () => {
|
||||
const r = spawnSync(DETECT, ['--doctor'], { env: caseEnv('doctor prints no JSON'), encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(r.status).toBe(1);
|
||||
const v = JSON.parse(r.stdout);
|
||||
expect(v.brain_ok).toBe(false);
|
||||
expect(v.error).toContain('exit 1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-gbrain-detect with a JSON-null gbrain config', () => {
|
||||
test('still prints detect JSON, and --doctor fails closed with a cause', () => {
|
||||
const env = { ...caseEnv('all checks pass'), HOME: path.join(tmp, 'null-config-home') };
|
||||
fs.mkdirSync(path.join(env.HOME, '.gbrain'), { recursive: true });
|
||||
fs.writeFileSync(path.join(env.HOME, '.gbrain', 'config.json'), 'null');
|
||||
const detect = spawnSync(DETECT, [], { env, encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(detect.status).toBe(0);
|
||||
expect(JSON.parse(detect.stdout).gbrain_doctor_ok).toBe(false);
|
||||
const doctor = spawnSync(DETECT, ['--doctor'], { env, encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(doctor.status).toBe(1);
|
||||
expect(JSON.parse(doctor.stdout).error).toContain('could not start');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-gbrain-detect gbrain_doctor_ok', () => {
|
||||
for (const name of Object.keys(CASES)) {
|
||||
const healthy = HEALTHY.includes(name);
|
||||
test(`${name}: gbrain_doctor_ok is ${healthy}`, () => {
|
||||
const r = spawnSync(DETECT, [], { env: caseEnv(name), encoding: 'utf-8', timeout: 30_000 });
|
||||
expect(r.status).toBe(0);
|
||||
expect(JSON.parse(r.stdout).gbrain_doctor_ok).toBe(healthy);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user