mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
/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.
110 lines
6.0 KiB
TypeScript
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(); }
|
|
});
|