Skip to content

Commit 2dffc46

Browse files
docs(audiobook-curator): inventory --report and convert --receipt are optional since #734; test both paths (#738)
* docs(audiobook-curator): document optional --report/--receipt migration; test both paths (#725 follow-up) * changeset: point at #738 * review: report/receipt wording; assert directory contents and identical stderr * docs: receipt is written when the command succeeds
1 parent 4036e89 commit 2dffc46

5 files changed

Lines changed: 99 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"agent-bundle": patch
3+
---
4+
5+
Document the `audiobook-curator` example's CLI migration accurately: since the `<tool>.cli.ts` projections replaced the `src/cli/` tree (#734), `inventory --report` and `convert --receipt` are optional, as they are on the tools, instead of required; a command run without one writes no report or receipt file, and exit codes, `--apply` gating, and error output are unchanged. (#738)

‎examples/audiobook-curator/README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,12 @@ result-schema-validated JSON value followed by a newline: the canonical final
166166
`Agent.Result` value, never the Markdown presentation or an intermediate
167167
Suspense fallback, and byte for byte the `structuredContent` of the tool call.
168168
`--report` and `--receipt` are optional on the command line exactly as they are
169-
on the tool; a command that gets one still writes the receipt file.
169+
on the tool; a command that gets one still writes the receipt file
170+
when it succeeds. This is a
171+
behavior change from the retired `src/cli/` tree, where `inventory --report`
172+
and `convert --receipt` were required: both commands now run without a receipt
173+
path and write no report or receipt file, and their exit codes, `--apply` gating, and error
174+
output are unchanged either way.
170175

171176
Each tool module declares its `inputSchema` as an inline zod literal, because
172177
the argv projection is compiled statically from that literal; it is the only

‎examples/audiobook-curator/tests/route-unit/cli-dispatch.test.ts‎

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
1+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

@@ -10,6 +10,7 @@ import { inputSchema as convertAudiobookInputSchema } from '../../src/mcp/curato
1010
import { inputSchema as inspectInputSchema, resultSchema as inspectResultSchema } from '../../src/mcp/curator/tools/inspect_sources.tsx';
1111
import { resultSchema as inventoryResultSchema } from '../../src/mcp/curator/tools/inventory_sources.tsx';
1212
import { resultSchema as audibleSearchResultSchema } from '../../src/mcp/curator/tools/search_audible.tsx';
13+
import { resultSchema as audibleSelectResultSchema } from '../../src/mcp/curator/tools/select_audible_edition.tsx';
1314
import { discoveryOperations } from '../../src/operations/discovery.ts';
1415

1516
const directories: string[] = [];
@@ -148,6 +149,22 @@ describe('audiobook-curator at the CLI dispatch proof level', () => {
148149
expect(inventoryResultSchema.parse(JSON.parse(await readFile(report, 'utf8')))).toEqual(receipt);
149150
});
150151

152+
it('runs inventory without --report, as the tool allows, and writes no report file', async () => {
153+
// Migration note (#734): the retired `src/cli/inventory.tsx` required
154+
// `--report`; the projected command shares the tool's optional field.
155+
const { directory, library } = await temporaryLibrary();
156+
const run = await invokeCli(['inventory', library, '--strict', '--json']);
157+
expect(await readdir(directory)).toEqual(['library']);
158+
const receipt = inventoryResultSchema.parse(cliJson(run));
159+
const tool = await invokeMcpTool('inventory_sources', { input: { source: library, strict: true } });
160+
161+
expect(run.exitCode).toBe(0);
162+
expect(receipt).toMatchObject({ exitCode: 0, operation: 'inventory', summary: { errors: 0, files: 0 } });
163+
expect(run.value).toEqual(receipt);
164+
expect(tool.isError).toBe(false);
165+
expect(withoutGeneratedAt(tool.structuredContent)).toEqual(withoutGeneratedAt(receipt));
166+
});
167+
151168
it('uses a failing inventory receipt exit code as the process exit code without ffprobe', async () => {
152169
const { directory, library, report } = await temporaryLibrary();
153170
await writeFile(join(library, 'broken.mp3'), 'not audio');
@@ -358,6 +375,15 @@ describe('audiobook-curator at the CLI dispatch proof level', () => {
358375
expect(planned.stderr).not.toContain('--yes');
359376
expect(planned.value).toBeUndefined();
360377

378+
// Migration note (#734): the retired `src/cli/convert.tsx` required
379+
// `--receipt`; the projected command shares the tool's optional field.
380+
// With or without it, the failure is the same and no receipt is written.
381+
const receipt = join(directory, 'convert-receipt.json');
382+
const withReceipt = await invokeCli([...argv, '--receipt', receipt, '--json']);
383+
expect(withReceipt.exitCode).toBe(1);
384+
expect(withReceipt.stderr).toBe(planned.stderr);
385+
await expect(readFile(receipt, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
386+
361387
// The projection declares confirm: false, so --yes is not an option here.
362388
const confirmed = await invokeCli([...argv, '--yes']);
363389
expect(confirmed.exitCode).toBe(2);
@@ -370,6 +396,60 @@ describe('audiobook-curator at the CLI dispatch proof level', () => {
370396
});
371397
});
372398

399+
describe('receipt paths are optional on the projected commands', () => {
400+
const candidateReport = async (): Promise<{ readonly candidates: string; readonly directory: string }> => {
401+
const { directory } = await temporaryLibrary();
402+
const candidates = join(directory, 'candidates.json');
403+
await writeFile(candidates, JSON.stringify({
404+
candidates: [{
405+
asin: 'B0CURATOR01',
406+
authors: [{ name: 'Ada Author' }],
407+
evidence: { authorMatch: true, languageMatch: true, narratorMatch: true, score: 100, strictIdentityMatch: true, titleMatch: true, unabridged: true },
408+
narrators: [{ name: 'Nora Narrator' }],
409+
region: 'us',
410+
title: 'The Selected Edition',
411+
}],
412+
errors: [],
413+
exitCode: 0,
414+
generatedAt: '2026-09-02T18:00:00.000Z',
415+
humanReviewRequired: true,
416+
mutation: false,
417+
operation: 'audible-search',
418+
query: { title: 'The Selected Edition' },
419+
reviewNote: 'Choose the matching edition.',
420+
}));
421+
return { candidates, directory };
422+
};
423+
424+
it('records an Audible selection with and without --receipt and writes the file only when asked', async () => {
425+
const { candidates, directory } = await candidateReport();
426+
const receiptPath = join(directory, 'selection.json');
427+
const argv = ['audible-select', '--candidate', '1', '--candidates', candidates, '--json'];
428+
429+
const without = await invokeCli(argv);
430+
expect(await readdir(directory)).toEqual(['candidates.json', 'library']);
431+
const withReceipt = await invokeCli([...argv, '--receipt', receiptPath]);
432+
const tool = await invokeMcpTool('select_audible_edition', { input: { candidate: 1, candidates } });
433+
434+
for (const run of [without, withReceipt]) {
435+
expect(run.exitCode).toBe(0);
436+
expect(run.stderr).toBe('');
437+
expect(run.routeId).toBe('tool:curator/select_audible_edition');
438+
expect(audibleSelectResultSchema.parse(cliJson(run))).toMatchObject({
439+
candidateNumber: 1,
440+
humanReviewed: true,
441+
mutation: false,
442+
operation: 'audible-select',
443+
selected: { asin: 'B0CURATOR01' },
444+
});
445+
}
446+
expect(withoutGeneratedAt(cliJson(withReceipt))).toEqual(withoutGeneratedAt(cliJson(without)));
447+
expect(withoutGeneratedAt(tool.structuredContent)).toEqual(withoutGeneratedAt(cliJson(without)));
448+
expect(audibleSelectResultSchema.parse(JSON.parse(await readFile(receiptPath, 'utf8')))).toEqual(withReceipt.value);
449+
expect((await readdir(directory)).filter((name) => name.endsWith('.json')).sort()).toEqual(['candidates.json', 'selection.json']);
450+
});
451+
});
452+
373453
describe('the rendered library-audit command', () => {
374454
it('emits exactly one final Markdown document when stdout is piped', async () => {
375455
const { report, run } = await invokeLibraryAudit();

‎website/docs/en/examples/audiobook-curator.mdx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ emits one result-schema-validated JSON value followed by a newline: the canonica
7676
`Agent.Result` value, never the Markdown presentation and never an intermediate Suspense fallback,
7777
and byte for byte the `structuredContent` of the tool call. `--report` and `--receipt` are
7878
optional on the command line exactly as they are on the tool; a command that gets one still
79-
writes the receipt file.
79+
writes the receipt file when it succeeds. This is a behavior change from the retired `src/cli/` tree, where
80+
`inventory --report` and `convert --receipt` were required: both commands now run without a
81+
receipt path and write no report or receipt file, and their exit codes, `--apply` gating, and error output are
82+
unchanged either way.
8083

8184
## Working in the workspace
8285

‎website/docs/zh/examples/audiobook-curator.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ description: '有声书策展器示例:一个由路由模块、请求上下文
6161
最终的 Markdown 文档——与 MCP 客户端作为文本内容收到的是同一份标题与报告。`--json` 选择机器输出,
6262
并输出一个经结果 schema 校验的 JSON 值加一个换行:这是最终的规范 `Agent.Result` 值,绝不是 Markdown
6363
表现,也绝不是中间的 Suspense 回退,并且与工具调用的 `structuredContent` 逐字节一致。`--report` 与
64-
`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令仍会写出收据文件。
64+
`--receipt` 在命令行上和在工具上一样是可选的;传入了的命令在成功时仍会写出收据文件。这相对于已移除的
65+
`src/cli/` 树是一处行为变化:那里的 `inventory --report` 与 `convert --receipt` 是必填的;现在这两条命令
66+
不带收据路径也能运行且不写报告或收据文件,而退出码、`--apply` 把关与错误输出在两种情况下都不变。
6567

6668
## 在工作区中开发
6769

0 commit comments

Comments
 (0)