@@ -610,11 +610,12 @@ describe("CLI dispatch", () => {
610610 expect ( saved . sandboxes . alpha ) . toBeUndefined ( ) ;
611611 } ) ;
612612
613- it ( "list is read-only and does not recover from session or gateway" , ( ) => {
614- const home = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "nemoclaw-cli-list-no-recover-" ) ) ;
613+ it ( "recovers a missing registry entry from the last onboard session during list" , ( ) => {
614+ const home = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "nemoclaw-cli-list-session-recover-" ) ) ;
615+ const localBin = path . join ( home , "bin" ) ;
615616 const nemoclawDir = path . join ( home , ".nemoclaw" ) ;
617+ fs . mkdirSync ( localBin , { recursive : true } ) ;
616618 fs . mkdirSync ( nemoclawDir , { recursive : true } ) ;
617- // Registry has only gamma — session references alpha, but list should NOT import it.
618619 fs . writeFileSync (
619620 path . join ( nemoclawDir , "sandboxes.json" ) ,
620621 JSON . stringify ( {
@@ -633,26 +634,333 @@ describe("CLI dispatch", () => {
633634 ) ;
634635 fs . writeFileSync (
635636 path . join ( nemoclawDir , "onboard-session.json" ) ,
637+ JSON . stringify (
638+ {
639+ version : 1 ,
640+ sessionId : "session-1" ,
641+ resumable : true ,
642+ status : "complete" ,
643+ mode : "interactive" ,
644+ startedAt : "2026-03-31T00:00:00.000Z" ,
645+ updatedAt : "2026-03-31T00:00:00.000Z" ,
646+ lastStepStarted : "policies" ,
647+ lastCompletedStep : "policies" ,
648+ failure : null ,
649+ sandboxName : "alpha" ,
650+ provider : "nvidia-prod" ,
651+ model : "nvidia/nemotron-3-super-120b-a12b" ,
652+ endpointUrl : null ,
653+ credentialEnv : null ,
654+ preferredInferenceApi : null ,
655+ nimContainer : null ,
656+ policyPresets : [ "pypi" ] ,
657+ metadata : { gatewayName : "nemoclaw" } ,
658+ steps : {
659+ preflight : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
660+ gateway : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
661+ sandbox : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
662+ provider_selection : {
663+ status : "complete" ,
664+ startedAt : null ,
665+ completedAt : null ,
666+ error : null ,
667+ } ,
668+ inference : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
669+ openclaw : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
670+ policies : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
671+ } ,
672+ } ,
673+ null ,
674+ 2 ,
675+ ) ,
676+ { mode : 0o600 } ,
677+ ) ;
678+ fs . writeFileSync (
679+ path . join ( localBin , "openshell" ) ,
680+ [
681+ "#!/usr/bin/env bash" ,
682+ 'if [ "$1" = "status" ]; then' ,
683+ " echo 'Server Status'" ,
684+ " echo" ,
685+ " echo ' Gateway: nemoclaw'" ,
686+ " echo ' Status: Connected'" ,
687+ " exit 0" ,
688+ "fi" ,
689+ 'if [ "$1" = "gateway" ] && [ "$2" = "info" ]; then' ,
690+ " echo 'Gateway Info'" ,
691+ " echo" ,
692+ " echo ' Gateway: nemoclaw'" ,
693+ " exit 0" ,
694+ "fi" ,
695+ 'if [ "$1" = "sandbox" ] && [ "$2" = "list" ]; then' ,
696+ " echo 'No sandboxes found.'" ,
697+ " exit 0" ,
698+ "fi" ,
699+ 'if [ "$1" = "inference" ] && [ "$2" = "get" ]; then' ,
700+ " exit 0" ,
701+ "fi" ,
702+ 'if [ "$1" = "--version" ]; then' ,
703+ " echo 'openshell 0.0.16'" ,
704+ " exit 0" ,
705+ "fi" ,
706+ "exit 0" ,
707+ ] . join ( "\n" ) ,
708+ { mode : 0o755 } ,
709+ ) ;
710+
711+ const r = runWithEnv ( "list" , {
712+ HOME : home ,
713+ PATH : `${ localBin } :${ process . env . PATH || "" } ` ,
714+ } ) ;
715+
716+ expect ( r . code ) . toBe ( 0 ) ;
717+ expect (
718+ r . out . includes ( "Recovered sandbox inventory from the last onboard session." ) ,
719+ ) . toBeTruthy ( ) ;
720+ expect ( r . out . includes ( "alpha" ) ) . toBeTruthy ( ) ;
721+ expect ( r . out . includes ( "gamma" ) ) . toBeTruthy ( ) ;
722+ const saved = JSON . parse ( fs . readFileSync ( path . join ( nemoclawDir , "sandboxes.json" ) , "utf8" ) ) ;
723+ expect ( saved . sandboxes . alpha ) . toBeTruthy ( ) ;
724+ expect ( saved . sandboxes . alpha . policies ) . toEqual ( [ "pypi" ] ) ;
725+ expect ( saved . sandboxes . gamma ) . toBeTruthy ( ) ;
726+ expect ( saved . defaultSandbox ) . toBe ( "gamma" ) ;
727+ } ) ;
728+
729+ it ( "imports additional live sandboxes into the registry during list recovery" , ( ) => {
730+ const home = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "nemoclaw-cli-list-live-recover-" ) ) ;
731+ const localBin = path . join ( home , "bin" ) ;
732+ const nemoclawDir = path . join ( home , ".nemoclaw" ) ;
733+ fs . mkdirSync ( localBin , { recursive : true } ) ;
734+ fs . mkdirSync ( nemoclawDir , { recursive : true } ) ;
735+ fs . writeFileSync (
736+ path . join ( nemoclawDir , "sandboxes.json" ) ,
636737 JSON . stringify ( {
637- version : 1 ,
638- sessionId : "session-1" ,
639- resumable : true ,
640- status : "complete" ,
641- sandboxName : "alpha" ,
738+ sandboxes : {
739+ gamma : {
740+ name : "gamma" ,
741+ model : "existing-model" ,
742+ provider : "existing-provider" ,
743+ gpuEnabled : false ,
744+ policies : [ "npm" ] ,
745+ } ,
746+ } ,
747+ defaultSandbox : "gamma" ,
642748 } ) ,
643749 { mode : 0o600 } ,
644750 ) ;
751+ fs . writeFileSync (
752+ path . join ( nemoclawDir , "onboard-session.json" ) ,
753+ JSON . stringify (
754+ {
755+ version : 1 ,
756+ sessionId : "session-1" ,
757+ resumable : true ,
758+ status : "complete" ,
759+ mode : "interactive" ,
760+ startedAt : "2026-03-31T00:00:00.000Z" ,
761+ updatedAt : "2026-03-31T00:00:00.000Z" ,
762+ lastStepStarted : "policies" ,
763+ lastCompletedStep : "policies" ,
764+ failure : null ,
765+ sandboxName : "alpha" ,
766+ provider : "nvidia-prod" ,
767+ model : "nvidia/nemotron-3-super-120b-a12b" ,
768+ endpointUrl : null ,
769+ credentialEnv : null ,
770+ preferredInferenceApi : null ,
771+ nimContainer : null ,
772+ policyPresets : [ "pypi" ] ,
773+ metadata : { gatewayName : "nemoclaw" } ,
774+ steps : {
775+ preflight : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
776+ gateway : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
777+ sandbox : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
778+ provider_selection : {
779+ status : "complete" ,
780+ startedAt : null ,
781+ completedAt : null ,
782+ error : null ,
783+ } ,
784+ inference : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
785+ openclaw : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
786+ policies : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
787+ } ,
788+ } ,
789+ null ,
790+ 2 ,
791+ ) ,
792+ { mode : 0o600 } ,
793+ ) ;
794+ fs . writeFileSync (
795+ path . join ( localBin , "openshell" ) ,
796+ [
797+ "#!/usr/bin/env bash" ,
798+ 'if [ "$1" = "status" ]; then' ,
799+ " echo 'Server Status'" ,
800+ " echo" ,
801+ " echo ' Gateway: nemoclaw'" ,
802+ " echo ' Status: Connected'" ,
803+ " exit 0" ,
804+ "fi" ,
805+ 'if [ "$1" = "gateway" ] && [ "$2" = "info" ]; then' ,
806+ " echo 'Gateway Info'" ,
807+ " echo" ,
808+ " echo ' Gateway: nemoclaw'" ,
809+ " exit 0" ,
810+ "fi" ,
811+ 'if [ "$1" = "sandbox" ] && [ "$2" = "list" ]; then' ,
812+ " echo 'NAME PHASE'" ,
813+ " echo 'alpha Ready'" ,
814+ " echo 'beta Ready'" ,
815+ " exit 0" ,
816+ "fi" ,
817+ 'if [ "$1" = "inference" ] && [ "$2" = "get" ]; then' ,
818+ " exit 0" ,
819+ "fi" ,
820+ 'if [ "$1" = "--version" ]; then' ,
821+ " echo 'openshell 0.0.16'" ,
822+ " exit 0" ,
823+ "fi" ,
824+ "exit 0" ,
825+ ] . join ( "\n" ) ,
826+ { mode : 0o755 } ,
827+ ) ;
645828
646- const r = runWithEnv ( "list" , { HOME : home } ) ;
829+ const r = runWithEnv ( "list" , {
830+ HOME : home ,
831+ PATH : `${ localBin } :${ process . env . PATH || "" } ` ,
832+ } ) ;
647833
648834 expect ( r . code ) . toBe ( 0 ) ;
649- // Only gamma (from registry) should appear — not alpha (from session).
835+ expect (
836+ r . out . includes ( "Recovered sandbox inventory from the last onboard session." ) ,
837+ ) . toBeTruthy ( ) ;
838+ expect (
839+ r . out . includes ( "Recovered 1 sandbox entry from the live OpenShell gateway." ) ,
840+ ) . toBeTruthy ( ) ;
841+ expect ( r . out . includes ( "alpha" ) ) . toBeTruthy ( ) ;
842+ expect ( r . out . includes ( "beta" ) ) . toBeTruthy ( ) ;
650843 expect ( r . out . includes ( "gamma" ) ) . toBeTruthy ( ) ;
651- expect ( r . out . includes ( "alpha" ) ) . toBeFalsy ( ) ;
652- expect ( r . out ) . not . toMatch ( / R e c o v e r e d / ) ;
653- // Registry must not have been mutated.
654844 const saved = JSON . parse ( fs . readFileSync ( path . join ( nemoclawDir , "sandboxes.json" ) , "utf8" ) ) ;
655- expect ( saved . sandboxes . alpha ) . toBeUndefined ( ) ;
845+ expect ( saved . sandboxes . alpha ) . toBeTruthy ( ) ;
846+ expect ( saved . sandboxes . alpha . policies ) . toEqual ( [ "pypi" ] ) ;
847+ expect ( saved . sandboxes . beta ) . toBeTruthy ( ) ;
848+ expect ( saved . sandboxes . gamma ) . toBeTruthy ( ) ;
849+ expect ( saved . defaultSandbox ) . toBe ( "gamma" ) ;
850+ } ) ;
851+
852+ it ( "skips invalid recovered sandbox names during list recovery" , ( ) => {
853+ const home = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "nemoclaw-cli-list-invalid-recover-" ) ) ;
854+ const localBin = path . join ( home , "bin" ) ;
855+ const nemoclawDir = path . join ( home , ".nemoclaw" ) ;
856+ fs . mkdirSync ( localBin , { recursive : true } ) ;
857+ fs . mkdirSync ( nemoclawDir , { recursive : true } ) ;
858+ fs . writeFileSync (
859+ path . join ( nemoclawDir , "sandboxes.json" ) ,
860+ JSON . stringify ( {
861+ sandboxes : {
862+ gamma : {
863+ name : "gamma" ,
864+ model : "existing-model" ,
865+ provider : "existing-provider" ,
866+ gpuEnabled : false ,
867+ policies : [ "npm" ] ,
868+ } ,
869+ } ,
870+ defaultSandbox : "gamma" ,
871+ } ) ,
872+ { mode : 0o600 } ,
873+ ) ;
874+ fs . writeFileSync (
875+ path . join ( nemoclawDir , "onboard-session.json" ) ,
876+ JSON . stringify (
877+ {
878+ version : 1 ,
879+ sessionId : "session-1" ,
880+ resumable : true ,
881+ status : "complete" ,
882+ mode : "interactive" ,
883+ startedAt : "2026-03-31T00:00:00.000Z" ,
884+ updatedAt : "2026-03-31T00:00:00.000Z" ,
885+ lastStepStarted : "policies" ,
886+ lastCompletedStep : "policies" ,
887+ failure : null ,
888+ sandboxName : "Alpha" ,
889+ provider : "nvidia-prod" ,
890+ model : "nvidia/nemotron-3-super-120b-a12b" ,
891+ endpointUrl : null ,
892+ credentialEnv : null ,
893+ preferredInferenceApi : null ,
894+ nimContainer : null ,
895+ policyPresets : [ "pypi" ] ,
896+ metadata : { gatewayName : "nemoclaw" } ,
897+ steps : {
898+ preflight : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
899+ gateway : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
900+ sandbox : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
901+ provider_selection : {
902+ status : "complete" ,
903+ startedAt : null ,
904+ completedAt : null ,
905+ error : null ,
906+ } ,
907+ inference : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
908+ openclaw : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
909+ policies : { status : "complete" , startedAt : null , completedAt : null , error : null } ,
910+ } ,
911+ } ,
912+ null ,
913+ 2 ,
914+ ) ,
915+ { mode : 0o600 } ,
916+ ) ;
917+ fs . writeFileSync (
918+ path . join ( localBin , "openshell" ) ,
919+ [
920+ "#!/usr/bin/env bash" ,
921+ 'if [ "$1" = "status" ]; then' ,
922+ " echo 'Server Status'" ,
923+ " echo" ,
924+ " echo ' Gateway: nemoclaw'" ,
925+ " echo ' Status: Connected'" ,
926+ " exit 0" ,
927+ "fi" ,
928+ 'if [ "$1" = "gateway" ] && [ "$2" = "info" ]; then' ,
929+ " echo 'Gateway Info'" ,
930+ " echo" ,
931+ " echo ' Gateway: nemoclaw'" ,
932+ " exit 0" ,
933+ "fi" ,
934+ 'if [ "$1" = "sandbox" ] && [ "$2" = "list" ]; then' ,
935+ " echo 'NAME PHASE'" ,
936+ " echo 'alpha Ready'" ,
937+ " echo 'Bad_Name Ready'" ,
938+ " exit 0" ,
939+ "fi" ,
940+ 'if [ "$1" = "inference" ] && [ "$2" = "get" ]; then' ,
941+ " exit 0" ,
942+ "fi" ,
943+ 'if [ "$1" = "--version" ]; then' ,
944+ " echo 'openshell 0.0.16'" ,
945+ " exit 0" ,
946+ "fi" ,
947+ "exit 0" ,
948+ ] . join ( "\n" ) ,
949+ { mode : 0o755 } ,
950+ ) ;
951+
952+ const r = runWithEnv ( "list" , {
953+ HOME : home ,
954+ PATH : `${ localBin } :${ process . env . PATH || "" } ` ,
955+ } ) ;
956+
957+ expect ( r . code ) . toBe ( 0 ) ;
958+ expect ( r . out . includes ( "alpha" ) ) . toBeTruthy ( ) ;
959+ expect ( r . out . includes ( "Bad_Name" ) ) . toBeFalsy ( ) ;
960+ const saved = JSON . parse ( fs . readFileSync ( path . join ( nemoclawDir , "sandboxes.json" ) , "utf8" ) ) ;
961+ expect ( saved . sandboxes . alpha ) . toBeTruthy ( ) ;
962+ expect ( saved . sandboxes . Bad_Name ) . toBeUndefined ( ) ;
963+ expect ( saved . sandboxes . Alpha ) . toBeUndefined ( ) ;
656964 expect ( saved . sandboxes . gamma ) . toBeTruthy ( ) ;
657965 } ) ;
658966
0 commit comments