@@ -7,24 +7,31 @@ import {
77 spyOn ,
88 test ,
99} from 'bun:test' ;
10- import { askQuestionsCommand } from './ask-questions.ts' ;
10+ import { mkdtemp , rm , writeFile } from 'node:fs/promises' ;
11+ import { tmpdir } from 'node:os' ;
12+ import { join } from 'node:path' ;
13+ import { askQuestionsCommand , parseQuestion } from './ask-questions.ts' ;
1114
1215// Note: These tests focus on CLI flag parsing and command structure
1316// They do NOT test the actual Mux API integration (that's tested via E2E)
1417
1518describe ( 'mux robots ask-questions' , ( ) => {
19+ let tempDir : string ;
1620 let exitSpy : Mock < typeof process . exit > ;
1721 let consoleErrorSpy : Mock < typeof console . error > ;
1822
19- beforeEach ( ( ) => {
23+ beforeEach ( async ( ) => {
24+ tempDir = await mkdtemp ( join ( tmpdir ( ) , 'mux-cli-robots-test-' ) ) ;
25+
2026 exitSpy = spyOn ( process , 'exit' ) . mockImplementation ( ( ( ) => {
2127 throw new Error ( 'process.exit called' ) ;
2228 } ) as never ) ;
2329
2430 consoleErrorSpy = spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } ) ;
2531 } ) ;
2632
27- afterEach ( ( ) => {
33+ afterEach ( async ( ) => {
34+ await rm ( tempDir , { recursive : true , force : true } ) ;
2835 exitSpy ?. mockRestore ( ) ;
2936 consoleErrorSpy ?. mockRestore ( ) ;
3037 } ) ;
@@ -71,6 +78,20 @@ describe('mux robots ask-questions', () => {
7178 . find ( ( o ) => o . name === 'json' ) ;
7279 expect ( opt ) . toBeDefined ( ) ;
7380 } ) ;
81+
82+ test ( 'has --wait flag' , ( ) => {
83+ const opt = askQuestionsCommand
84+ . getOptions ( )
85+ . find ( ( o ) => o . name === 'wait' ) ;
86+ expect ( opt ) . toBeDefined ( ) ;
87+ } ) ;
88+
89+ test ( 'has --file flag' , ( ) => {
90+ const opt = askQuestionsCommand
91+ . getOptions ( )
92+ . find ( ( o ) => o . name === 'file' ) ;
93+ expect ( opt ) . toBeDefined ( ) ;
94+ } ) ;
7495 } ) ;
7596
7697 describe ( 'Input validation' , ( ) => {
@@ -84,4 +105,103 @@ describe('mux robots ask-questions', () => {
84105 expect ( exitSpy ) . toHaveBeenCalled ( ) ;
85106 } ) ;
86107 } ) ;
108+
109+ describe ( 'parseQuestion helper' , ( ) => {
110+ test ( 'plain question without pipe yields no answer_options' , ( ) => {
111+ expect ( parseQuestion ( 'What is this?' ) ) . toEqual ( {
112+ question : 'What is this?' ,
113+ } ) ;
114+ } ) ;
115+
116+ test ( 'question with pipe splits answer_options on commas' , ( ) => {
117+ expect ( parseQuestion ( 'How many speakers?|one,two,three or more' ) ) . toEqual (
118+ {
119+ question : 'How many speakers?' ,
120+ answer_options : [ 'one' , 'two' , 'three or more' ] ,
121+ } ,
122+ ) ;
123+ } ) ;
124+
125+ test ( 'trims whitespace around question and options' , ( ) => {
126+ expect ( parseQuestion ( ' Q? | a , b , c ' ) ) . toEqual ( {
127+ question : 'Q?' ,
128+ answer_options : [ 'a' , 'b' , 'c' ] ,
129+ } ) ;
130+ } ) ;
131+
132+ test ( 'splits only on first pipe so options may contain pipes' , ( ) => {
133+ expect ( parseQuestion ( 'Choose?|a|b,c|d' ) ) . toEqual ( {
134+ question : 'Choose?' ,
135+ answer_options : [ 'a|b' , 'c|d' ] ,
136+ } ) ;
137+ } ) ;
138+
139+ test ( 'throws on empty question' , ( ) => {
140+ expect ( ( ) => parseQuestion ( '|a,b' ) ) . toThrow ( / q u e s t i o n / i) ;
141+ } ) ;
142+
143+ test ( 'throws when pipe is present but options list is empty' , ( ) => {
144+ expect ( ( ) => parseQuestion ( 'Q?|' ) ) . toThrow ( / a n s w e r _ o p t i o n s / i) ;
145+ } ) ;
146+ } ) ;
147+
148+ describe ( '--file mode' , ( ) => {
149+ test ( 'errors when config file does not exist' , async ( ) => {
150+ const configPath = join ( tempDir , 'nope.json' ) ;
151+ try {
152+ await askQuestionsCommand . parse ( [ 'asset_abc' , '--file' , configPath ] ) ;
153+ } catch ( _error ) {
154+ // Expected
155+ }
156+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
157+ const msg = consoleErrorSpy . mock . calls [ 0 ] ?. [ 0 ] ?? '' ;
158+ expect ( msg ) . toMatch ( / f i l e n o t f o u n d / i) ;
159+ } ) ;
160+
161+ test ( 'errors when config file is invalid JSON' , async ( ) => {
162+ const configPath = join ( tempDir , 'bad.json' ) ;
163+ await writeFile ( configPath , '{ bad' ) ;
164+ try {
165+ await askQuestionsCommand . parse ( [ 'asset_abc' , '--file' , configPath ] ) ;
166+ } catch ( _error ) {
167+ // Expected
168+ }
169+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
170+ const msg = consoleErrorSpy . mock . calls [ 0 ] ?. [ 0 ] ?? '' ;
171+ expect ( msg ) . toMatch ( / i n v a l i d j s o n / i) ;
172+ } ) ;
173+
174+ test ( 'errors when --file combined with --question' , async ( ) => {
175+ const configPath = join ( tempDir , 'config.json' ) ;
176+ await writeFile (
177+ configPath ,
178+ JSON . stringify ( {
179+ questions : [ { question : 'What is this?' } ] ,
180+ } ) ,
181+ ) ;
182+ try {
183+ await askQuestionsCommand . parse ( [
184+ 'asset_abc' ,
185+ '--file' ,
186+ configPath ,
187+ '--question' ,
188+ 'Other?' ,
189+ ] ) ;
190+ } catch ( _error ) {
191+ // Expected
192+ }
193+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
194+ const msg = consoleErrorSpy . mock . calls [ 0 ] ?. [ 0 ] ?? '' ;
195+ expect ( msg ) . toMatch ( / - - f i l e c a n n o t b e c o m b i n e d / i) ;
196+ } ) ;
197+
198+ test ( 'requires either --file or --question' , async ( ) => {
199+ try {
200+ await askQuestionsCommand . parse ( [ 'asset_abc' ] ) ;
201+ } catch ( _error ) {
202+ // Expected
203+ }
204+ expect ( exitSpy ) . toHaveBeenCalled ( ) ;
205+ } ) ;
206+ } ) ;
87207} ) ;
0 commit comments