Files
gstack/test/sync-gbrain-source-probe.test.ts
T
garrytan a05965fb0a B9: read cycle_freshness from a doctor run that includes it
/sync-gbrain Step 3.5 and the --full auto-dream decision read CYCLE from
gbrain doctor --json --fast, which skips the DB checks that carry
cycle_freshness, so the answer was always "unknown". Both now read
gbrain doctor --json --scope=brain (DB checks, no skill-file walk), parse
its JSON even when doctor exits 1 for a failing check, and match the
issue that names this source: "never completed" is never, "last cycled
Nh ago" is completed. When the installed gbrain does not report the
check, they say "unknown: installed gbrain does not expose
cycle_freshness" (lib readCycleStatus carries the reason to the --full
SKIP row).

The source-probe fixture that pinned the --fast call and a made-up
message format now answers --scope=brain with gbrain's real message.
2026-10-03 21:30:04 +00:00

110 lines
6.0 KiB
TypeScript

import { expect, test } from 'bun:test';
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const ROOT = resolve(import.meta.dir, '..');
const skill = readFileSync(join(ROOT, 'sync-gbrain/SKILL.md'), 'utf8');
function shellStep(title: string): string {
const start = skill.indexOf(title);
if (start < 0) throw new Error(`${title} missing from generated skill`);
const block = /```bash\n([\s\S]*?)\n```/.exec(skill.slice(start));
if (!block) throw new Error(`${title} has no bash fence`);
return block[1].replaceAll('~/.claude/skills/gstack/bin/gstack-gbrain-read-capability.ts', join(ROOT, 'bin/gstack-gbrain-read-capability.ts'));
}
function fixture(pin = 'client-fixture', opts: { pageCount?: number; cycleStatus?: string; cycleMessage?: string; sourceError?: boolean; wrongRegistration?: boolean } = {}) {
const root = mkdtempSync(join(tmpdir(), 'gbrain-step3-'));
const repo = join(root, 'repo'), home = join(root, 'home'), bin = join(root, 'bin');
mkdirSync(repo); mkdirSync(home); mkdirSync(bin); mkdirSync(join(home, '.gstack'));
if (opts.wrongRegistration) mkdirSync(join(root, 'sibling'));
const init = spawnSync('git', ['init', '--quiet', repo], { timeout: 10_000 });
if (init.status !== 0) throw new Error('git init failed');
writeFileSync(join(repo, '.gbrain-source'), `${pin}\n`);
writeFileSync(join(home, '.gstack', '.gbrain-sync-state.json'), JSON.stringify({
schema_version: 1, last_writer: 'gstack-gbrain-sync', last_stages: [{
name: 'code', ran: true, ok: true, detail: { status: 'ok', source_id: 'client-fixture', source_path: repo },
}],
}, null, 2));
const log = join(root, 'calls');
writeFileSync(join(bin, 'gbrain'), `#!/usr/bin/env bun
import { appendFileSync } from 'node:fs';
const args = process.argv.slice(2).join(' ');
appendFileSync(${JSON.stringify(log)}, args + '\\n');
if (args === 'sources list --json') console.log(${JSON.stringify(JSON.stringify({
...(opts.sourceError ? { error: { code: 'partial_read' } } : {}),
sources: [{ id: 'client-fixture', local_path: opts.wrongRegistration ? join(root, 'sibling') : repo, page_count: opts.pageCount ?? 7 }],
}))});
// B9: cycle_freshness is a DB check that \`doctor --fast\` skips, so the
// skill reads \`--scope=brain\`; the message follows gbrain's real format.
else if (args === 'doctor --json --scope=brain') console.log(${JSON.stringify(JSON.stringify({ checks: [{
name: 'cycle_freshness', status: opts.cycleStatus ?? 'warn', message: opts.cycleMessage ?? "Source 'client-fixture' has never completed a full cycle. Run \`gbrain dream --source <id>\` to cycle a source.",
}] }))});
else process.exit(2);
`);
chmodSync(join(bin, 'gbrain'), 0o755);
const run = (title: string) => spawnSync('bash', ['-c', shellStep(title)], {
cwd: repo, encoding: 'utf8', timeout: 10_000,
env: { ...process.env, HOME: home, GSTACK_HOME: join(home, '.gstack'), PATH: `${bin}:${process.env.PATH}` },
});
return { run, calls: () => { try { return readFileSync(log, 'utf8').trim().split('\n'); } catch { return []; } }, cleanup: () => rmSync(root, { recursive: true, force: true }) };
}
test('actual generated Step 3/3.5 shell reads a pretty state and probes the pinned source', () => {
const f = fixture();
try {
const pages = f.run('## Step 3: Code-index health check');
const cycle = f.run('## Step 3.5: Call-graph health check');
expect(pages.status, pages.stderr).toBe(0);
expect(cycle.status, cycle.stderr).toBe(0);
expect(pages.stdout).toContain('cwd source: client-fixture, page_count: 7');
expect(cycle.stdout).toContain('call graph for client-fixture: never');
expect(f.calls()).toEqual(['sources list --json', 'sources list --json', 'doctor --json --scope=brain']);
} finally { f.cleanup(); }
});
test('unverified pin never queries another source or reports zero pages', () => {
const f = fixture('other-source');
try {
const pages = f.run('## Step 3: Code-index health check');
const cycle = f.run('## Step 3.5: Call-graph health check');
expect(pages.status, pages.stderr).toBe(0);
expect(cycle.status, cycle.stderr).toBe(0);
expect(pages.stdout).not.toContain('page_count: 0');
expect(cycle.stdout).toContain('unknown');
expect(f.calls()).toEqual([]);
} finally { f.cleanup(); }
});
test('verified zero pages and completed cycle retain their existing meanings', () => {
const f = fixture('client-fixture', { pageCount: 0, cycleStatus: 'ok' });
try {
expect(f.run('## Step 3: Code-index health check').stdout).toContain('page_count: 0');
expect(f.run('## Step 3.5: Call-graph health check').stdout).toContain('call graph for client-fixture: completed');
} finally { f.cleanup(); }
});
test('source-list error and another source cycle never masquerade as zero or never', () => {
const f = fixture('client-fixture', { sourceError: true, cycleMessage: 'never cycled other-source' });
try {
expect(f.run('## Step 3: Code-index health check').stdout).toContain('page_count: \n');
expect(f.run('## Step 3.5: Call-graph health check').stdout).toContain('call graph for : unknown');
expect(f.calls()).toEqual(['sources list --json', 'sources list --json']);
} finally { f.cleanup(); }
});
test('sibling registration with zero pages is unknown, not a reindex offer', () => {
const f = fixture('client-fixture', { pageCount: 0, wrongRegistration: true });
try {
const pages = f.run('## Step 3: Code-index health check');
const cycle = f.run('## Step 3.5: Call-graph health check');
expect(pages.status, pages.stderr).toBe(0);
expect(cycle.status, cycle.stderr).toBe(0);
expect(pages.stdout).toContain('page_count: \n');
expect(pages.stdout).not.toContain('page_count: 0');
expect(cycle.stdout).toContain('call graph for : unknown');
expect(f.calls()).toEqual(['sources list --json', 'sources list --json']);
} finally { f.cleanup(); }
});