diff --git a/README.md b/README.md index c71686c..2a8ddd9 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,16 @@ bkper group update "Cash" -b abc123 --hidden true bkper group delete "Cash" -b abc123 ``` +### Book setup guidance (important) + +When setting up a full Book or starter chart of accounts, prefer a hierarchical group structure by default. + +Create top-level groups first, then child groups with `--parent`, then accounts with `--groups`. + +Verify the resulting group hierarchy and account memberships before reporting success. + +Avoid using the same name for a Group and an Account in the same Book. +
Command reference @@ -448,7 +458,7 @@ CSV is significantly more token-efficient than JSON for tabular data, and for wi ### Batch Operations & Piping -Write commands (`account create`, `group create`, `transaction create`) accept JSON data piped via stdin for batch operations. The `transaction update` command also accepts stdin for batch updates. The input format follows the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts) exactly -- a single JSON object or an array of objects. +Write commands (`account create`, `transaction create`) accept JSON data piped via stdin for batch operations. The `transaction update` command also accepts stdin for batch updates. The input format follows the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts) exactly -- a single JSON object or an array of objects. ```bash # Create transactions @@ -465,15 +475,13 @@ echo '[{ echo '[{"name":"Cash","type":"ASSET"},{"name":"Revenue","type":"INCOMING"}]' | \ bkper account create -b abc123 -# Create groups -echo '[{"name":"Fixed Costs","hidden":true}]' | \ - bkper group create -b abc123 - # Pipe from a script python export_bank.py | bkper transaction create -b abc123 ``` -The input follows the exact `bkper.Transaction`, `bkper.Account`, or `bkper.Group` type from the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts). Custom properties go inside the `properties` object. +The input follows the exact `bkper.Transaction` or `bkper.Account` type from the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts). Custom properties go inside the `properties` object. + +Groups are created explicitly with `bkper group create --name` and optional `--parent` so hierarchy stays deterministic during setup. The `--property` CLI flag can override or delete properties from the stdin payload: @@ -491,21 +499,17 @@ bkper account create -b abc123 < accounts.json **Piping between commands:** -All JSON output is designed to be piped directly as stdin to other commands. The output of any list or batch create command can feed directly into a create or update command: +For resources that support stdin creation, JSON output can be piped directly into create or update commands: ```bash # Copy all accounts from one book to another bkper account list -b $BOOK_A --format json | bkper account create -b $BOOK_B -# Copy all groups from one book to another -bkper group list -b $BOOK_A --format json | bkper group create -b $BOOK_B - # Copy transactions matching a query bkper transaction list -b $BOOK_A -q "after:2025-01-01" --format json | \ bkper transaction create -b $BOOK_B -# Clone a full chart of accounts: groups, then accounts, then transactions -bkper group list -b $SOURCE --format json | bkper group create -b $DEST +# Clone accounts, then transactions bkper account list -b $SOURCE --format json | bkper account create -b $DEST bkper transaction list -b $SOURCE -q "after:2025-01-01" --format json | \ bkper transaction create -b $DEST @@ -555,15 +559,6 @@ Only the fields below are meaningful when creating or updating resources via std | `groups` | `[{"name":"..."}, ...]` | Groups to assign by name or id | | `properties` | `{"key": "value", ...}` | Custom key/value properties | -**Group** (`bkper.Group`) - -| Field | Type | Notes | -| ------------ | ---------------------------------- | -------------------------------- | -| `name` | `string` | Group name (required) | -| `hidden` | `boolean` | Hide from transactions main menu | -| `parent` | `{"name":"..."}` or `{"id":"..."}` | Parent group for nesting | -| `properties` | `{"key": "value", ...}` | Custom key/value properties | -
--- diff --git a/src/commands/groups/batch-create.ts b/src/commands/groups/batch-create.ts deleted file mode 100644 index 524cec3..0000000 --- a/src/commands/groups/batch-create.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { getBkperInstance } from '../../bkper-factory.js'; -import { Group } from 'bkper-js'; -import { parsePropertyFlag } from '../../utils/properties.js'; - -/** - * Creates multiple groups from stdin items using the batch API. - * Outputs a flat JSON array of all created groups. - * - * Stdin items must follow the bkper.Group format exactly. - * - * @param bookId - Target book ID - * @param items - Parsed stdin items as bkper.Group payloads - * @param propertyOverrides - CLI --property flags that override stdin properties - */ -export async function batchCreateGroups( - bookId: string, - items: Record[], - propertyOverrides?: string[] -): Promise { - const bkper = getBkperInstance(); - const book = await bkper.getBook(bookId); - - const groups: Group[] = []; - - for (const item of items) { - const group = new Group(book, item as bkper.Group); - - // CLI --property flags override stdin properties - if (propertyOverrides) { - for (const raw of propertyOverrides) { - const [key, value] = parsePropertyFlag(raw); - if (value === '') { - group.deleteProperty(key); - } else { - group.setProperty(key, value); - } - } - } - - groups.push(group); - } - - const results = await book.batchCreateGroups(groups); - const allResults = results.map(result => result.json()); - - console.log(JSON.stringify(allResults, null, 2)); -} diff --git a/src/commands/groups/index.ts b/src/commands/groups/index.ts index ca9a1eb..3ba5c68 100644 --- a/src/commands/groups/index.ts +++ b/src/commands/groups/index.ts @@ -3,4 +3,3 @@ export { getGroup } from './get.js'; export { createGroup, CreateGroupOptions } from './create.js'; export { updateGroup, UpdateGroupOptions } from './update.js'; export { deleteGroup } from './delete.js'; -export { batchCreateGroups } from './batch-create.js'; diff --git a/src/commands/groups/register.ts b/src/commands/groups/register.ts index 5b50837..20da87b 100644 --- a/src/commands/groups/register.ts +++ b/src/commands/groups/register.ts @@ -3,15 +3,7 @@ import { withAction } from '../action.js'; import { collectProperty } from '../cli-helpers.js'; import { renderListResult, renderItem } from '../../render/index.js'; import { validateRequiredOptions, throwIfErrors } from '../../utils/validation.js'; -import { parseStdinItems } from '../../input/index.js'; -import { - listGroupsFormatted, - getGroup, - createGroup, - updateGroup, - deleteGroup, - batchCreateGroups, -} from './index.js'; +import { listGroupsFormatted, getGroup, createGroup, updateGroup, deleteGroup } from './index.js'; export function registerGroupCommands(program: Command): void { const groupCommand = program.command('group').description('Manage Groups'); @@ -50,30 +42,19 @@ export function registerGroupCommands(program: Command): void { .option('-p, --property ', 'Set a property (repeatable)', collectProperty) .action(options => withAction('creating group', async format => { - const stdinData = !process.stdin.isTTY ? await parseStdinItems() : null; - - if (stdinData && stdinData.items.length > 0) { - throwIfErrors( - validateRequiredOptions(options, [{ name: 'book', flag: '--book' }]) - ); - await batchCreateGroups(options.book, stdinData.items, options.property); - } else if (stdinData && stdinData.items.length === 0) { - console.log(JSON.stringify([], null, 2)); - } else { - throwIfErrors( - validateRequiredOptions(options, [ - { name: 'book', flag: '--book' }, - { name: 'name', flag: '--name' }, - ]) - ); - const group = await createGroup(options.book, { - name: options.name, - parent: options.parent, - hidden: options.hidden, - property: options.property, - }); - renderItem(group.json(), format); - } + throwIfErrors( + validateRequiredOptions(options, [ + { name: 'book', flag: '--book' }, + { name: 'name', flag: '--name' }, + ]) + ); + const group = await createGroup(options.book, { + name: options.name, + parent: options.parent, + hidden: options.hidden, + property: options.property, + }); + renderItem(group.json(), format); })() ); diff --git a/src/docs-compliance/rules.ts b/src/docs-compliance/rules.ts index 574455b..5db6d5f 100644 --- a/src/docs-compliance/rules.ts +++ b/src/docs-compliance/rules.ts @@ -76,6 +76,36 @@ export function evaluateReadmeCompliance(content: string): ComplianceResult { }); } + if (!content.includes('### Book setup guidance (important)')) { + errors.push({ + code: 'missing-book-setup-guidance-title', + message: 'Missing `Book setup guidance (important)` section.', + }); + } + + if ( + !content.includes( + 'Create top-level groups first, then child groups with `--parent`, then accounts with `--groups`.' + ) + ) { + errors.push({ + code: 'missing-book-setup-order-guidance', + message: + 'Missing guidance to create top-level groups first, then child groups, then accounts.', + }); + } + + if ( + !content.includes( + 'Verify the resulting group hierarchy and account memberships before reporting success.' + ) + ) { + errors.push({ + code: 'missing-book-setup-verification-guidance', + message: 'Missing guidance to verify hierarchy and account memberships before success.', + }); + } + if (!content.includes('LLM-first output guidance (important):')) { errors.push({ code: 'missing-llm-guidance-title', @@ -111,5 +141,31 @@ export function evaluateReadmeCompliance(content: string): ComplianceResult { }); } + if ( + content.includes( + 'Write commands (`account create`, `group create`, `transaction create`) accept JSON data piped via stdin' + ) + ) { + errors.push({ + code: 'group-create-stdin-documented', + message: 'README should not document stdin batch creation for `group create`.', + }); + } + + const groupPipePattern = /bkper\s+group\s+list\b.*\|\s*bkper\s+group\s+create\b/; + if (groupPipePattern.test(content)) { + errors.push({ + code: 'group-create-pipe-documented', + message: 'README should not document piping group JSON into `group create`.', + }); + } + + if (content.includes('**Group** (`bkper.Group`)')) { + errors.push({ + code: 'group-stdin-fields-documented', + message: 'README should not document stdin writable fields for `bkper.Group`.', + }); + } + return {errors}; } diff --git a/test/integration/groups/group-stdin.test.ts b/test/integration/groups/group-stdin.test.ts index 53d2b1e..25e1534 100644 --- a/test/integration/groups/group-stdin.test.ts +++ b/test/integration/groups/group-stdin.test.ts @@ -32,7 +32,7 @@ describe('CLI - group stdin', function () { }); describe('JSON stdin', function () { - it('should create groups from JSON array', async function () { + it('should reject group creation from JSON stdin', async function () { const jsonInput = JSON.stringify([ { name: 'Stdin Group A' }, { name: 'Stdin Group B' }, @@ -40,33 +40,19 @@ describe('CLI - group stdin', function () { const result = await runBkperWithStdin(['group', 'create', '-b', bookId], jsonInput); - expect(result.exitCode).to.equal(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).to.be.an('array').with.length(2); - expect(parsed[0].name).to.equal('Stdin Group A'); - expect(parsed[1].name).to.equal('Stdin Group B'); - }); - - it('should create a single group from JSON object', async function () { - const jsonInput = JSON.stringify({ name: 'Stdin Group C' }); - - const result = await runBkperWithStdin(['group', 'create', '-b', bookId], jsonInput); - - expect(result.exitCode).to.equal(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).to.be.an('array').with.length(1); - expect(parsed[0].name).to.equal('Stdin Group C'); + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include('Missing required option: --name'); }); }); describe('verification', function () { - it('should list all created groups', async function () { + it('should not create groups from stdin input', async function () { const result = await runBkperJson(['group', 'list', '-b', bookId]); const names = result.map(g => g.name); - expect(names).to.include('Stdin Group A'); - expect(names).to.include('Stdin Group B'); - expect(names).to.include('Stdin Group C'); + expect(names).to.not.include('Stdin Group A'); + expect(names).to.not.include('Stdin Group B'); + expect(names).to.not.include('Stdin Group C'); }); }); }); diff --git a/test/integration/helpers/api-helpers.ts b/test/integration/helpers/api-helpers.ts index c6c40f3..ac78f36 100644 --- a/test/integration/helpers/api-helpers.ts +++ b/test/integration/helpers/api-helpers.ts @@ -184,7 +184,7 @@ export function runBkper( ); // Close stdin immediately so CLI commands that check for piped input - // (account create, group create, transaction create) don't hang + // (account create and transaction create) don't hang // waiting for data that will never come. if (child.stdin) { child.stdin.end(); diff --git a/test/integration/pipelines/pipeline.test.ts b/test/integration/pipelines/pipeline.test.ts index 4df0abd..d16069b 100644 --- a/test/integration/pipelines/pipeline.test.ts +++ b/test/integration/pipelines/pipeline.test.ts @@ -35,101 +35,37 @@ describe('CLI - pipeline (end-to-end)', function () { }); // ---------------------------------------------------------------- - // Groups: round-trip piping + // Groups: stdin is not supported for creation // ---------------------------------------------------------------- - describe('groups round-trip', function () { - it('should pipe group list output as group create input', async function () { - // Seed groups in book A - const seedResult = await runBkperWithStdin( - ['group', 'create', '-b', bookA], - JSON.stringify([{ name: 'Pipe Group Alpha' }, { name: 'Pipe Group Beta' }]) - ); - expect(seedResult.exitCode).to.equal(0); + describe('groups creation', function () { + it('should reject piping group JSON into group create', async function () { + await runBkper([ + 'group', + 'create', + '-b', + bookA, + '--name', + 'Pipe Group Alpha', + ]); + await runBkper([ + 'group', + 'create', + '-b', + bookA, + '--name', + 'Pipe Group Beta', + ]); - // List groups from book A as JSON const listResult = await runBkper(['--format', 'json', 'group', 'list', '-b', bookA]); expect(listResult.exitCode).to.equal(0); - const listedGroups = JSON.parse(listResult.stdout); - expect(listedGroups).to.be.an('array'); - expect(listedGroups.length).to.be.greaterThanOrEqual(2); - // Pipe list output into book B create const pipeResult = await runBkperWithStdin( ['group', 'create', '-b', bookB], listResult.stdout ); - expect(pipeResult.exitCode).to.equal(0); - - // Verify groups exist in book B - const verifyResult = await runBkperJson(['group', 'list', '-b', bookB]); - const names = verifyResult.map(g => g.name); - expect(names).to.include('Pipe Group Alpha'); - expect(names).to.include('Pipe Group Beta'); - }); - - it('should pipe group batch create output as group create input', async function () { - // Create groups in book A via batch - const createResult = await runBkperWithStdin( - ['group', 'create', '-b', bookA], - JSON.stringify([{ name: 'Batch Pipe Group X' }]) - ); - expect(createResult.exitCode).to.equal(0); - - // Parse batch output (flat JSON array) and pipe to book B - const parsed = JSON.parse(createResult.stdout); - expect(parsed).to.be.an('array').with.length(1); - - const pipeResult = await runBkperWithStdin( - ['group', 'create', '-b', bookB], - createResult.stdout - ); - expect(pipeResult.exitCode).to.equal(0); - - // Verify - const verifyResult = await runBkperJson(['group', 'list', '-b', bookB]); - const names = verifyResult.map(g => g.name); - expect(names).to.include('Batch Pipe Group X'); - }); - - it('should pipe single group get output as group create input', async function () { - // Get a group from book A - const groups = await runBkperJson(['group', 'list', '-b', bookA]); - const targetGroup = groups.find(g => g.name === 'Pipe Group Alpha'); - expect(targetGroup).to.exist; - - const getResult = await runBkper([ - '--format', - 'json', - 'group', - 'get', - targetGroup!.id!, - '-b', - bookA, - ]); - expect(getResult.exitCode).to.equal(0); - - // Pipe single group JSON into create (single object -> one-item batch) - // Create in a fresh book to avoid name collision - const bookC = await createTestBook(uniqueTestName('test-pipe-grp-get')); - try { - const pipeResult = await runBkperWithStdin( - ['group', 'create', '-b', bookC], - getResult.stdout - ); - expect(pipeResult.exitCode).to.equal(0); - - const verifyResult = await runBkperJson([ - 'group', - 'list', - '-b', - bookC, - ]); - const names = verifyResult.map(g => g.name); - expect(names).to.include('Pipe Group Alpha'); - } finally { - await deleteTestBook(bookC); - } + expect(pipeResult.exitCode).to.not.equal(0); + expect(pipeResult.stderr).to.include('Missing required option: --name'); }); }); @@ -493,7 +429,7 @@ describe('CLI - pipeline (end-to-end)', function () { // ---------------------------------------------------------------- describe('stdin { items: [...] } wrapper', function () { - it('should accept { items: [...] } wrapper for group create', async function () { + it('should reject { items: [...] } wrapper for group create', async function () { const bookC = await createTestBook(uniqueTestName('test-pipe-wrap-grp')); try { const wrappedInput = JSON.stringify({ @@ -504,16 +440,8 @@ describe('CLI - pipeline (end-to-end)', function () { ['group', 'create', '-b', bookC], wrappedInput ); - expect(result.exitCode).to.equal(0); - - const verifyResult = await runBkperJson([ - 'group', - 'list', - '-b', - bookC, - ]); - const names = verifyResult.map(g => g.name); - expect(names).to.include('Wrapped Group'); + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include('Missing required option: --name'); } finally { await deleteTestBook(bookC); } @@ -599,11 +527,10 @@ describe('CLI - pipeline (end-to-end)', function () { expect(parsed).to.be.an('array').with.length(0); }); - it('should handle empty array stdin gracefully for groups', async function () { + it('should reject empty array stdin for groups', async function () { const result = await runBkperWithStdin(['group', 'create', '-b', bookA], '[]'); - expect(result.exitCode).to.equal(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).to.be.an('array').with.length(0); + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include('Missing required option: --name'); }); it('should handle empty array stdin gracefully for transactions', async function () { @@ -613,14 +540,13 @@ describe('CLI - pipeline (end-to-end)', function () { expect(parsed).to.be.an('array').with.length(0); }); - it('should handle { items: [] } wrapper with empty items', async function () { + it('should reject { items: [] } wrapper for groups', async function () { const result = await runBkperWithStdin( ['group', 'create', '-b', bookA], JSON.stringify({ items: [] }) ); - expect(result.exitCode).to.equal(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).to.be.an('array').with.length(0); + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include('Missing required option: --name'); }); }); @@ -629,36 +555,50 @@ describe('CLI - pipeline (end-to-end)', function () { // ---------------------------------------------------------------- describe('cross-resource workflow', function () { - it('should pipe groups, accounts, and transactions across books', async function () { + it('should copy groups explicitly, then pipe accounts and transactions across books', async function () { const sourceBook = await createTestBook(uniqueTestName('test-pipe-src')); const destBook = await createTestBook(uniqueTestName('test-pipe-dest')); try { - // Step 1: Create groups in source - const grpCreate = await runBkperWithStdin( - ['group', 'create', '-b', sourceBook], - JSON.stringify([{ name: 'XFlow Assets' }, { name: 'XFlow Income' }]) - ); - expect(grpCreate.exitCode).to.equal(0); - - // Step 2: Pipe groups from source to dest - const grpList = await runBkper([ - '--format', - 'json', + // Step 1: Create matching groups explicitly in both books + const sourceAssets = await runBkper([ 'group', - 'list', + 'create', '-b', sourceBook, + '--name', + 'XFlow Assets', ]); - expect(grpList.exitCode).to.equal(0); - - const grpPipe = await runBkperWithStdin( - ['group', 'create', '-b', destBook], - grpList.stdout - ); - expect(grpPipe.exitCode).to.equal(0); + expect(sourceAssets.exitCode).to.equal(0); + const sourceIncome = await runBkper([ + 'group', + 'create', + '-b', + sourceBook, + '--name', + 'XFlow Income', + ]); + expect(sourceIncome.exitCode).to.equal(0); + const destAssets = await runBkper([ + 'group', + 'create', + '-b', + destBook, + '--name', + 'XFlow Assets', + ]); + expect(destAssets.exitCode).to.equal(0); + const destIncome = await runBkper([ + 'group', + 'create', + '-b', + destBook, + '--name', + 'XFlow Income', + ]); + expect(destIncome.exitCode).to.equal(0); - // Step 3: Create accounts in source + // Step 2: Create accounts in source const acctCreate = await runBkperWithStdin( ['account', 'create', '-b', sourceBook], JSON.stringify([ @@ -668,7 +608,7 @@ describe('CLI - pipeline (end-to-end)', function () { ); expect(acctCreate.exitCode).to.equal(0); - // Step 4: Pipe accounts from source to dest + // Step 3: Pipe accounts from source to dest const acctList = await runBkper([ '--format', 'json', @@ -685,7 +625,7 @@ describe('CLI - pipeline (end-to-end)', function () { ); expect(acctPipe.exitCode).to.equal(0); - // Step 5: Create transactions in source + // Step 4: Create transactions in source const txCreate = await runBkperWithStdin( ['transaction', 'create', '-b', sourceBook], JSON.stringify([ @@ -707,7 +647,7 @@ describe('CLI - pipeline (end-to-end)', function () { ); expect(txCreate.exitCode).to.equal(0); - // Step 6: Pipe transactions from source to dest + // Step 5: Pipe transactions from source to dest const txList = await runBkper([ '--format', 'json', @@ -726,7 +666,7 @@ describe('CLI - pipeline (end-to-end)', function () { ); expect(txPipe.exitCode).to.equal(0); - // Step 7: Verify everything in dest + // Step 6: Verify everything in dest const destGroups = await runBkperJson([ 'group', 'list', @@ -819,16 +759,15 @@ describe('CLI - pipeline (end-to-end)', function () { } }); - it('should output batch group create as a flat JSON array', async function () { + it('should reject batch group create via stdin', async function () { const bookC = await createTestBook(uniqueTestName('test-fmt-grp')); try { const result = await runBkperWithStdin( ['group', 'create', '-b', bookC], JSON.stringify([{ name: 'Fmt Group' }]) ); - expect(result.exitCode).to.equal(0); - const parsed = JSON.parse(result.stdout); - expect(parsed).to.be.an('array'); + expect(result.exitCode).to.not.equal(0); + expect(result.stderr).to.include('Missing required option: --name'); } finally { await deleteTestBook(bookC); } diff --git a/test/integration/transactions/transaction-commands.test.ts b/test/integration/transactions/transaction-commands.test.ts index 4ff4748..bcb0168 100644 --- a/test/integration/transactions/transaction-commands.test.ts +++ b/test/integration/transactions/transaction-commands.test.ts @@ -471,30 +471,48 @@ describe('CLI - transaction commands', function () { expect(result.exitCode).to.not.equal(0); }); - it('should fail when missing required --date option', async function () { + it('should create a draft transaction when --date is missing', async function () { const result = await runBkper([ + '--format', + 'json', 'transaction', 'create', '-b', bookId, '--amount', '100', + '--description', + 'Draft without date', ]); - expect(result.exitCode).to.not.equal(0); + expect(result.exitCode).to.equal(0); + const created = JSON.parse(result.stdout) as bkper.Transaction; + expect(created).to.be.an('object'); + expect(created.id).to.be.a('string'); + expect(created.description).to.equal('Draft without date'); + expect(created.posted).to.not.equal(true); }); - it('should fail when missing required --amount option', async function () { + it('should create a draft transaction when --amount is missing', async function () { const result = await runBkper([ + '--format', + 'json', 'transaction', 'create', '-b', bookId, '--date', '2025-01-01', + '--description', + 'Draft without amount', ]); - expect(result.exitCode).to.not.equal(0); + expect(result.exitCode).to.equal(0); + const created = JSON.parse(result.stdout) as bkper.Transaction; + expect(created).to.be.an('object'); + expect(created.id).to.be.a('string'); + expect(created.description).to.equal('Draft without amount'); + expect(created.posted).to.not.equal(true); }); it('should fail when missing required --query option for list', async function () { diff --git a/test/unit/commands/groups/batch-create.test.ts b/test/unit/commands/groups/batch-create.test.ts deleted file mode 100644 index 8b4d962..0000000 --- a/test/unit/commands/groups/batch-create.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { expect, setupTestEnvironment } from '../../helpers/test-setup.js'; -import { setMockBkper } from '../../helpers/mock-factory.js'; - -// Import after mock setup -const { batchCreateGroups } = await import('../../../../src/commands/groups/batch-create.js'); - -describe('CLI - group batch-create Command', function () { - let mockBook: any; - let batchCalls: any[][]; - let consoleOutput: string[]; - - beforeEach(function () { - setupTestEnvironment(); - batchCalls = []; - consoleOutput = []; - - const originalLog = console.log; - console.log = (...args: any[]) => { - consoleOutput.push(args.join(' ')); - }; - - mockBook = { - getGroup: async (name: string) => ({ - getId: () => `${name}-id`, - getName: () => name, - }), - batchCreateGroups: async (groups: any[]) => { - batchCalls.push(groups); - return groups.map((g: any, idx: number) => ({ - json: () => ({ id: `grp-${idx}`, name: g.getName() }), - })); - }, - }; - - setMockBkper({ - setConfig: () => {}, - getBook: async () => mockBook, - }); - - afterEach(() => { - console.log = originalLog; - }); - }); - - it('should create a single group', async function () { - await batchCreateGroups('book-123', [{ name: 'Assets' }]); - - expect(batchCalls).to.have.length(1); - expect(batchCalls[0]).to.have.length(1); - expect(batchCalls[0][0].getName()).to.equal('Assets'); - }); - - it('should output flat JSON array for created groups', async function () { - await batchCreateGroups('book-123', [{ name: 'Assets' }, { name: 'Liabilities' }]); - - expect(consoleOutput).to.have.length(1); - const parsed = JSON.parse(consoleOutput[0]); - expect(parsed).to.be.an('array').with.length(2); - expect(parsed[0]).to.have.property('name'); - expect(parsed[1]).to.have.property('name'); - }); - - it('should send all items in a single batch call', async function () { - const items = Array.from({ length: 150 }, (_, i) => ({ name: `Group-${i}` })); - await batchCreateGroups('book-123', items); - - expect(batchCalls).to.have.length(1); - expect(batchCalls[0]).to.have.length(150); - }); - - it('should set properties from stdin payload', async function () { - await batchCreateGroups('book-123', [{ name: 'Test', properties: { color: 'red' } }]); - - expect(batchCalls).to.have.length(1); - const group = batchCalls[0][0]; - expect(group.getProperty('color')).to.equal('red'); - }); - - it('should apply property overrides from CLI flags', async function () { - await batchCreateGroups('book-123', [{ name: 'Test' }], ['region=EU']); - - expect(batchCalls).to.have.length(1); - const group = batchCalls[0][0]; - expect(group.getProperty('region')).to.equal('EU'); - }); - - it('should set hidden status when provided as boolean', async function () { - await batchCreateGroups('book-123', [{ name: 'Internal', hidden: true }]); - - expect(batchCalls).to.have.length(1); - const group = batchCalls[0][0]; - expect(group.isHidden()).to.be.true; - }); - - it('should reject hidden as string (must be boolean)', async function () { - await batchCreateGroups('book-123', [{ name: 'Internal', hidden: 'true' as any }]); - - expect(batchCalls).to.have.length(1); - const group = batchCalls[0][0]; - // String 'true' is truthy but not strictly boolean true - expect(group.isHidden()).to.equal('true'); - }); -}); diff --git a/test/unit/docs-compliance/rules.test.ts b/test/unit/docs-compliance/rules.test.ts index 1cf6bdf..8334e01 100644 --- a/test/unit/docs-compliance/rules.test.ts +++ b/test/unit/docs-compliance/rules.test.ts @@ -5,6 +5,10 @@ import { evaluateReadmeCompliance } from '../../../src/docs-compliance/rules.js' describe('docs-compliance rules', function () { it('should pass when README content follows required rules', function () { const readme = ` +### Book setup guidance (important) +Create top-level groups first, then child groups with \`--parent\`, then accounts with \`--groups\`. +Verify the resulting group hierarchy and account memberships before reporting success. + ### Query semantics (transactions and balances) - \`on:2025\` → full year - \`after:\` is **inclusive** and \`before:\` is **exclusive**. @@ -69,10 +73,41 @@ bkper balance list -b abc123 -q "on:2025-12-31" const result = evaluateReadmeCompliance(readme); const codes = result.errors.map(e => e.code); + expect(codes).to.include('missing-book-setup-guidance-title'); + expect(codes).to.include('missing-book-setup-order-guidance'); + expect(codes).to.include('missing-book-setup-verification-guidance'); expect(codes).to.include('missing-llm-guidance-title'); expect(codes).to.include('missing-csv-guidance'); expect(codes).to.include('missing-json-guidance'); expect(codes).to.include('missing-query-semantics-section'); expect(codes).to.include('missing-after-before-semantics'); }); + + it('should report when README documents group stdin batch creation', function () { + const readme = ` +### Book setup guidance (important) +Create top-level groups first, then child groups with \`--parent\`, then accounts with \`--groups\`. +Verify the resulting group hierarchy and account memberships before reporting success. + +### Query semantics (transactions and balances) +- \`after:\` is **inclusive** and \`before:\` is **exclusive**. + +**LLM-first output guidance (important):** +- **LLM consumption of lists/reports** → CSV +- **Programmatic processing / pipelines** → JSON + +Write commands (\`account create\`, \`group create\`, \`transaction create\`) accept JSON data piped via stdin. +\`\`\`bash +bkper group list -b $BOOK_A --format json | bkper group create -b $BOOK_B +\`\`\` + +**Group** (\`bkper.Group\`) +`; + + const result = evaluateReadmeCompliance(readme); + const codes = result.errors.map(e => e.code); + expect(codes).to.include('group-create-stdin-documented'); + expect(codes).to.include('group-create-pipe-documented'); + expect(codes).to.include('group-stdin-fields-documented'); + }); });