@@ -89,6 +89,7 @@ test("CLI verbs are callable from moshscript in dry-run mode", async () => {
8989 assert . match ( output , / w o u l d r u n : m o s h c o d e m c p i n s t a l l h t t p s : \/ \/ e x a m p l e \. c o m \/ m c p / ) ;
9090} ) ;
9191
92+ // ai() verb — headless, non-interactive engine invocation.
9293test ( "aiExecArgs maps each engine to its headless invocation" , ( ) => {
9394 assert . deepEqual ( aiExecArgs ( "claude" , "hi" ) , [ "-p" , "hi" ] ) ;
9495 assert . deepEqual ( aiExecArgs ( "codex" , "hi" ) , [ "exec" , "hi" ] ) ;
@@ -109,3 +110,78 @@ test("ai() in dry-run narrates the engine invocation and returns empty string",
109110 assert . equal ( out , "" ) ;
110111 assert . match ( ctx . lines . join ( "\n" ) , / w o u l d r u n : c o d e x e x e c / ) ;
111112} ) ;
113+
114+ // R8: non-zero exits return { ok: false } instead of throwing, so scripts can
115+ // branch on outcomes without a try/catch.
116+ test ( "R8: a non-zero CLI exit returns { ok: false } instead of throwing" , async ( ) => {
117+ // Run a real `moshcode` command that will fail (unknown engine).
118+ // We use the actual moshcode binary via runMoshcode with a non-dry context.
119+ const lines = [ ] ;
120+ const ctx = { dryRun : false , out : ( l ) => lines . push ( l ) } ;
121+ // `moshcode agents nonexistent-engine-xyz` should exit non-zero.
122+ const result = runMoshcode ( "agents" , [ "nonexistent-engine-xyz-99" ] , ctx ) ;
123+ assert . equal ( result . ok , false , "non-zero exit should return ok: false" ) ;
124+ assert . ok ( result . code !== 0 , "should have a non-zero exit code" ) ;
125+ assert . equal ( typeof result . code , "number" ) ;
126+ } ) ;
127+
128+ test ( "R8: a non-zero exit does NOT crash a moshscript — script continues" , async ( ) => {
129+ const lines = [ ] ;
130+ // The script calls a failing CLI verb then continues to the next line.
131+ // Under the old throwing behavior, the second say() would never run.
132+ const result = await runScript (
133+ `const r = agents("nonexistent-engine-xyz-99");
134+ say("still alive after fail, ok=" + r.ok);` ,
135+ { commands : moshVocabulary ( ) , out : ( s ) => lines . push ( s ) }
136+ ) ;
137+ const output = lines . join ( "\n" ) ;
138+ assert . match ( output , / s t i l l a l i v e a f t e r f a i l , o k = f a l s e / ,
139+ "script should continue after a non-zero CLI exit" ) ;
140+ } ) ;
141+
142+ // shell() verb — the system verb for arbitrary shell commands.
143+ test ( "shell() is in the vocabulary" , ( ) => {
144+ assert . ok ( moshVocabulary ( ) . has ( "shell" ) , "expected shell() in the vocabulary" ) ;
145+ } ) ;
146+
147+ test ( "shell() in dry-run narrates the command without running it" , ( ) => {
148+ const ctx = dryCtx ( ) ;
149+ const cmd = moshVocabulary ( ) . get ( "shell" ) ;
150+ const result = cmd . run ( ctx , "echo hello" ) ;
151+ assert . equal ( result . ok , true ) ;
152+ assert . equal ( result . dryRun , true ) ;
153+ assert . match ( ctx . lines . join ( "\n" ) , / w o u l d r u n : .* e c h o h e l l o / ) ;
154+ } ) ;
155+
156+ test ( "shell() throws when called without arguments" , ( ) => {
157+ const ctx = dryCtx ( ) ;
158+ const cmd = moshVocabulary ( ) . get ( "shell" ) ;
159+ assert . throws ( ( ) => cmd . run ( ctx ) , / s h e l l \( \) r e q u i r e s a c o m m a n d s t r i n g / ) ;
160+ } ) ;
161+
162+ test ( "shell() runs a real command and returns { ok, code }" , ( ) => {
163+ const lines = [ ] ;
164+ const ctx = { dryRun : false , out : ( l ) => lines . push ( l ) } ;
165+ const cmd = moshVocabulary ( ) . get ( "shell" ) ;
166+ const result = cmd . run ( ctx , "true" ) ;
167+ assert . equal ( result . ok , true ) ;
168+ assert . equal ( result . code , 0 ) ;
169+ } ) ;
170+
171+ test ( "shell() returns { ok: false } on non-zero exit without throwing" , ( ) => {
172+ const lines = [ ] ;
173+ const ctx = { dryRun : false , out : ( l ) => lines . push ( l ) } ;
174+ const cmd = moshVocabulary ( ) . get ( "shell" ) ;
175+ const result = cmd . run ( ctx , "false" ) ;
176+ assert . equal ( result . ok , false ) ;
177+ assert . ok ( result . code !== 0 ) ;
178+ } ) ;
179+
180+ test ( "shell() is callable from moshscript and the script continues on failure" , async ( ) => {
181+ const lines = [ ] ;
182+ await runScript (
183+ `const r = shell("false"); say("continued, ok=" + r.ok);` ,
184+ { commands : moshVocabulary ( ) , out : ( s ) => lines . push ( s ) }
185+ ) ;
186+ assert . match ( lines . join ( "\n" ) , / c o n t i n u e d , o k = f a l s e / ) ;
187+ } ) ;
0 commit comments