mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
* fix(schema): make bundled pack inspection truthful (#2029) Two live bugs: - parseYamlMini had no block-scalar support, so a 'description: |' swallowed every following top-level key — the active gbrain-recommended pack loaded with 0 page types. Add parseBlockScalar for |/|-/|+ and >/>-/>+ in both mapping and sequence-sibling positions. - The bundled-pack list was hand-copied in three places (operations.ts had 2 names, mutate.ts had 3, load-active.ts had 7). New single registry src/core/schema-pack/bundled.ts carries all 7 shipped packs; every consumer derives from it. Takeover of #2029, rebased onto master (schema.ts hunks already landed). Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(minions): subagent default client resolves config-stored Anthropic key (#2048) The legacy subagent path constructed a bare new Anthropic() (env-only), so launchd/MCP workers whose key lives in gbrain config (anthropic_api_key) failed auth. anthropic-key.ts now exports resolveAnthropicKey() (env first, then config; hasAnthropicKey delegates) and makeSubagentHandler passes it as apiKey. Partial takeover of #2048 — only the auth patch; the path patches were superseded by the outputRoot mechanism (#2415). Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(schema): point bundled-registry test at bundled.ts source of truth The bundled pack list moved from load-active.ts to bundled.ts in the truthful-inspection refactor; the T4 registry test still grepped load-active.ts source. Assert BUNDLED_PACK_NAMES directly instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(schema): block scalars keep '#' as literal content Inside a YAML block scalar '#' is content, not a comment; parseBlockScalar was routing lines through stripComment/isBlank, truncating descriptions like 'see issue #2029' and blanking comment-looking lines. Use the raw line inside the scalar. Adds a pinning test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: JiraiyaETH <JiraiyaETH@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
220 lines
8.8 KiB
TypeScript
220 lines
8.8 KiB
TypeScript
// v0.38 Phase C: gbrain schema CLI smoke tests.
|
|
//
|
|
// Tests the runSchema dispatch + each subcommand's output shape via
|
|
// the public CLI entrypoint. Hermetic — uses Bun's subprocess to run
|
|
// the CLI like a user would.
|
|
|
|
import { describe, expect, test, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const REPO_ROOT = join(import.meta.dir, '..');
|
|
|
|
// Default-isolated GBRAIN_HOME for every gbrain() call. Without this,
|
|
// tests that read `~/.gbrain/config.json` inherit the developer's real
|
|
// brain config — and sibling Conductor worktrees writing to the same
|
|
// config (e.g. via `schema use` or `config set` during their own tests)
|
|
// cause flakes (the failing test pre-fix saw `schema_pack: "gbrain-base-v2"`
|
|
// from another worktree, which doesn't exist in the bundle, and got
|
|
// exit 1 instead of the asserted 0).
|
|
let DEFAULT_GBRAIN_HOME: string;
|
|
|
|
beforeAll(() => {
|
|
DEFAULT_GBRAIN_HOME = mkdtempSync(join(tmpdir(), 'gbrain-schema-cli-default-'));
|
|
});
|
|
|
|
afterAll(() => {
|
|
rmSync(DEFAULT_GBRAIN_HOME, { recursive: true, force: true });
|
|
});
|
|
|
|
function gbrain(
|
|
args: string[],
|
|
extraEnv: Record<string, string> = {},
|
|
): { stdout: string; stderr: string; code: number } {
|
|
// bun's spawnSync does NOT inherit env mutations done via process.env = ...,
|
|
// so pass env explicitly. CLAUDE.md flags this pattern as load-bearing for
|
|
// any subprocess test that needs GBRAIN_HOME isolation.
|
|
const env = {
|
|
...process.env,
|
|
GBRAIN_DATABASE_URL: '',
|
|
DATABASE_URL: '',
|
|
GBRAIN_HOME: DEFAULT_GBRAIN_HOME,
|
|
...extraEnv,
|
|
};
|
|
const result = spawnSync('bun', ['run', 'src/cli.ts', ...args], {
|
|
cwd: REPO_ROOT,
|
|
encoding: 'utf-8',
|
|
env,
|
|
});
|
|
return {
|
|
stdout: result.stdout ?? '',
|
|
stderr: result.stderr ?? '',
|
|
code: result.status ?? -1,
|
|
};
|
|
}
|
|
|
|
describe('gbrain schema CLI (Phase C)', () => {
|
|
test('schema with no subcommand shows help text', () => {
|
|
// Note: `schema --help` is intercepted by the CLI's parent help system
|
|
// and prints generic help (`gbrain --help` for full command list). The
|
|
// schema-specific help fires when no subcommand is provided.
|
|
const r = gbrain(['schema']);
|
|
expect(r.stdout + r.stderr).toMatch(/schema|active|list|show|validate|use/i);
|
|
});
|
|
|
|
test('schema list shows all bundled packs', () => {
|
|
const r = gbrain(['schema', 'list']);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('Bundled packs:');
|
|
expect(r.stdout).toContain('gbrain-base');
|
|
expect(r.stdout).toContain('gbrain-recommended');
|
|
expect(r.stdout).toContain('gbrain-base-v2');
|
|
expect(r.stdout).toContain('gbrain-investor');
|
|
});
|
|
|
|
test('schema show gbrain-base prints manifest details', () => {
|
|
const r = gbrain(['schema', 'show', 'gbrain-base']);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('gbrain-base v1.0.0');
|
|
// v0.41.11.0: page types extended from 22 to 24 by promoting
|
|
// `conversation` and `atom` into gbrain-base.
|
|
// v0.41.23.0: extended to 25 by adding `extract_receipt` for the
|
|
// unified extract receipt-writer surface (D-EXTRACT-19 belt+suspenders).
|
|
// v0.42.56.0 (#2390): extended to 27 by adding the Life Chronicle
|
|
// `event` + `diary` temporal types (life/events/, life/diary/).
|
|
expect(r.stdout).toContain('Page types (27)');
|
|
expect(r.stdout).toContain('event :: temporal');
|
|
expect(r.stdout).toContain('diary :: temporal');
|
|
expect(r.stdout).toContain('Link verbs (12)');
|
|
expect(r.stdout).toContain('Takes kinds: fact, take, bet, hunch');
|
|
expect(r.stdout).toContain('person :: entity');
|
|
expect(r.stdout).toContain('company :: entity');
|
|
});
|
|
|
|
test('schema validate gbrain-base passes', () => {
|
|
const r = gbrain(['schema', 'validate', 'gbrain-base']);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('✓');
|
|
expect(r.stdout).toContain('valid manifest');
|
|
});
|
|
|
|
test('schema show/validate exposes bundled gbrain-recommended', () => {
|
|
const show = gbrain(['schema', 'show', 'gbrain-recommended']);
|
|
expect(show.code).toBe(0);
|
|
expect(show.stdout).toContain('gbrain-recommended v1.0.0');
|
|
expect(show.stdout).toContain('Page types (');
|
|
expect(show.stdout).toContain('meeting :: temporal');
|
|
|
|
const validate = gbrain(['schema', 'validate', 'gbrain-recommended']);
|
|
expect(validate.code).toBe(0);
|
|
expect(validate.stdout).toContain('valid manifest');
|
|
});
|
|
|
|
test('schema show exposes bundled gbrain-base-v2 successor pack', () => {
|
|
const r = gbrain(['schema', 'show', 'gbrain-base-v2']);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('gbrain-base-v2 v1.0.0');
|
|
expect(r.stdout).toContain('Page types (');
|
|
expect(r.stdout).toContain('Link verbs (14)');
|
|
});
|
|
|
|
test('schema active loads configured gbrain-recommended with real types', () => {
|
|
const home = mkdtempSync(join(tmpdir(), 'gbrain-schema-active-recommended-'));
|
|
try {
|
|
mkdirSync(join(home, '.gbrain'), { recursive: true });
|
|
writeFileSync(join(home, '.gbrain', 'config.json'), JSON.stringify({ schema_pack: 'gbrain-recommended' }), 'utf-8');
|
|
const r = gbrain(['schema', 'active'], { GBRAIN_HOME: home });
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('Active pack: gbrain-recommended');
|
|
expect(r.stdout).not.toContain('Page types: 0');
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('schema active reports default resolution', () => {
|
|
const r = gbrain(['schema', 'active']);
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('Active pack:');
|
|
expect(r.stdout).toContain('Pack identity:');
|
|
});
|
|
|
|
test('schema show unknown-pack errors with hint', () => {
|
|
const r = gbrain(['schema', 'show', 'nonexistent-pack']);
|
|
expect(r.code).not.toBe(0);
|
|
expect(r.stderr).toContain('Unknown pack');
|
|
expect(r.stderr).toContain('schema list');
|
|
});
|
|
|
|
test('unknown subcommand exits with hint', () => {
|
|
const r = gbrain(['schema', 'frobnicate']);
|
|
expect(r.code).toBe(2);
|
|
expect(r.stderr).toContain('Unknown schema subcommand');
|
|
});
|
|
|
|
test('schema use without arg shows usage hint', () => {
|
|
const r = gbrain(['schema', 'use']);
|
|
expect(r.code).toBe(2);
|
|
expect(r.stderr).toContain('Usage:');
|
|
});
|
|
});
|
|
|
|
describe('gbrain schema use (Phase C, gap-fill T3)', () => {
|
|
let home: string;
|
|
|
|
beforeEach(() => {
|
|
home = mkdtempSync(join(tmpdir(), 'gbrain-schema-use-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(home, { recursive: true, force: true });
|
|
});
|
|
|
|
test('writes schema_pack to ~/.gbrain/config.json on happy path', () => {
|
|
const r = gbrain(['schema', 'use', 'gbrain-base'], { GBRAIN_HOME: home });
|
|
expect(r.code).toBe(0);
|
|
expect(r.stdout).toContain('Active schema pack set to: gbrain-base');
|
|
expect(r.stdout).toContain('schema active');
|
|
const cfgPath = join(home, '.gbrain', 'config.json');
|
|
expect(existsSync(cfgPath)).toBe(true);
|
|
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
expect(cfg.schema_pack).toBe('gbrain-base');
|
|
});
|
|
|
|
test('preserves pre-existing config fields when writing schema_pack', () => {
|
|
// Pre-seed a config with engine + a custom key so the merge preserves them.
|
|
mkdirSync(join(home, '.gbrain'), { recursive: true });
|
|
const cfgPath = join(home, '.gbrain', 'config.json');
|
|
writeFileSync(cfgPath, JSON.stringify({ engine: 'pglite', openai_key: 'sk-fake' }, null, 2), 'utf-8');
|
|
const r = gbrain(['schema', 'use', 'gbrain-base'], { GBRAIN_HOME: home });
|
|
expect(r.code).toBe(0);
|
|
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
expect(cfg.engine).toBe('pglite');
|
|
expect(cfg.openai_key).toBe('sk-fake');
|
|
expect(cfg.schema_pack).toBe('gbrain-base');
|
|
});
|
|
|
|
test('overwrites prior schema_pack value on re-run', () => {
|
|
// First set a placeholder, then overwrite via the CLI.
|
|
mkdirSync(join(home, '.gbrain'), { recursive: true });
|
|
const cfgPath = join(home, '.gbrain', 'config.json');
|
|
writeFileSync(cfgPath, JSON.stringify({ engine: 'pglite', schema_pack: 'something-else' }, null, 2), 'utf-8');
|
|
const r = gbrain(['schema', 'use', 'gbrain-base'], { GBRAIN_HOME: home });
|
|
expect(r.code).toBe(0);
|
|
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
|
|
expect(cfg.schema_pack).toBe('gbrain-base');
|
|
});
|
|
|
|
test('unknown pack rejected with exit 1 + paste-ready hint', () => {
|
|
const r = gbrain(['schema', 'use', 'no-such-pack-xyz'], { GBRAIN_HOME: home });
|
|
expect(r.code).toBe(1);
|
|
expect(r.stderr).toContain('Unknown pack');
|
|
expect(r.stderr).toContain('schema list');
|
|
// Importantly: a failed `use` must NOT have written a config.
|
|
const cfgPath = join(home, '.gbrain', 'config.json');
|
|
expect(existsSync(cfgPath)).toBe(false);
|
|
});
|
|
});
|