cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/portal-dist-prebuild
goq / scripts / tests / test_sigil_bootstrap.py
test_sigil_bootstrap.py 23.7K executable
1#!/usr/bin/env python323from __future__ import annotations45import base646import hashlib7import importlib.util8import io9import os10import pathlib11import re12import subprocess13import tarfile14import tempfile15import unittest161718REPO_DIR = pathlib.Path(__file__).resolve().parents[2]19BOOTSTRAP_PATH = REPO_DIR / "website" / "install-sigil"20MODULE_PATH = REPO_DIR / "scripts" / "verify-sigil-bootstrap.py"21SPEC = importlib.util.spec_from_file_location("sigil_bootstrap_verifier", MODULE_PATH)22assert SPEC is not None and SPEC.loader is not None23VERIFIER = importlib.util.module_from_spec(SPEC)24SPEC.loader.exec_module(VERIFIER)2526PUBLISHER_KEY = base64.b64encode(b"Ed" + b"A" * 8 + b"B" * 32).decode("ascii")27OTHER_PUBLISHER_KEY = base64.b64encode(b"Ed" + b"C" * 8 + b"D" * 32).decode("ascii")28RELEASE_TAG = "v1.2.3-alpha.1"29ASSET_TARGET_CONTRACT = "linux-glibc2.17-x86_64"30ASSET_NAME = f"sigil-{RELEASE_TAG}-{ASSET_TARGET_CONTRACT}.tar.gz"31ASSET_BASE = (32 "https://github.com/FelineStateMachine/goq/releases/download/"33 f"{RELEASE_TAG}/{ASSET_NAME}"34)35MINISIGN_VERSION = "0.12"36MINISIGN_ASSET_NAME = f"minisign-{MINISIGN_VERSION}-linux.tar.gz"37MINISIGN_URL = (38 "https://github.com/jedisct1/minisign/releases/download/0.12/"39 f"{MINISIGN_ASSET_NAME}"40)41MINISIGN_SHA256 = (42 "9a599b48ba6eb7b1e80f12f36b94ceca7c00b7a5173c95c3efc88d9822957e73"43)44ZERO_SHA256 = "0" * 6445MINISIGN_CORE_MEMBERS = {46 "minisign-linux/",47 "minisign-linux/aarch64/",48 "minisign-linux/x86_64/",49 "minisign-linux/aarch64/minisign",50 "minisign-linux/x86_64/minisign",51}52MINISIGN_APPLEDOUBLE_MEMBERS = {53 "._minisign-linux",54 "minisign-linux/._aarch64",55 "minisign-linux/._x86_64",56 "minisign-linux/aarch64/._minisign",57 "minisign-linux/x86_64/._minisign",58}596061def bootstrap_with_pins(62 source: str,63 *,64 publisher_key: str = PUBLISHER_KEY,65 release_tag: str = RELEASE_TAG,66 minisign_sha256: str | None = None,67) -> str:68 source, publisher_replacements = re.subn(69 r'^readonly publisher_key="[^"\n]*"$',70 f'readonly publisher_key="{publisher_key}"',71 source,72 count=1,73 flags=re.MULTILINE,74 )75 source, tag_replacements = re.subn(76 r'^readonly release_tag="[^"\n]*"$',77 f'readonly release_tag="{release_tag}"',78 source,79 count=1,80 flags=re.MULTILINE,81 )82 if minisign_sha256 is not None:83 source, minisign_replacements = re.subn(84 r'^readonly minisign_sha256="[^"\n]*"$',85 f'readonly minisign_sha256="{minisign_sha256}"',86 source,87 count=1,88 flags=re.MULTILINE,89 )90 else:91 minisign_replacements = 192 if (93 publisher_replacements != 194 or tag_replacements != 195 or minisign_replacements != 196 ):97 raise AssertionError("bootstrap fixture pins must each occur exactly once")98 return source99100101class SigilBootstrapContractTests(unittest.TestCase):102 @classmethod103 def setUpClass(cls) -> None:104 cls.bootstrap_source = BOOTSTRAP_PATH.read_text(encoding="utf-8")105106 def test_closed_channel_requires_all_three_sentinels(self) -> None:107 closed = bootstrap_with_pins(108 self.bootstrap_source,109 publisher_key="unconfigured",110 release_tag="unconfigured",111 )112 self.assertEqual(113 VERIFIER.verify_contract(closed, "unconfigured\n"),114 "closed",115 )116 partial_states = [117 (118 bootstrap_with_pins(self.bootstrap_source, release_tag="unconfigured"),119 "unconfigured\n",120 ),121 (122 bootstrap_with_pins(self.bootstrap_source, publisher_key="unconfigured"),123 "unconfigured\n",124 ),125 (closed, f"untrusted comment: fixture\n{PUBLISHER_KEY}\n"),126 (bootstrap_with_pins(self.bootstrap_source), "unconfigured\n"),127 ]128 for bootstrap, public_key in partial_states:129 with self.subTest(bootstrap=bootstrap[:80], public_key=public_key[:30]):130 with self.assertRaisesRegex(VERIFIER.VerificationError, "partially configured"):131 VERIFIER.verify_contract(bootstrap, public_key)132133 def test_open_channel_binds_exact_repository_key_and_release_tag(self) -> None:134 public_key = f"untrusted comment: Sigil fixture key\n{PUBLISHER_KEY}\n"135 self.assertEqual(136 VERIFIER.verify_contract(bootstrap_with_pins(self.bootstrap_source), public_key),137 "open",138 )139 with self.assertRaisesRegex(VERIFIER.VerificationError, "does not match"):140 VERIFIER.verify_contract(141 bootstrap_with_pins(self.bootstrap_source),142 f"untrusted comment: wrong\n{OTHER_PUBLISHER_KEY}\n",143 )144145 def test_malformed_pins_and_repository_drift_fail_closed(self) -> None:146 configured = bootstrap_with_pins(self.bootstrap_source)147 public_key = f"untrusted comment: fixture\n{PUBLISHER_KEY}\n"148 for malformed_tag in (149 "main",150 "v1",151 "v1.2",152 "v1.2.3/asset",153 "v1.2.3+build",154 ):155 with self.subTest(tag=malformed_tag):156 with self.assertRaisesRegex(VERIFIER.VerificationError, "tag is malformed"):157 VERIFIER.verify_contract(158 bootstrap_with_pins(self.bootstrap_source, release_tag=malformed_tag),159 public_key,160 )161162 with self.assertRaisesRegex(VERIFIER.VerificationError, "publisher key is malformed"):163 VERIFIER.verify_contract(164 bootstrap_with_pins(self.bootstrap_source, publisher_key="not-a-key"),165 public_key,166 )167 with self.assertRaisesRegex(VERIFIER.VerificationError, "unexpected content"):168 VERIFIER.verify_contract(configured, f"trusted comment: no\n{PUBLISHER_KEY}\n")169 with self.assertRaisesRegex(VERIFIER.VerificationError, "release repository"):170 VERIFIER.verify_contract(171 configured.replace("FelineStateMachine/goq", "attacker/goq", 1),172 public_key,173 )174 with self.assertRaisesRegex(VERIFIER.VerificationError, "exactly once"):175 VERIFIER.verify_contract(176 configured + f'\nreadonly publisher_key="{PUBLISHER_KEY}"\n',177 public_key,178 )179180 def test_verifier_provisioning_is_bound_to_reviewed_exact_pins(self) -> None:181 pins = VERIFIER.parse_bootstrap_pins(self.bootstrap_source)182 self.assertEqual(pins["asset_target_contract"], ASSET_TARGET_CONTRACT)183 self.assertEqual(pins["minisign_version"], MINISIGN_VERSION)184 self.assertEqual(pins["minisign_url"], MINISIGN_URL)185 self.assertEqual(pins["minisign_sha256"], MINISIGN_SHA256)186187 public_key = "unconfigured\n"188 replacements = {189 f'readonly asset_target_contract="{ASSET_TARGET_CONTRACT}"': (190 'readonly asset_target_contract="' + "bazzite" + '-x86_64"'191 ),192 'readonly minisign_version="0.12"': 'readonly minisign_version="0.13"',193 f'readonly minisign_url="{MINISIGN_URL}"': (194 'readonly minisign_url="https://example.invalid/minisign.tar.gz"'195 ),196 f'readonly minisign_sha256="{MINISIGN_SHA256}"': (197 f'readonly minisign_sha256="{ZERO_SHA256}"'198 ),199 }200 for original, replacement in replacements.items():201 with self.subTest(pin=original.split("=", 1)[0]):202 mutated = self.bootstrap_source.replace(original, replacement, 1)203 with self.assertRaises(VERIFIER.VerificationError):204 VERIFIER.verify_contract(mutated, public_key)205206 with self.assertRaisesRegex(VERIFIER.VerificationError, "exactly once"):207 VERIFIER.verify_contract(208 self.bootstrap_source209 + f'\nreadonly minisign_sha256="{MINISIGN_SHA256}"\n',210 public_key,211 )212213 def test_rejects_structurally_invalid_minisign_public_keys(self) -> None:214 invalid_keys = {215 "wrong algorithm": base64.b64encode(b"ED" + b"A" * 40).decode("ascii"),216 "short payload": base64.b64encode(b"Ed" + b"A" * 39).decode("ascii"),217 "long payload": base64.b64encode(b"Ed" + b"A" * 41).decode("ascii"),218 "invalid alphabet": "RW" + "!" * 54,219 "invalid padding": PUBLISHER_KEY[:-1] + "=",220 }221 for name, key in invalid_keys.items():222 with self.subTest(name=name):223 with self.assertRaisesRegex(VERIFIER.VerificationError, "malformed"):224 VERIFIER.verify_contract(225 bootstrap_with_pins(self.bootstrap_source, publisher_key=key),226 f"untrusted comment: fixture\n{key}\n",227 )228229 def test_current_repository_state_is_a_complete_valid_contract(self) -> None:230 public_key = (REPO_DIR / "release" / "sigil-minisign.pub").read_text(encoding="utf-8")231 self.assertIn(232 VERIFIER.verify_contract(self.bootstrap_source, public_key),233 {"closed", "open"},234 )235236237class SigilBootstrapBehaviorTests(unittest.TestCase):238 def setUp(self) -> None:239 self.temporary = tempfile.TemporaryDirectory()240 self.root = pathlib.Path(self.temporary.name)241 self.tmp_dir = self.root / "tmp"242 self.tmp_dir.mkdir()243 self.fixture_dir = self.root / "assets"244 self.fixture_dir.mkdir()245 self.fake_bin = self.root / "bin"246 self.fake_bin.mkdir()247 self.curl_log = self.root / "curl.log"248 self.stage_marker = self.root / "stage.marker"249 self.forbidden_marker = self.root / "forbidden.marker"250 self.minisign_marker = self.root / "minisign.marker"251 self.bootstrap = self.root / "install-sigil"252 self.write_command_stubs()253 minisign_digest = self.write_minisign_fixture()254 self.write_release_fixture()255 self.bootstrap.write_text(256 bootstrap_with_pins(257 BOOTSTRAP_PATH.read_text(encoding="utf-8"),258 minisign_sha256=minisign_digest,259 ),260 encoding="utf-8",261 )262 self.bootstrap.chmod(0o755)263264 def tearDown(self) -> None:265 self.temporary.cleanup()266267 def write_stub(self, name: str, source: str) -> None:268 path = self.fake_bin / name269 path.write_text(source, encoding="utf-8")270 path.chmod(0o755)271272 def write_command_stubs(self) -> None:273 self.write_stub(274 "uname",275 """#!/usr/bin/env bash276set -euo pipefail277case "${1:-}" in278 -s) printf '%s\n' "${BOOTSTRAP_TEST_OS:-Linux}" ;;279 -m) printf '%s\n' "${BOOTSTRAP_TEST_ARCH:-x86_64}" ;;280 *) exit 64 ;;281esac282""",283 )284 self.write_stub(285 "id",286 """#!/usr/bin/env bash287set -euo pipefail288[[ "${1:-}" == -u ]] || exit 64289printf '%s\n' "${BOOTSTRAP_TEST_UID:-1000}"290""",291 )292 self.write_stub(293 "grep",294 """#!/usr/bin/env bash295set -euo pipefail296if [[ "${*: -1}" == /etc/os-release ]]; then297 [[ "${BOOTSTRAP_TEST_BAZZITE:-1}" == 1 ]]298 exit299fi300exec /usr/bin/grep "$@"301""",302 )303 self.write_stub(304 "curl",305 """#!/usr/bin/env bash306set -euo pipefail307output=''308url=''309proto=''310tls=''311fail=0312silent=0313show_error=0314location=0315while (( $# )); do316 case "$1" in317 --proto) proto="$2"; shift 2 ;;318 --tlsv1.2) tls=1; shift ;;319 --fail) fail=1; shift ;;320 --silent) silent=1; shift ;;321 --show-error) show_error=1; shift ;;322 --location) location=1; shift ;;323 --retry|--output) option="$1"; value="$2"; shift 2; [[ "$option" == --output ]] && output="$value" ;;324 --retry-all-errors) shift ;;325 https://*) url="$1"; shift ;;326 *) exit 64 ;;327 esac328done329[[ "$proto" == =https && "$tls" == 1 && "$fail" == 1 && "$silent" == 1 \330 && "$show_error" == 1 && "$location" == 1 && -n "$output" && -n "$url" ]] || exit 64331name="${url##*/}"332printf '%s\n' "$url" >>"$BOOTSTRAP_CURL_LOG"333if [[ -n "${BOOTSTRAP_TEST_CURL_PARTIAL:-}" && "$name" == *"$BOOTSTRAP_TEST_CURL_PARTIAL" ]]; then334 printf partial >"$output"335 exit 22336fi337cp -- "$BOOTSTRAP_FIXTURE_DIR/$name" "$output"338""",339 )340 self.write_stub(341 "minisign",342 """#!/usr/bin/env bash343set -euo pipefail344printf 'PATH minisign invoked\n' >>"$BOOTSTRAP_FORBIDDEN_MARKER"345exit 99346""",347 )348 self.write_stub(349 "systemctl",350 """#!/usr/bin/env bash351set -euo pipefail352printf 'systemctl %s\n' "$*" >>"$BOOTSTRAP_FORBIDDEN_MARKER"353exit 99354""",355 )356357 def write_minisign_fixture(self, *, variant: str = "valid") -> str:358 archive_path = self.fixture_dir / MINISIGN_ASSET_NAME359 verifier = b"""#!/usr/bin/env bash360set -euo pipefail361printf 'invoked\\n' >>"$BOOTSTRAP_MINISIGN_MARKER"362[[ "${BOOTSTRAP_TEST_MINISIGN:-ok}" == ok ]] || exit 1363message=''364signature=''365key=''366while (( $# )); do367 case "$1" in368 -Vm) message="$2"; shift 2 ;;369 -x) signature="$2"; shift 2 ;;370 -P) key="$2"; shift 2 ;;371 *) exit 64 ;;372 esac373done374[[ -s "$message" && -s "$signature" && "$key" == "$BOOTSTRAP_EXPECTED_KEY" ]]375"""376 with tarfile.open(archive_path, "w:gz") as archive:377 self.add_archive_directory(archive, "minisign-linux/")378 self.add_archive_directory(archive, "minisign-linux/aarch64/")379 self.add_archive_directory(archive, "minisign-linux/x86_64/")380 self.add_archive_file(archive, "._minisign-linux", b"metadata\n", 0o644)381 self.add_archive_file(382 archive, "minisign-linux/._aarch64", b"metadata\n", 0o644383 )384 self.add_archive_file(385 archive, "minisign-linux/._x86_64", b"metadata\n", 0o644386 )387 self.add_archive_file(388 archive,389 "minisign-linux/aarch64/._minisign",390 b"metadata\n",391 0o644,392 )393 self.add_archive_file(394 archive,395 "minisign-linux/x86_64/._minisign",396 b"metadata\n",397 0o644,398 )399 self.add_archive_file(400 archive,401 "minisign-linux/aarch64/minisign",402 b"fixture aarch64 binary\n",403 0o755,404 )405 if variant == "symlink":406 link = tarfile.TarInfo("minisign-linux/x86_64/minisign")407 link.type = tarfile.SYMTYPE408 link.linkname = "../aarch64/minisign"409 archive.addfile(link)410 else:411 self.add_archive_file(412 archive,413 "minisign-linux/x86_64/minisign",414 verifier,415 0o755,416 )417 if variant == "unexpected":418 self.add_archive_file(419 archive,420 "minisign-linux/README",421 b"unexpected\n",422 0o644,423 )424 return hashlib.sha256(archive_path.read_bytes()).hexdigest()425426 def repin_minisign_fixture(self, *, variant: str) -> None:427 digest = self.write_minisign_fixture(variant=variant)428 self.bootstrap.write_text(429 bootstrap_with_pins(430 BOOTSTRAP_PATH.read_text(encoding="utf-8"),431 minisign_sha256=digest,432 ),433 encoding="utf-8",434 )435 self.bootstrap.chmod(0o755)436437 def write_release_fixture(438 self,439 *,440 variant: str = "valid",441 valid_checksum: bool = True,442 ) -> None:443 archive_path = self.fixture_dir / ASSET_NAME444 with tarfile.open(archive_path, "w:gz") as archive:445 self.add_archive_file(446 archive,447 "payload/stage-this-release.sh",448 b"#!/usr/bin/env bash\nset -euo pipefail\nprintf 'staged\\n' >>\"$BOOTSTRAP_STAGE_MARKER\"\n",449 0o755,450 )451 self.add_archive_file(452 archive,453 "payload/install-bazzite-package.sh",454 b"#!/usr/bin/env bash\nset -euo pipefail\nprintf 'forbidden\\n' >>\"$BOOTSTRAP_FORBIDDEN_MARKER\"\n",455 0o755,456 )457 if variant == "escape":458 self.add_archive_file(archive, "../escape", b"no\n", 0o644)459 if variant == "symlink":460 link = tarfile.TarInfo("payload/unsafe-link")461 link.type = tarfile.SYMTYPE462 link.linkname = "stage-this-release.sh"463 archive.addfile(link)464465 digest = hashlib.sha256(archive_path.read_bytes()).hexdigest()466 if not valid_checksum:467 digest = "0" * 64468 (self.fixture_dir / f"{ASSET_NAME}.sha256").write_text(469 f"{digest} {ASSET_NAME}\n", encoding="utf-8"470 )471 (self.fixture_dir / f"{ASSET_NAME}.minisig").write_text(472 "fixture signature\n", encoding="utf-8"473 )474475 @staticmethod476 def add_archive_directory(archive: tarfile.TarFile, name: str) -> None:477 info = tarfile.TarInfo(name)478 info.type = tarfile.DIRTYPE479 info.mode = 0o755480 archive.addfile(info)481482 @staticmethod483 def add_archive_file(484 archive: tarfile.TarFile,485 name: str,486 payload: bytes,487 mode: int,488 ) -> None:489 info = tarfile.TarInfo(name)490 info.size = len(payload)491 info.mode = mode492 archive.addfile(info, io.BytesIO(payload))493494 def run_bootstrap(self, **overrides: str) -> subprocess.CompletedProcess[str]:495 environment = os.environ.copy()496 environment.update(497 {498 "PATH": f"{self.fake_bin}:{environment['PATH']}",499 "TMPDIR": str(self.tmp_dir),500 "BOOTSTRAP_FIXTURE_DIR": str(self.fixture_dir),501 "BOOTSTRAP_EXPECTED_KEY": PUBLISHER_KEY,502 "BOOTSTRAP_CURL_LOG": str(self.curl_log),503 "BOOTSTRAP_STAGE_MARKER": str(self.stage_marker),504 "BOOTSTRAP_FORBIDDEN_MARKER": str(self.forbidden_marker),505 "BOOTSTRAP_MINISIGN_MARKER": str(self.minisign_marker),506 }507 )508 environment.update(overrides)509 return subprocess.run(510 [str(self.bootstrap)],511 check=False,512 stdout=subprocess.PIPE,513 stderr=subprocess.PIPE,514 text=True,515 env=environment,516 )517518 def assert_rejected(519 self, expected: str, **overrides: str520 ) -> subprocess.CompletedProcess[str]:521 result = self.run_bootstrap(**overrides)522 self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)523 self.assertIn(expected, result.stderr)524 self.assertFalse(self.stage_marker.exists())525 self.assertFalse(self.forbidden_marker.exists())526 self.assertEqual(list(self.tmp_dir.glob("goq-sigil-install.*")), [])527 return result528529 def test_rejects_wrong_os_arch_root_and_non_bazzite(self) -> None:530 cases = [531 ("Sigil requires Linux", {"BOOTSTRAP_TEST_OS": "Darwin"}),532 ("Sigil currently requires x86_64", {"BOOTSTRAP_TEST_ARCH": "aarch64"}),533 ("not root", {"BOOTSTRAP_TEST_UID": "0"}),534 ("currently supports Bazzite", {"BOOTSTRAP_TEST_BAZZITE": "0"}),535 ]536 for expected, overrides in cases:537 with self.subTest(expected=expected):538 self.assert_rejected(expected, **overrides)539540 def test_verifier_download_and_digest_fail_before_any_verifier_executes(self) -> None:541 self.assert_rejected(542 "Minisign verifier download failed",543 BOOTSTRAP_TEST_CURL_PARTIAL=MINISIGN_ASSET_NAME,544 )545 self.assertFalse(self.minisign_marker.exists())546 self.assertEqual(547 self.curl_log.read_text(encoding="utf-8").splitlines(),548 [MINISIGN_URL],549 )550551 (self.fixture_dir / MINISIGN_ASSET_NAME).write_bytes(b"")552 self.assert_rejected("Minisign verifier download is empty")553 self.assertFalse(self.minisign_marker.exists())554555 digest = self.write_minisign_fixture()556 self.assertNotEqual(digest, ZERO_SHA256)557 with (self.fixture_dir / MINISIGN_ASSET_NAME).open("ab") as archive:558 archive.write(b"corrupt")559 self.assert_rejected("Minisign verifier checksum does not match")560 self.assertFalse(self.minisign_marker.exists())561562 def test_verifier_archive_shape_and_types_fail_before_execution(self) -> None:563 self.repin_minisign_fixture(variant="unexpected")564 self.assert_rejected("Minisign verifier archive has unexpected contents")565 self.assertFalse(self.minisign_marker.exists())566567 self.repin_minisign_fixture(variant="symlink")568 self.assert_rejected("Minisign verifier archive contains a special file or link")569 self.assertFalse(self.minisign_marker.exists())570571 def test_verifier_fixture_models_the_full_official_archive_layout(self) -> None:572 with tarfile.open(self.fixture_dir / MINISIGN_ASSET_NAME, "r:gz") as archive:573 self.assertEqual(574 {member.name.rstrip("/") for member in archive.getmembers()},575 {576 member.rstrip("/")577 for member in MINISIGN_CORE_MEMBERS578 | MINISIGN_APPLEDOUBLE_MEMBERS579 },580 )581582 def test_rejects_partial_download_bad_signature_and_bad_checksum(self) -> None:583 self.assert_rejected(584 "signed release asset download failed",585 BOOTSTRAP_TEST_CURL_PARTIAL=".sha256",586 )587 self.assertEqual(588 self.curl_log.read_text(encoding="utf-8").splitlines(),589 [MINISIGN_URL, ASSET_BASE, f"{ASSET_BASE}.sha256"],590 )591 (self.fixture_dir / f"{ASSET_NAME}.sha256").write_text("", encoding="utf-8")592 self.assert_rejected("signed release asset download is empty")593 self.write_release_fixture()594 self.assert_rejected(595 "publisher signature is invalid",596 BOOTSTRAP_TEST_MINISIGN="fail",597 )598 self.assertEqual(self.minisign_marker.read_text(encoding="utf-8"), "invoked\n")599 self.write_release_fixture(valid_checksum=False)600 self.assert_rejected("release checksum does not match")601602 def test_rejects_malformed_checksum_declarations(self) -> None:603 checksum_path = self.fixture_dir / f"{ASSET_NAME}.sha256"604 valid = checksum_path.read_text(encoding="utf-8")605 checksum_path.write_text(valid + valid, encoding="utf-8")606 self.assert_rejected("release checksum file is malformed")607608 self.write_release_fixture()609 digest = hashlib.sha256((self.fixture_dir / ASSET_NAME).read_bytes()).hexdigest()610 checksum_path.write_text(f"{digest} wrong.tar.gz\n", encoding="utf-8")611 self.assert_rejected("release checksum declaration is malformed")612613 def test_rejects_escaping_paths_and_links_before_extraction(self) -> None:614 self.write_release_fixture(variant="escape")615 self.assert_rejected("escapes its payload root")616 self.write_release_fixture(variant="symlink")617 self.assert_rejected("special file or link")618619 def test_success_invokes_only_the_package_stager_and_cleans_up(self) -> None:620 result = self.run_bootstrap()621 self.assertEqual(result.returncode, 0, result.stdout + result.stderr)622 self.assertEqual(self.stage_marker.read_text(encoding="utf-8"), "staged\n")623 self.assertEqual(self.minisign_marker.read_text(encoding="utf-8"), "invoked\n")624 self.assertFalse(self.forbidden_marker.exists())625 self.assertIn(f"Sigil runtime {RELEASE_TAG} is installed.", result.stdout)626 self.assertIn(627 ".local/libexec/sigil-spark/current/docs/sigil-host-activation.md",628 result.stdout,629 )630 self.assertNotIn("fresh Bazzite runbook", result.stdout)631 self.assertEqual(list(self.tmp_dir.glob("goq-sigil-install.*")), [])632633634if __name__ == "__main__":635 unittest.main()