@@ -57,6 +57,168 @@ public void testCommandResult() {
5757
5858 }
5959
60+ // --- CommandResult propagation tests (#604) ---
61+
62+ @ Test
63+ public void testUsageErrorResult () {
64+ // A command with a required option, invoked without it, should return USAGE_ERROR
65+ CommandResult result = AeshRuntimeRunner .builder ()
66+ .command (RequiredOptionCommand .class )
67+ .execute ();
68+ assertEquals (CommandResult .USAGE_ERROR .getResultValue (), result .getResultValue ());
69+ }
70+
71+ @ Test
72+ public void testCommandNotFoundResult () throws Exception {
73+ // CommandNotFoundException propagates from CommandRuntime and is mapped to
74+ // COMMAND_NOT_FOUND (127) by AeshRuntimeRunner. Verify the constant and that
75+ // CommandRuntime throws when the command name is unknown.
76+ assertEquals (127 , CommandResult .COMMAND_NOT_FOUND .getResultValue ());
77+ assertEquals (127 , CommandResult .COMMAND_NOT_FOUND .getExitCode ());
78+ assertTrue ("COMMAND_NOT_FOUND is failure" , CommandResult .COMMAND_NOT_FOUND .isFailure ());
79+
80+ org .aesh .command .CommandRuntime <CommandInvocation > runtime = org .aesh .command .AeshCommandRuntimeBuilder .builder ()
81+ .commandRegistry (org .aesh .command .impl .registry .AeshCommandRegistryBuilder .builder ()
82+ .command (Bar1Command .class )
83+ .create ())
84+ .build ();
85+
86+ try {
87+ runtime .executeCommand ("nonexistent" );
88+ assertTrue ("Should have thrown CommandNotFoundException" , false );
89+ } catch (org .aesh .command .CommandNotFoundException e ) {
90+ // correct — AeshRuntimeRunner maps this to COMMAND_NOT_FOUND (127)
91+ }
92+ }
93+
94+ @ Test
95+ public void testCustomExitCodeResult () {
96+ // A command returning a custom exit code (42) should propagate it
97+ CommandResult result = AeshRuntimeRunner .builder ()
98+ .command (CustomExitCodeCommand .class )
99+ .execute ();
100+ assertEquals (42 , result .getResultValue ());
101+ assertEquals (42 , result .getExitCode ());
102+ assertTrue ("Non-zero exit code is failure" , result .isFailure ());
103+ }
104+
105+ @ Test
106+ public void testNullReturnNormalizedToSuccess () {
107+ // A command returning null should be treated as SUCCESS
108+ CommandResult result = AeshRuntimeRunner .builder ()
109+ .command (NullReturnCommand .class )
110+ .execute ();
111+ assertEquals (CommandResult .SUCCESS .getResultValue (), result .getResultValue ());
112+ assertTrue ("null return should be SUCCESS" , result .isSuccess ());
113+ }
114+
115+ @ Test
116+ public void testMultiLineShortCircuitsOnNonZero () throws Exception {
117+ // executeCommand(String... lines) should stop on any non-zero result (#604)
118+ // Using CommandRuntime directly to exercise the multi-line overload
119+ MultiLineTrackingCommand .reset ();
120+
121+ org .aesh .command .CommandRuntime <CommandInvocation > runtime = org .aesh .command .AeshCommandRuntimeBuilder .builder ()
122+ .commandRegistry (org .aesh .command .impl .registry .AeshCommandRegistryBuilder .builder ()
123+ .command (MultiLineTrackingCommand .class )
124+ .create ())
125+ .build ();
126+
127+ // First command returns custom code 5 (non-zero) — second should NOT run
128+ CommandResult result = runtime .executeCommand (
129+ "multitrack --code 5" ,
130+ "multitrack --code 0" );
131+
132+ assertEquals (5 , result .getResultValue ());
133+ assertTrue ("Should have stopped after non-zero result" , result .isFailure ());
134+ assertEquals ("Only first command should have executed" , 1 , MultiLineTrackingCommand .callCount );
135+ }
136+
137+ @ Test
138+ public void testMultiLineContinuesOnSuccess () throws Exception {
139+ // executeCommand(String... lines) should continue when all commands succeed
140+ MultiLineTrackingCommand .reset ();
141+
142+ org .aesh .command .CommandRuntime <CommandInvocation > runtime = org .aesh .command .AeshCommandRuntimeBuilder .builder ()
143+ .commandRegistry (org .aesh .command .impl .registry .AeshCommandRegistryBuilder .builder ()
144+ .command (MultiLineTrackingCommand .class )
145+ .create ())
146+ .build ();
147+
148+ CommandResult result = runtime .executeCommand (
149+ "multitrack --code 0" ,
150+ "multitrack --code 0" ,
151+ "multitrack --code 0" );
152+
153+ assertEquals (0 , result .getResultValue ());
154+ assertTrue ("All succeeded" , result .isSuccess ());
155+ assertEquals ("All three commands should have executed" , 3 , MultiLineTrackingCommand .callCount );
156+ }
157+
158+ @ Test
159+ public void testExitCodeClamping () {
160+ // getExitCode() clamps to POSIX 0-255 range
161+ assertEquals (0 , CommandResult .SUCCESS .getExitCode ());
162+ assertEquals (1 , CommandResult .FAILURE .getExitCode ());
163+ assertEquals (2 , CommandResult .USAGE_ERROR .getExitCode ());
164+ assertEquals (127 , CommandResult .COMMAND_NOT_FOUND .getExitCode ());
165+ assertEquals (130 , CommandResult .INTERRUPTED .getExitCode ());
166+
167+ // Negative values clamp to 1
168+ assertEquals (1 , CommandResult .valueOf (-1 ).getExitCode ());
169+ // Values > 255 clamp to 255
170+ assertEquals (255 , CommandResult .valueOf (256 ).getExitCode ());
171+ // Values in range pass through
172+ assertEquals (42 , CommandResult .valueOf (42 ).getExitCode ());
173+ }
174+
175+ // --- Helper commands for CommandResult tests ---
176+
177+ @ CommandDefinition (name = "required-opt" , description = "requires --name" )
178+ public static class RequiredOptionCommand implements Command <CommandInvocation > {
179+ @ Option (name = "name" , required = true )
180+ private String name ;
181+
182+ @ Override
183+ public CommandResult execute (CommandInvocation ci ) {
184+ return CommandResult .SUCCESS ;
185+ }
186+ }
187+
188+ @ CommandDefinition (name = "custom-exit" , description = "returns exit code 42" )
189+ public static class CustomExitCodeCommand implements Command <CommandInvocation > {
190+ @ Override
191+ public CommandResult execute (CommandInvocation ci ) {
192+ return CommandResult .valueOf (42 );
193+ }
194+ }
195+
196+ @ CommandDefinition (name = "null-return" , description = "returns null (should be SUCCESS)" )
197+ public static class NullReturnCommand implements Command <CommandInvocation > {
198+ @ Override
199+ public CommandResult execute (CommandInvocation ci ) {
200+ return null ;
201+ }
202+ }
203+
204+ @ CommandDefinition (name = "multitrack" , description = "tracks calls, returns specified code" )
205+ public static class MultiLineTrackingCommand implements Command <CommandInvocation > {
206+ @ Option (name = "code" , description = "exit code to return" , defaultValue = "0" )
207+ private int code ;
208+
209+ static int callCount = 0 ;
210+
211+ static void reset () {
212+ callCount = 0 ;
213+ }
214+
215+ @ Override
216+ public CommandResult execute (CommandInvocation ci ) {
217+ callCount ++;
218+ return CommandResult .valueOf (code );
219+ }
220+ }
221+
60222 @ Test
61223 public void testInstantiatedCommand () {
62224 Bar1Command bar1Cmd = new Bar1Command ();
0 commit comments