@@ -102,8 +102,8 @@ def setUpClass(cls) -> None:
102102 "boatstack-helper-old.exe" if os .name == "nt" else "boatstack-helper-old"
103103 )
104104 for output , version , source in (
105- (cls .helper , "v0.7.contract-new " , "b" * 40 ),
106- (cls .old_helper , "v0.7.contract-old " , "a" * 40 ),
105+ (cls .helper , "v9.9.10 " , "b" * 40 ),
106+ (cls .old_helper , "v9.9.9 " , "a" * 40 ),
107107 ):
108108 ldflags = " " .join (
109109 (
@@ -884,7 +884,7 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
884884 )
885885 self .assertTrue (updated ["doctor" ]["healthy" ])
886886 pin = json .loads ((repository / ".boatstack" / "runtime.json" ).read_text ())
887- self .assertEqual (pin ["version" ], "v0.7.contract-new " )
887+ self .assertEqual (pin ["version" ], "v9.9.10 " )
888888 self .assertEqual (pin ["sha256" ], hashlib .sha256 (self .helper .read_bytes ()).hexdigest ())
889889 self .assertNotIn ("path" , pin )
890890 events = [json .loads (line ) for line in self .run_command (
@@ -900,6 +900,210 @@ def test_offline_installer_initializes_updates_and_guards_through_kernel(self) -
900900 self .assertTrue (event ["committed_effects" ])
901901 self .assertEqual (event ["verification" ]["result" ], "satisfied" )
902902
903+ def test_installer_download_and_checksum_failures_are_actionable_and_non_mutating (self ) -> None :
904+ # control-law: installer-failure-identifies-exact-artifact-and-does-not-mutate-repository
905+ if os .name == "nt" :
906+ self .skipTest ("the repository contract job exercises the POSIX installer" )
907+ with tempfile .TemporaryDirectory () as temp :
908+ root = Path (temp )
909+ repository = root / "repo"
910+ repository .mkdir ()
911+ self .init_repository (repository )
912+ fake_bin = root / "fake-bin"
913+ fake_bin .mkdir ()
914+ curl = fake_bin / "curl"
915+ curl .write_text ("#!/bin/sh\n exit 22\n " )
916+ curl .chmod (0o755 )
917+ env = dict (os .environ )
918+ env .update (
919+ {
920+ "PATH" : f"{ fake_bin } { os .pathsep } { env ['PATH' ]} " ,
921+ "BOATSTACK_REPO" : str (repository ),
922+ "BOATSTACK_HOME" : str (root / "home" ),
923+ "BOATSTACK_INSTALL_DIR" : str (root / "bin" ),
924+ "BOATSTACK_VERSION" : "v9.9.9" ,
925+ }
926+ )
927+ unavailable = self .run_command (
928+ "bash" , REPO / "install.sh" , cwd = repository , env = env , expected = 1 ,
929+ )
930+ self .assertIn (
931+ "BOATSTACK_RUNTIME_ARTIFACT_UNAVAILABLE: version=v9.9.9 asset=boatstack-helper_" ,
932+ unavailable .stderr ,
933+ )
934+ self .assertEqual (
935+ self .run_command ("git" , "status" , "--porcelain" , cwd = repository ).stdout ,
936+ "" ,
937+ )
938+
939+ self .assertFalse ((root / "home" ).exists ())
940+ self .assertFalse ((root / "bin" ).exists ())
941+
942+ checksum_env = dict (env )
943+ checksum_env .update (
944+ {
945+ "BOATSTACK_BINARY" : str (self .helper ),
946+ "BOATSTACK_BINARY_SHA256" : "0" * 64 ,
947+ }
948+ )
949+ mismatch = self .run_command (
950+ "bash" , REPO / "install.sh" , cwd = repository , env = checksum_env , expected = 1 ,
951+ )
952+ self .assertIn (
953+ "BOATSTACK_RUNTIME_ARTIFACT_CHECKSUM_MISMATCH: version=v9.9.9" ,
954+ mismatch .stderr ,
955+ )
956+ self .assertEqual (
957+ self .run_command ("git" , "status" , "--porcelain" , cwd = repository ).stdout ,
958+ "" ,
959+ )
960+
961+ def test_installer_hydrates_exact_local_runtime_without_repository_state (self ) -> None :
962+ # control-law: approved-runtime-hydration-restores-only-trusted-runtime-storage
963+ if os .name == "nt" :
964+ self .skipTest ("the repository contract job exercises the POSIX installer" )
965+ with tempfile .TemporaryDirectory () as temp :
966+ root = Path (temp )
967+ repository = root / "repo"
968+ repository .mkdir ()
969+ self .init_repository (repository )
970+ version = self .run_command (self .helper , "version" ).stdout .strip ()
971+ digest = hashlib .sha256 (self .helper .read_bytes ()).hexdigest ()
972+ env = dict (os .environ )
973+ env .update (
974+ {
975+ "BOATSTACK_REPO" : str (repository ),
976+ "BOATSTACK_HOME" : str (root / "home" ),
977+ "BOATSTACK_INSTALL_DIR" : str (root / "bin" ),
978+ "BOATSTACK_MODE" : "hydrate" ,
979+ "BOATSTACK_VERSION" : version ,
980+ "BOATSTACK_BINARY" : str (self .helper ),
981+ "BOATSTACK_BINARY_SHA256" : digest ,
982+ }
983+ )
984+
985+ hydrated = self .run_command ("bash" , REPO / "install.sh" , cwd = repository , env = env )
986+ runtime = root / "home" / "runtimes" / f"{ version } -{ digest } " / "boatstack-runtime"
987+ self .assertTrue (runtime .is_file ())
988+ self .assertTrue ((root / "bin" / "boatstack" ).is_file ())
989+ self .assertNotIn ("Review and commit" , hydrated .stdout )
990+ self .assertFalse ((repository / ".boatstack" ).exists ())
991+ self .assertFalse ((repository / ".git" / "boatstack" ).exists ())
992+ self .assertEqual (
993+ self .run_command ("git" , "status" , "--porcelain" , cwd = repository ).stdout ,
994+ "" ,
995+ )
996+
997+ wrong_digest = dict (env )
998+ wrong_digest .update (
999+ {
1000+ "BOATSTACK_HOME" : str (root / "digest-home" ),
1001+ "BOATSTACK_INSTALL_DIR" : str (root / "digest-bin" ),
1002+ "BOATSTACK_EXPECTED_RUNTIME_SHA256" : "0" * 64 ,
1003+ }
1004+ )
1005+ digest_rejected = self .run_command (
1006+ "bash" , REPO / "install.sh" , cwd = repository , env = wrong_digest , expected = 1 ,
1007+ )
1008+ self .assertIn ("BOATSTACK_RUNTIME_ARTIFACT_CHECKSUM_MISMATCH" , digest_rejected .stderr )
1009+ self .assertFalse ((root / "digest-home" ).exists ())
1010+ self .assertFalse ((root / "digest-bin" ).exists ())
1011+
1012+ wrong_version = dict (env )
1013+ wrong_version .update (
1014+ {
1015+ "BOATSTACK_HOME" : str (root / "wrong-home" ),
1016+ "BOATSTACK_INSTALL_DIR" : str (root / "wrong-bin" ),
1017+ "BOATSTACK_VERSION" : "v9.9.9" ,
1018+ }
1019+ )
1020+ rejected = self .run_command (
1021+ "bash" , REPO / "install.sh" , cwd = repository , env = wrong_version , expected = 1 ,
1022+ )
1023+ self .assertIn ("Boatstack runtime version mismatch" , rejected .stderr )
1024+ self .assertFalse ((root / "wrong-home" ).exists ())
1025+ self .assertFalse ((root / "wrong-bin" ).exists ())
1026+
1027+ old_version = self .run_command (self .old_helper , "version" ).stdout .strip ()
1028+ old_digest = hashlib .sha256 (self .old_helper .read_bytes ()).hexdigest ()
1029+ older_runtime = dict (env )
1030+ older_runtime .update (
1031+ {
1032+ "BOATSTACK_HOME" : str (root / "old-home" ),
1033+ "BOATSTACK_INSTALL_DIR" : str (root / "old-bin" ),
1034+ "BOATSTACK_VERSION" : old_version ,
1035+ "BOATSTACK_BINARY" : str (self .old_helper ),
1036+ "BOATSTACK_BINARY_SHA256" : old_digest ,
1037+ "BOATSTACK_EXPECTED_RUNTIME_SHA256" : old_digest ,
1038+ }
1039+ )
1040+ self .run_command ("bash" , REPO / "install.sh" , cwd = repository , env = older_runtime )
1041+ restored = root / "old-home" / "runtimes" / f"{ old_version } -{ old_digest } " / "boatstack-runtime"
1042+ self .assertTrue (restored .is_file ())
1043+ self .assertFalse ((repository / ".boatstack" ).exists ())
1044+ self .assertFalse ((repository / ".git" / "boatstack" ).exists ())
1045+
1046+ def test_launcher_renders_missing_runtime_as_text_and_json_before_state (self ) -> None :
1047+ # control-law: launcher-diagnostic-precedes-runtime-dispatch-and-managed-state
1048+ with tempfile .TemporaryDirectory () as temp :
1049+ root = Path (temp )
1050+ repository = root / "repo"
1051+ repository .mkdir ()
1052+ self .init_repository (repository )
1053+ runtime = b"required but absent"
1054+ pin = {
1055+ "schema_version" : 1 ,
1056+ "version" : "v9.9.9" ,
1057+ "sha256" : hashlib .sha256 (runtime ).hexdigest (),
1058+ "source_revision" : "9" * 40 ,
1059+ "program_fingerprint" : "a" * 64 ,
1060+ "state_schema_version" : 3 ,
1061+ }
1062+ pin_path = repository / ".boatstack" / "runtime.json"
1063+ pin_path .parent .mkdir ()
1064+ pin_path .write_text (json .dumps (pin , indent = 2 ) + "\n " )
1065+ launcher = root / ("boatstack.exe" if os .name == "nt" else "boatstack" )
1066+ launcher .write_bytes (self .helper .read_bytes ())
1067+ launcher .chmod (0o755 )
1068+ env = dict (os .environ )
1069+ env .update (
1070+ {
1071+ "BOATSTACK_HOME" : str (root / "runtime-home" ),
1072+ "BOATSTACK_STATE_ROOT" : str (root / "state-root" ),
1073+ }
1074+ )
1075+
1076+ rendered = self .run_command (
1077+ launcher , "next" , "--repo" , repository , "--format" , "json" ,
1078+ env = env , expected = 1 ,
1079+ )
1080+ diagnostic = json .loads (rendered .stderr )
1081+ self .assertEqual (diagnostic ["schema" ], "boatstack-bootstrap-diagnostic" )
1082+ self .assertEqual (diagnostic ["schema_revision" ], 1 )
1083+ self .assertEqual (diagnostic ["code" ], "BOATSTACK_RUNTIME_NOT_INSTALLED" )
1084+ self .assertEqual (diagnostic ["required_runtime" ], {
1085+ "version" : pin ["version" ],
1086+ "sha256" : pin ["sha256" ],
1087+ "source_revision" : pin ["source_revision" ],
1088+ })
1089+ self .assertIn ("/v9.9.10/install" , diagnostic ["recovery" ]["command" ])
1090+ self .assertIn (pin ["sha256" ], diagnostic ["recovery" ]["command" ])
1091+ self .assertIn ("BOATSTACK_MODE=hydrate" , diagnostic ["recovery" ]["command" ])
1092+ self .assertNotIn ("BOATSTACK_MODE=update" , diagnostic ["recovery" ]["command" ])
1093+ self .assertTrue (diagnostic ["recovery" ]["requires_confirmation" ])
1094+ self .assertFalse (diagnostic ["flow_run_created" ])
1095+ self .assertFalse (diagnostic ["managed_state_changed" ])
1096+
1097+ text = self .run_command (
1098+ launcher , "next" , "--repo" , repository , "--format" , "text" ,
1099+ env = env , expected = 1 ,
1100+ )
1101+ self .assertIn ("Blocked:" , text .stderr )
1102+ self .assertIn ("BOATSTACK_RUNTIME_NOT_INSTALLED" , text .stderr )
1103+ self .assertIn ("BOATSTACK_VERSION=v9.9.9" , text .stderr )
1104+ self .assertFalse ((root / "state-root" ).exists ())
1105+ self .assertFalse ((repository / ".git" / "boatstack" ).exists ())
1106+
9031107 def test_program_changing_update_is_explicit_atomic_and_dormant_safe (self ) -> None :
9041108 # control-law: accepted-program-delta-atomically-pins-runtime-and-program
9051109 if os .name == "nt" :
@@ -997,7 +1201,7 @@ def test_program_changing_update_is_explicit_atomic_and_dormant_safe(self) -> No
9971201 env ["BOATSTACK_ACCEPT_PROGRAM_CHANGE" ] = "true"
9981202 self .run_command ("bash" , REPO / "install.sh" , cwd = repository , env = env )
9991203 pin = json .loads ((repository / ".boatstack" / "runtime.json" ).read_text ())
1000- self .assertEqual (pin ["version" ], "v0.7.contract-new " )
1204+ self .assertEqual (pin ["version" ], "v9.9.10 " )
10011205 self .assertEqual (pin ["sha256" ], hashlib .sha256 (self .helper .read_bytes ()).hexdigest ())
10021206 self .assertNotIn ("path" , pin )
10031207 doctor = json .loads (
0 commit comments