1- import { mkdir , mkdtemp , readFile , rm , writeFile } from 'node:fs/promises' ;
1+ import { mkdir , mkdtemp , readdir , readFile , rm , writeFile } from 'node:fs/promises' ;
22import { tmpdir } from 'node:os' ;
33import { join } from 'node:path' ;
44
@@ -10,6 +10,7 @@ import { inputSchema as convertAudiobookInputSchema } from '../../src/mcp/curato
1010import { inputSchema as inspectInputSchema , resultSchema as inspectResultSchema } from '../../src/mcp/curator/tools/inspect_sources.tsx' ;
1111import { resultSchema as inventoryResultSchema } from '../../src/mcp/curator/tools/inventory_sources.tsx' ;
1212import { 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' ;
1314import { discoveryOperations } from '../../src/operations/discovery.ts' ;
1415
1516const 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 ( ) ;
0 commit comments