diff --git a/src/commands/takes.ts b/src/commands/takes.ts index 981463822..8d7eca1cc 100644 --- a/src/commands/takes.ts +++ b/src/commands/takes.ts @@ -3,6 +3,7 @@ * * Subcommands: * takes — list takes for a page + * takes list — list all active takes (#2079) * takes search "" [--who h] — keyword search across all takes * takes add ...flags — append a take (markdown + DB) * takes update --row N ...flags — update mutable fields @@ -129,11 +130,10 @@ function writeBody(path: string, body: string): void { // --- Subcommands --- async function cmdList(engine: BrainEngine, args: string[]): Promise { - const slug = args[0]; - if (!slug) { - console.error('Usage: gbrain takes [--json]'); - process.exit(1); - } + // #2079: slug is optional. `gbrain takes list` (no slug) lists ALL active + // takes — CLI parity with the takes_list operation. A leading flag is not + // a slug. + const slug = args[0] && !args[0].startsWith('-') ? args[0] : undefined; const json = flagPresent(args, '--json'); const holder = flagValue(args, '--who'); const kind = flagValue(args, '--kind') as string | undefined; @@ -153,17 +153,19 @@ async function cmdList(engine: BrainEngine, args: string[]): Promise { return; } + const scope = slug ?? 'this brain'; if (takes.length === 0) { - console.log(`No takes on ${slug}.`); + console.log(`No takes on ${scope}.`); return; } - console.log(`# Takes on ${slug}\n`); + console.log(`# Takes on ${scope}\n`); for (const t of takes) { const tag = t.active ? '' : ' [superseded]'; const w = Number(t.weight).toFixed(2); const since = t.since_date ?? ''; const src = t.source ? ` — ${t.source}` : ''; - console.log(`#${t.row_num} [${t.kind} • ${t.holder} • w=${w}${since ? ` • ${since}` : ''}]${tag}\n ${t.claim}${src}\n`); + const where = slug ? '' : `${t.page_slug} `; + console.log(`${where}#${t.row_num} [${t.kind} • ${t.holder} • w=${w}${since ? ` • ${since}` : ''}]${tag}\n ${t.claim}${src}\n`); } } @@ -555,6 +557,8 @@ export async function runTakes(engine: BrainEngine, args: string[]): Promise [--json] [--who h] [--kind k] [--sort weight|since_date|created_at] [--expired] List takes for a page + takes list [--json] [--who h] [--kind k] [--sort ...] [--expired] + List all active takes across the brain (#2079) takes search "" [--limit N] [--json] Keyword search across all takes takes add --claim "..." --kind --who @@ -584,6 +588,9 @@ Common flags: const rest = args.slice(1); switch (sub) { + // #2079: `takes list` used to be parsed as page slug "list" and printed + // "No takes on list." — reading exactly like an empty takes table. + case 'list': return cmdList(engine, rest); case 'search': return cmdSearch(engine, rest); case 'add': return cmdAdd(engine, rest, await resolveTakesSourceId(engine)); case 'update': return cmdUpdate(engine, rest, await resolveTakesSourceId(engine)); diff --git a/test/takes-list-subcommand.test.ts b/test/takes-list-subcommand.test.ts new file mode 100644 index 000000000..14aa46bcd --- /dev/null +++ b/test/takes-list-subcommand.test.ts @@ -0,0 +1,76 @@ +/** + * #2079 — `gbrain takes list` used to parse "list" as a PAGE SLUG: cmdList + * looked up a page named "list" and printed "No takes on list." even when the + * brain held many takes — reading exactly like an empty takes table, so + * agents concluded there were no takes and moved on. + * + * Fix: `list` is a real subcommand (CLI parity with the takes_list op). + * Bare `takes ` still lists per-page. + */ + +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; +import { PGLiteEngine } from '../src/core/pglite-engine.ts'; +import { runTakes } from '../src/commands/takes.ts'; + +let engine: PGLiteEngine; + +async function captureStdout(fn: () => Promise): Promise { + const lines: string[] = []; + const orig = console.log; + console.log = (...args: unknown[]) => { lines.push(args.join(' ')); }; + try { + await fn(); + } finally { + console.log = orig; + } + return lines.join('\n'); +} + +beforeAll(async () => { + engine = new PGLiteEngine(); + await engine.connect({}); + await engine.initSchema(); + await engine.putPage('companies/acme-example', { + type: 'company', + title: 'Acme Example', + compiled_truth: 'Acme Example is a test company.', + }); + const [row] = await engine.executeRaw<{ id: number }>( + `SELECT id FROM pages WHERE slug = 'companies/acme-example'`, + ); + await engine.addTakesBatch([{ + page_id: row.id, + row_num: 1, + claim: 'Acme will ship the widget by Q3.', + kind: 'bet', + holder: 'self', + weight: 0.7, + }]); +}); + +afterAll(async () => { + await engine.disconnect(); +}); + +describe('gbrain takes list (#2079)', () => { + test('`takes list` lists all takes instead of slug-ifying "list"', async () => { + const out = await captureStdout(() => runTakes(engine, ['list'])); + expect(out).not.toContain('No takes on list.'); + expect(out).toContain('Acme will ship the widget by Q3.'); + expect(out).toContain('companies/acme-example'); + }); + + test('`takes list --json` returns the full take rows', async () => { + const out = await captureStdout(() => runTakes(engine, ['list', '--json'])); + const parsed = JSON.parse(out); + expect(Array.isArray(parsed)).toBe(true); + expect(parsed.length).toBe(1); + expect(parsed[0].claim).toContain('Acme will ship'); + }); + + test('per-page form still works: `takes `', async () => { + const out = await captureStdout(() => runTakes(engine, ['companies/acme-example'])); + expect(out).toContain('# Takes on companies/acme-example'); + expect(out).toContain('Acme will ship the widget by Q3.'); + }); +});