@@ -644,6 +644,202 @@ func TestService_ExecSandbox(t *testing.T) {
644644 require .Contains (t , err .Error (), "completed before becoming ready" )
645645 })
646646
647+ t .Run ("prints run failure output to stderr on polling completion" , func (t * testing.T ) {
648+ setup := setupTest (t )
649+
650+ runID := "run-setup-failed"
651+
652+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
653+ return api.SandboxConnectionInfo {
654+ Sandboxable : false ,
655+ Polling : api.PollingResult {Completed : true },
656+ }, nil
657+ }
658+
659+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
660+ require .Equal (t , runID , id )
661+ return "# Failed task:\n \n - setup\n " , nil
662+ }
663+
664+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
665+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
666+ Command : []string {"echo" , "hello" },
667+ RunID : runID ,
668+ Json : true ,
669+ })
670+
671+ require .Error (t , err )
672+ require .Contains (t , err .Error (), "completed before becoming ready" )
673+ require .Contains (t , setup .mockStderr .String (), "Failed task" )
674+ })
675+
676+ t .Run ("prints run failure output to stderr when polling loop completes" , func (t * testing.T ) {
677+ setup := setupTest (t )
678+
679+ runID := "run-polling-failed"
680+ calls := atomic.Int32 {}
681+ backoff := 0
682+
683+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
684+ if calls .Add (1 ) == 1 {
685+ return api.SandboxConnectionInfo {
686+ Sandboxable : false ,
687+ Polling : api.PollingResult {Completed : false , BackoffMs : & backoff },
688+ }, nil
689+ }
690+ return api.SandboxConnectionInfo {
691+ Sandboxable : false ,
692+ Polling : api.PollingResult {Completed : true },
693+ }, nil
694+ }
695+
696+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
697+ require .Equal (t , runID , id )
698+ return "# Failed task:\n \n - setup\n " , nil
699+ }
700+
701+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
702+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
703+ Command : []string {"echo" , "hello" },
704+ RunID : runID ,
705+ Json : true ,
706+ })
707+
708+ require .Error (t , err )
709+ require .Contains (t , err .Error (), "completed before becoming ready" )
710+ require .Contains (t , setup .mockStderr .String (), "Failed task" )
711+ })
712+
713+ t .Run ("gracefully degrades when GetRunPrompt fails on polling completion" , func (t * testing.T ) {
714+ setup := setupTest (t )
715+
716+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
717+ return api.SandboxConnectionInfo {
718+ Sandboxable : false ,
719+ Polling : api.PollingResult {Completed : true },
720+ }, nil
721+ }
722+
723+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
724+ return "" , errors .New ("server unavailable" )
725+ }
726+
727+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
728+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
729+ Command : []string {"echo" , "hello" },
730+ RunID : "run-no-prompt" ,
731+ Json : true ,
732+ })
733+
734+ require .Error (t , err )
735+ require .Contains (t , err .Error (), "completed before becoming ready" )
736+ require .Empty (t , setup .mockStderr .String ())
737+ })
738+
739+ t .Run ("prints run failure output to stderr when GetSandboxConnectionInfo returns an error" , func (t * testing.T ) {
740+ setup := setupTest (t )
741+
742+ runID := "run-conn-error"
743+
744+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
745+ return api.SandboxConnectionInfo {}, errors .New ("This run or task is no longer available for sandbox" )
746+ }
747+
748+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
749+ require .Equal (t , runID , id )
750+ return "# Failed task:\n \n - preflight\n " , nil
751+ }
752+
753+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
754+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
755+ Command : []string {"echo" , "hello" },
756+ RunID : runID ,
757+ Json : true ,
758+ })
759+
760+ require .Error (t , err )
761+ require .Contains (t , err .Error (), "unable to get sandbox connection info" )
762+ require .Contains (t , setup .mockStderr .String (), "Failed task" )
763+ })
764+
765+ t .Run ("uses timed_out FailureReason for natural error message" , func (t * testing.T ) {
766+ setup := setupTest (t )
767+
768+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
769+ return api.SandboxConnectionInfo {
770+ Sandboxable : false ,
771+ Polling : api.PollingResult {Completed : true },
772+ FailureReason : "timed_out" ,
773+ }, nil
774+ }
775+
776+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
777+ return "" , nil
778+ }
779+
780+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
781+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
782+ Command : []string {"echo" , "hello" },
783+ RunID : "run-timed-out" ,
784+ Json : true ,
785+ })
786+
787+ require .Error (t , err )
788+ require .Contains (t , err .Error (), "timed out before becoming ready" )
789+ })
790+
791+ t .Run ("uses cancelled FailureReason for natural error message" , func (t * testing.T ) {
792+ setup := setupTest (t )
793+
794+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
795+ return api.SandboxConnectionInfo {
796+ Sandboxable : false ,
797+ Polling : api.PollingResult {Completed : true },
798+ FailureReason : "cancelled" ,
799+ }, nil
800+ }
801+
802+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
803+ return "" , nil
804+ }
805+
806+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
807+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
808+ Command : []string {"echo" , "hello" },
809+ RunID : "run-cancelled" ,
810+ Json : true ,
811+ })
812+
813+ require .Error (t , err )
814+ require .Contains (t , err .Error (), "was cancelled before becoming ready" )
815+ })
816+
817+ t .Run ("uses failed FailureReason for natural error message" , func (t * testing.T ) {
818+ setup := setupTest (t )
819+
820+ setup .mockAPI .MockGetSandboxConnectionInfo = func (id , token string ) (api.SandboxConnectionInfo , error ) {
821+ return api.SandboxConnectionInfo {
822+ Sandboxable : false ,
823+ Polling : api.PollingResult {Completed : true },
824+ FailureReason : "failed" ,
825+ }, nil
826+ }
827+
828+ setup .mockAPI .MockGetRunPrompt = func (id string ) (string , error ) {
829+ return "" , nil
830+ }
831+
832+ _ , err := setup .service .ExecSandbox (cli.ExecSandboxConfig {
833+ ConfigFile : setup .absConfig (".rwx/sandbox.yml" ),
834+ Command : []string {"echo" , "hello" },
835+ RunID : "run-failed" ,
836+ Json : true ,
837+ })
838+
839+ require .Error (t , err )
840+ require .Contains (t , err .Error (), "failed before becoming ready" )
841+ })
842+
647843 t .Run ("shows firewall hint when SSH connection times out" , func (t * testing.T ) {
648844 setup := setupTest (t )
649845
0 commit comments