mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
a2fc29b70b
Retain the rejected VERSION rollback result. Replace general installer execution with pinned-signature archive selection and atomic binary/VERSION swaps, preserve unrelated distribution assets, and fail closed on missing matching native acceptance before production mutation. Change-source: pulse-maintainer
630 lines
31 KiB
Python
630 lines
31 KiB
Python
import hashlib
|
|
import importlib.util
|
|
import io
|
|
import tarfile
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
|
|
SCRIPTS = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load(name, filename):
|
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS / filename)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
engine = load("demo_transaction", "demo-runtime-transaction.py")
|
|
dispatcher = load("demo_dispatcher", "dispatch-demo-runtime.py")
|
|
native = load("demo_native", "tests/demo_runtime_native.py")
|
|
|
|
|
|
def request(mode="recover"):
|
|
return {"mode": mode, "hostname": "fixture", "local_url": "http://127.0.0.1:7655",
|
|
"public_url": "https://demo.example/api/health", "version": "v6.4.5",
|
|
"control_sha": "a" * 40, "run_id": "123", "run_attempt": "1",
|
|
"binary_sha256": hashlib.sha256(b"changed binary").hexdigest() if mode == "update" else "",
|
|
"version_sha256": hashlib.sha256(b"6.4.6\n").hexdigest() if mode == "update" else "",
|
|
"profile": {**{name: "2" for name in engine.COUNT_KEYS},
|
|
"seed_duration": "2h", "sample_interval": "5m", "update_interval": "15s"}}
|
|
|
|
|
|
class FixtureHost(engine.Host):
|
|
"""Real filesystem estate with a virtual *unchanged* 300-second clock.
|
|
|
|
Commands/services/journal/HTTP are adapters, not native or installed proof.
|
|
"""
|
|
def __init__(self):
|
|
self.clock = 0
|
|
self.pid = 100
|
|
self.running = True
|
|
self.runtime_version = "6.4.5"
|
|
self.starts = 0
|
|
self.stops = 0
|
|
self.install_count = 0
|
|
self.fail_at = None
|
|
self.failed = False
|
|
self.rollback_failure = False
|
|
self.unhealthy = False
|
|
self.journal_failure = False
|
|
self.ready_until = 0
|
|
self.cancel_at = None
|
|
self.cancel_in_recovery = False
|
|
self.co_host_change = False
|
|
self.co_pid = 200
|
|
self.transaction = None
|
|
|
|
def now(self):
|
|
return self.clock
|
|
|
|
def sleep(self, seconds):
|
|
self.clock += seconds
|
|
|
|
def identity(self, hostname):
|
|
if hostname != "fixture":
|
|
raise engine.Failure("host-identity")
|
|
|
|
def state(self):
|
|
state = {"ActiveState": "active", "SubState": "running", "MainPID": str(self.pid), "NRestarts": "0"}
|
|
result = {"pulse": state, "pulse-relay": {**state, "MainPID": "200"}, "caddy": {**state, "MainPID": str(self.co_pid)}}
|
|
if not self.running:
|
|
result["pulse"] = {**state, "ActiveState": "inactive", "SubState": "dead", "MainPID": "0"}
|
|
return result
|
|
|
|
def status(self, url):
|
|
if url == engine.RELAY_HEALTH:
|
|
return "200"
|
|
if self.clock < self.ready_until or self.unhealthy:
|
|
return "503"
|
|
if self.failed and self.rollback_failure:
|
|
return "503"
|
|
return "200" if self.running else "000"
|
|
|
|
def version(self, url):
|
|
return self.runtime_version
|
|
|
|
def cursors(self):
|
|
return {name: str(self.clock) for name in engine.SERVICES}
|
|
|
|
def check_journal(self, cursors):
|
|
phase = self.transaction.receipt["status"] if self.transaction else ""
|
|
if self.journal_failure:
|
|
raise engine.Failure("journal-observation")
|
|
if self.co_host_change and self.clock >= 55:
|
|
self.co_pid = 201
|
|
if self.cancel_at is not None and self.clock >= self.cancel_at and not self.failed:
|
|
self.failed = True
|
|
handler = signal.getsignal(signal.SIGTERM)
|
|
handler(signal.SIGTERM, None)
|
|
if self.cancel_in_recovery and phase == "recovering" and self.clock >= 350:
|
|
# Exercise the actual installed handler decision; do not signal the
|
|
# source-proof executor or kill another process to prove it.
|
|
assert signal.getsignal(signal.SIGTERM) == signal.SIG_IGN
|
|
self.cancel_in_recovery = False
|
|
if self.fail_at is not None and self.clock >= self.fail_at and not self.failed:
|
|
self.failed = True
|
|
raise engine.Failure("new-service-crash")
|
|
|
|
def stop(self):
|
|
self.stops += 1
|
|
self.running = False
|
|
|
|
def start(self):
|
|
self.starts += 1
|
|
self.pid += 1
|
|
self.running = True
|
|
if self.transaction.receipt["status"] == "recovering":
|
|
self.runtime_version = "6.4.5"
|
|
self.unhealthy = False
|
|
|
|
def install(self, attempt, req):
|
|
# Keep the clock/services virtual, but use the real narrow file swaps.
|
|
super().install(attempt, req)
|
|
self.install_count += 1
|
|
self.runtime_version = req["version"].lstrip("v")
|
|
|
|
def profile(self, req, unhealthy):
|
|
super().profile(req, unhealthy)
|
|
if self.unhealthy:
|
|
self.unhealthy = False
|
|
|
|
|
|
class TransactionTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tmp.cleanup)
|
|
root = Path(self.tmp.name)
|
|
self.paths = {name: root / "estate" / name for name in engine.PATHS}
|
|
for name, path in self.paths.items():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if name in {"dropins", "data"}:
|
|
path.mkdir()
|
|
else:
|
|
path.write_bytes(b"old " + name.encode())
|
|
path.chmod(0o755 if name == "binary" else 0o644)
|
|
(self.paths["dropins"] / "limits.conf").write_bytes(b"old limits")
|
|
(self.paths["data"] / ".env").write_text("PRIVATE_TOKEN=not-logged\n")
|
|
(self.paths["data"] / "billing.json").write_text('{"capabilities":[],"integrity":"private"}')
|
|
(self.paths["data"] / "persisted-history").write_bytes(b"original history")
|
|
(self.paths["data"] / "alerts").mkdir()
|
|
(self.paths["data"] / "alerts/events.db").write_bytes(b"synthetic opaque persistent file")
|
|
self.root = root / "control"
|
|
self.attempt = self.root / "attempts" / ("a" * 64)
|
|
self.attempt.mkdir(parents=True)
|
|
self.paths["version"].write_text("6.4.5\n")
|
|
(self.attempt / "binary").write_bytes(b"changed binary")
|
|
(self.attempt / "version").write_bytes(b"6.4.6\n")
|
|
# Untouched distribution/service assets are outside the chosen operation.
|
|
self.untouched = {name: root / "unowned" / name for name in
|
|
("agent", "scripts", "helper", "auto-update", "timer", "symlink", "marker", "backup")}
|
|
for name, path in self.untouched.items():
|
|
path.parent.mkdir(parents=True, exist_ok=True); path.write_text("original " + name)
|
|
self.untouched_before = engine.estate_hash(self.untouched)
|
|
self.addCleanup(patch.stopall)
|
|
patch.object(engine, "ROOT", self.root).start()
|
|
patch.object(engine, "PATHS", self.paths).start()
|
|
self.before = engine.estate_hash(self.paths)
|
|
self.host = FixtureHost()
|
|
|
|
def run_transaction(self, mode="recover", version=None):
|
|
req = request(mode)
|
|
if version:
|
|
req["version"] = version
|
|
tx = engine.Transaction(self.host, req, self.attempt)
|
|
self.host.transaction = tx
|
|
result = tx.run()
|
|
self.assertEqual(tx.receipt, json.loads((self.attempt / "receipt.json").read_text()))
|
|
return result, tx.receipt
|
|
|
|
def test_healthy_profile_change_keeps_data_and_watches_complete_window(self):
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(receipt["status"], "committed")
|
|
self.assertEqual(receipt["forward"]["elapsed_seconds"], 300)
|
|
self.assertEqual(receipt["forward"]["samples"], 61)
|
|
self.assertEqual(receipt["baseline_services"]["pulse"]["MainPID"], "100")
|
|
self.assertEqual(receipt["forward"]["initial_services"], receipt["forward"]["last_services"])
|
|
self.assertEqual(receipt["forward"]["observed_version"], "6.4.5")
|
|
self.assertEqual(self.host.install_count, 0)
|
|
for name in ("binary", "unit", "dropins"):
|
|
self.assertEqual(engine.estate_hash({name: self.paths[name]}),
|
|
engine.estate_hash({name: self.attempt / "snapshot" / name}))
|
|
self.assertEqual((self.paths["data"] / "alerts/events.db").read_bytes(), b"synthetic opaque persistent file")
|
|
self.assertTrue((self.attempt / "snapshot/data/persisted-history").exists())
|
|
self.assertNotIn("not-logged", json.dumps(receipt))
|
|
|
|
def test_healthy_noop_requires_profile_and_full_watch_without_restart(self):
|
|
self.host.profile(request(), False)
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(receipt["status"], "healthy_noop")
|
|
self.assertEqual(receipt["forward"]["elapsed_seconds"], 300)
|
|
self.assertFalse(receipt["mutated"])
|
|
self.assertEqual(self.host.stops, 0)
|
|
|
|
def test_noop_cancellation_retains_terminal_failure_without_stopping_service(self):
|
|
self.host.profile(request(), False)
|
|
self.host.cancel_at = 55
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "refused")
|
|
self.assertEqual(receipt["failure"], "cancelled-forward")
|
|
self.assertEqual(self.host.stops, 0)
|
|
self.assertFalse(receipt["mutated"])
|
|
|
|
def test_update_restores_complete_changed_executable_unit_and_data_after_late_failure(self):
|
|
self.host.fail_at = 55
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "rolled_back")
|
|
self.assertEqual(receipt["failure"], "new-service-crash")
|
|
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
self.assertEqual(self.host.runtime_version, "6.4.5")
|
|
|
|
def test_success_changes_only_runtime_and_profile_leaving_distribution_assets(self):
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual((code, receipt["status"]), (0, "committed"))
|
|
self.assertEqual(self.paths["binary"].read_bytes(), b"changed binary")
|
|
self.assertEqual(self.paths["version"].read_text(), "6.4.6\n")
|
|
for name in ("unit", "dropins"):
|
|
self.assertEqual(engine.estate_hash({name: self.paths[name]}),
|
|
engine.estate_hash({name: self.attempt / "snapshot" / name}))
|
|
self.assertEqual(engine.estate_hash(self.untouched), self.untouched_before)
|
|
self.assertEqual(receipt["forward"]["elapsed_seconds"], 300)
|
|
|
|
def test_late_failure_restores_VERSION_and_runtime_with_untouched_helpers(self):
|
|
self.host.fail_at = 55
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual((code, receipt["status"]), (1, "rolled_back"))
|
|
self.assertEqual(self.paths["version"].read_text(), "6.4.5\n")
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
self.assertEqual(engine.estate_hash(self.untouched), self.untouched_before)
|
|
self.assertEqual(receipt["recovery"]["runtime_sha256"], receipt["runtime_snapshot_sha256"])
|
|
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
|
|
|
|
def test_payload_version_mismatch_restores_without_installing(self):
|
|
(self.attempt / "version").write_text("6.4.7\n")
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual((code, receipt["status"]), (1, "rolled_back"))
|
|
self.assertEqual(receipt["failure"], "runtime-payload-identity")
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
|
|
def test_failure_at_299_seconds_still_collects_entire_recovery_and_cancellation(self):
|
|
self.host.fail_at = 299
|
|
self.host.cancel_in_recovery = True
|
|
previous = signal.getsignal(signal.SIGTERM)
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
|
|
self.assertGreaterEqual(self.host.clock, 600)
|
|
self.assertFalse(self.host.cancel_in_recovery)
|
|
self.assertEqual(signal.getsignal(signal.SIGTERM), previous)
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
|
|
def test_forward_cancel_owns_full_recovery(self):
|
|
self.host.cancel_at = 55
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["failure"], "cancelled-forward")
|
|
self.assertEqual(receipt["status"], "rolled_back")
|
|
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
|
|
|
|
def test_failed_rollback_retains_estate_and_blocks_next_transaction(self):
|
|
self.host.fail_at = 55
|
|
self.host.rollback_failure = True
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "rollback_failed")
|
|
self.assertTrue(receipt["snapshot_retained"])
|
|
next_attempt = self.root / "attempts" / ("b" * 64)
|
|
next_attempt.mkdir()
|
|
self.attempt = next_attempt
|
|
self.host.rollback_failure = False
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(receipt["failure"], "prior-recovery-required")
|
|
self.assertFalse(receipt["mutated"])
|
|
|
|
def test_unhealthy_recovery_retains_original_generated_history(self):
|
|
self.host.unhealthy = True
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 0)
|
|
self.assertFalse(receipt["healthy_baseline"])
|
|
self.assertFalse((self.paths["data"] / "alerts/events.db").exists())
|
|
self.assertEqual((self.attempt / "snapshot/data/alerts/events.db").read_bytes(), b"synthetic opaque persistent file")
|
|
self.assertEqual(receipt["forward"]["elapsed_seconds"], 300)
|
|
|
|
def test_failed_unhealthy_recovery_is_not_a_healthy_rollback(self):
|
|
self.host.unhealthy = True
|
|
self.host.fail_at = 55
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "recovery_required")
|
|
self.assertEqual(receipt["rollback"], "unavailable-unhealthy-baseline")
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
self.assertFalse(self.host.running)
|
|
|
|
def test_update_refuses_unhealthy_baseline_without_stopping_or_pruning(self):
|
|
self.host.unhealthy = True
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["failure"], "update-baseline-unhealthy")
|
|
self.assertEqual(self.host.stops, 0)
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
|
|
def test_recovery_refuses_different_healthy_source(self):
|
|
code, receipt = self.run_transaction(version="v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["failure"], "recovery-source-mismatch")
|
|
self.assertEqual(self.host.stops, 0)
|
|
|
|
def test_listener_readiness_does_not_spend_the_watch_window(self):
|
|
self.host.ready_until = 30
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(receipt["forward"]["elapsed_seconds"], 300)
|
|
self.assertGreaterEqual(self.host.clock, 330)
|
|
self.assertEqual(self.host.starts, 1)
|
|
|
|
def test_missing_journal_fails_closed_and_retains_failure(self):
|
|
self.host.journal_failure = True
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "rollback_failed")
|
|
self.assertEqual(receipt["recovery_failure"], "journal-observation")
|
|
|
|
def test_cohost_restart_is_not_silently_accepted_as_recovery(self):
|
|
self.host.co_host_change = True
|
|
code, receipt = self.run_transaction("update", "v6.4.6")
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "rollback_failed")
|
|
self.assertEqual(receipt["recovery_failure"], "cohost-identity-changed")
|
|
|
|
def test_snapshot_headroom_failure_never_deletes_persistent_files(self):
|
|
with patch.object(engine.shutil, "disk_usage", return_value=type("Usage", (), {"free": 0})()):
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["status"], "rolled_back")
|
|
self.assertFalse(receipt["mutated"])
|
|
self.assertEqual(receipt["failure"], "snapshot-headroom")
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
|
|
def test_linked_history_cannot_remove_files_outside_estate(self):
|
|
(self.paths["data"] / "alerts/events.db").unlink()
|
|
outside = self.root / "private-file"
|
|
outside.write_text("keep")
|
|
(self.paths["data"] / "alerts/events.db").symlink_to(outside)
|
|
self.host.unhealthy = True
|
|
code, receipt = self.run_transaction()
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(receipt["failure"], "history-symlink")
|
|
self.assertEqual(outside.read_text(), "keep")
|
|
|
|
def test_shared_host_lock_refusal_is_terminal_and_has_no_mutation(self):
|
|
lock = self.root / "host.lock"
|
|
fd = os.open(lock, os.O_CREAT | os.O_RDWR, 0o600)
|
|
self.addCleanup(os.close, fd)
|
|
engine.fcntl.flock(fd, engine.fcntl.LOCK_EX | engine.fcntl.LOCK_NB)
|
|
(self.attempt / "request.json").write_text(json.dumps(request()))
|
|
with patch.object(engine, "LOCK", lock), patch.object(engine.sys, "argv", ["transaction", str(self.attempt)]):
|
|
self.assertEqual(engine.main(), 1)
|
|
receipt = json.loads((self.attempt / "receipt.json").read_text())
|
|
self.assertEqual(receipt["status"], "refused")
|
|
self.assertFalse(receipt["mutated"])
|
|
self.assertEqual(engine.estate_hash(self.paths), self.before)
|
|
|
|
|
|
class InputAndCommandTest(unittest.TestCase):
|
|
def test_cohost_check_uses_the_existing_relay_health_route(self):
|
|
self.assertEqual(engine.RELAY_HEALTH, "https://relay.pulserelay.pro/healthz")
|
|
|
|
def test_valid_current_envelope(self):
|
|
engine.validate(request())
|
|
engine.validate(request("update"))
|
|
|
|
def test_no_candidate_prerelease_arbitrary_service_url_or_profile(self):
|
|
cases = [("mode", "shell"), ("version", "v6.4.6-rc.1"), ("control_sha", "main"),
|
|
("local_url", "http://other-host:7655"), ("public_url", "http://demo.example/api/health"),
|
|
("public_url", "https://user:secret@demo.example/api/health"),
|
|
("public_url", "https://demo.example/api/health?token=secret"),
|
|
("binary_sha256", "arbitrary"), ("version_sha256", "arbitrary")]
|
|
for name, value in cases:
|
|
with self.subTest(name=name, value=value):
|
|
req = request(); req[name] = value
|
|
with self.assertRaises(engine.Failure):
|
|
engine.validate(req)
|
|
req = request(); req["service"] = "license"
|
|
with self.assertRaises(engine.Failure):
|
|
engine.validate(req)
|
|
req = request(); req["profile"]["nodes"] = "2; restart caddy"
|
|
with self.assertRaises(engine.Failure):
|
|
engine.validate(req)
|
|
|
|
def test_http_observation_rejects_redirect_and_transport_failure(self):
|
|
host = engine.Host()
|
|
with patch.object(host, "command", return_value="302") as command:
|
|
self.assertEqual(host.status("https://demo.example/api/health"), "302")
|
|
self.assertNotIn("--location", command.call_args.args[0])
|
|
self.assertIn("--disable", command.call_args.args[0])
|
|
self.assertIn("/dev/null", command.call_args.args[0])
|
|
with patch.object(host, "command", side_effect=engine.Failure("command-failed")):
|
|
self.assertEqual(host.status("https://demo.example/api/health"), "000")
|
|
|
|
def test_malformed_or_unavailable_journal_is_not_no_crash(self):
|
|
host = engine.Host()
|
|
for text in ["not-json", '{"MESSAGE":"fine"}', '{"MESSAGE":[],"__CURSOR":"c"}']:
|
|
with self.subTest(text=text), patch.object(host, "command", return_value=text):
|
|
with self.assertRaises(engine.Failure):
|
|
host.check_journal({"pulse": "c"})
|
|
with patch.object(host, "command", return_value='{"MESSAGE":"panic: fixture","__CURSOR":"d"}'):
|
|
with self.assertRaisesRegex(engine.Failure, "new-service-crash"):
|
|
host.check_journal({"pulse": "c"})
|
|
|
|
def test_journal_cursor_advances_without_returning_private_message(self):
|
|
host = engine.Host(); cursors = {"pulse": "c"}
|
|
with patch.object(host, "command", return_value='{"MESSAGE":"private normal message","__CURSOR":"d"}'):
|
|
host.check_journal(cursors)
|
|
self.assertEqual(cursors, {"pulse": "d"})
|
|
|
|
def test_available_empty_journal_window_has_no_non_json_header(self):
|
|
host = engine.Host(); cursors = {"pulse": "c"}
|
|
with patch.object(host, "command", return_value="") as command:
|
|
host.check_journal(cursors)
|
|
self.assertIn("--quiet", command.call_args.args[0])
|
|
self.assertEqual(cursors, {"pulse": "c"})
|
|
|
|
def test_no_journal_cursor_and_malformed_service_state_fail_closed(self):
|
|
host = engine.Host()
|
|
with patch.object(host, "command", return_value=""):
|
|
with self.assertRaises(engine.Failure):
|
|
host.cursors()
|
|
with self.assertRaises(engine.Failure):
|
|
host.state()
|
|
|
|
|
|
class DispatchLifecycleTest(unittest.TestCase):
|
|
def test_observer_budget_covers_child_and_recovery_and_no_wait_owned_by_ssh(self):
|
|
source = dispatcher.BOOTSTRAP
|
|
self.assertIn("RuntimeMaxSec=45min", source)
|
|
self.assertIn("TimeoutStopSec=20min", source)
|
|
self.assertIn("KillMode=mixed", source)
|
|
self.assertIn("+ 4000", source)
|
|
self.assertGreater(4000, 45 * 60 + 20 * 60)
|
|
self.assertNotIn("'--wait'", source)
|
|
self.assertNotIn("systemctl', 'stop'", source)
|
|
|
|
def test_bootstrap_retains_intent_and_repeated_or_uncertain_launch_does_not_replay(self):
|
|
# Execute the real bootstrap with only its filesystem/root/systemd/clock
|
|
# adapters rebound, including a lost submission response. No host tools.
|
|
for uncertain in (False, True):
|
|
with self.subTest(uncertain=uncertain), tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory) / "state"
|
|
source = dispatcher.BOOTSTRAP.replace("'/var/lib/pulse-deploy/demo'", repr(str(root)))
|
|
source = source.replace("root.stat().st_uid == 0", "root.stat().st_uid == os.getuid()")
|
|
clock = [0]
|
|
calls = []
|
|
payload = {"request": request(), "source": "c291cmNl", "binary": "", "version_file": ""}
|
|
|
|
def launch(argv, **kwargs):
|
|
calls.append(argv)
|
|
attempt = next((root / "attempts").iterdir())
|
|
if uncertain:
|
|
raise subprocess.TimeoutExpired("systemd-run", 30)
|
|
(attempt / "receipt.json").write_text(json.dumps({"status": "committed"}))
|
|
return subprocess.CompletedProcess(argv, 0)
|
|
|
|
class Input:
|
|
def read(self, _limit):
|
|
return json.dumps(payload)
|
|
|
|
with patch.object(os, "geteuid", return_value=0), patch.object(subprocess, "run", side_effect=launch), \
|
|
patch.object(dispatcher.sys, "stdin", Input()), patch("time.monotonic", side_effect=lambda: clock[0]), \
|
|
patch("time.sleep", side_effect=lambda seconds: clock.__setitem__(0, clock[0] + seconds)), patch("builtins.print"):
|
|
for _ in range(2):
|
|
with self.assertRaises(SystemExit):
|
|
exec(compile(source, "fixed-bootstrap", "exec"), {})
|
|
self.assertEqual(len(calls), 1)
|
|
attempt = next((root / "attempts").iterdir())
|
|
self.assertTrue((attempt / "intent.json").exists())
|
|
self.assertEqual((attempt / "request.json").stat().st_mode & 0o777, 0o600)
|
|
self.assertEqual(attempt.stat().st_mode & 0o777, 0o700)
|
|
if uncertain:
|
|
self.assertEqual(json.loads((attempt / "launch.json").read_text())["state"], "uncertain")
|
|
|
|
|
|
class NativeAdmissionTest(unittest.TestCase):
|
|
def test_native_driver_refuses_wrong_context_before_any_service_command(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
result_path = Path(directory) / "result.json"
|
|
existing = Path(directory) / "existing-estate"
|
|
existing.mkdir()
|
|
cases = [(1000, "true", "github-hosted", []),
|
|
(0, "false", "github-hosted", []),
|
|
(0, "true", "self-hosted", []),
|
|
(0, "true", "github-hosted", [existing])]
|
|
for uid, actions, environment, estate in cases:
|
|
with self.subTest(uid=uid, actions=actions, environment=environment, occupied=bool(estate)), \
|
|
patch.object(native, "RESULT", result_path), patch.object(native, "OWNED", estate), \
|
|
patch.object(os, "geteuid", return_value=uid), \
|
|
patch.dict(os.environ, {"GITHUB_ACTIONS": actions, "RUNNER_ENVIRONMENT": environment}), \
|
|
patch.object(native.socket, "gethostname", return_value="disposable-fixture"), \
|
|
patch.object(native, "command", side_effect=AssertionError("must not execute a service command")) as command:
|
|
self.assertEqual(native.main(), 1)
|
|
command.assert_not_called()
|
|
result = json.loads(result_path.read_text())
|
|
self.assertFalse(result["passed"])
|
|
self.assertFalse(result["cleanup_complete"])
|
|
self.assertFalse(result["signed_published_installer_acceptance"])
|
|
self.assertEqual(result["failure_code"], "not-an-empty-disposable-public-ci-runner")
|
|
self.assertTrue(existing.is_dir())
|
|
|
|
def test_synthetic_changed_executables_have_actual_persistent_effects(self):
|
|
compile(native.binary("1.0.1"), "fixture-runtime", "exec")
|
|
self.assertNotEqual(native.binary("1.0.1"), native.binary("1.0.2", 55))
|
|
self.assertIn(b"persistent-marker", native.binary("1.0.1"))
|
|
self.assertNotIn(b"curl", native.binary("1.0.1"))
|
|
|
|
|
|
|
|
class RuntimeSelectionTest(unittest.TestCase):
|
|
def archive(self, directory, entries):
|
|
path = Path(directory) / 'release.tgz'
|
|
with tarfile.open(path, 'w:gz') as stream:
|
|
for name, content, kind in entries:
|
|
member = tarfile.TarInfo(name); member.size = len(content); member.type = kind
|
|
if kind == tarfile.SYMTYPE:
|
|
member.linkname = '/outside'; member.size = 0
|
|
stream.addfile(member)
|
|
else:
|
|
stream.addfile(member, io.BytesIO(content))
|
|
return path
|
|
|
|
def elf(self):
|
|
body = bytearray(64); body[:6] = b'\x7fELF\x02\x01'; body[18:20] = b'\x3e\x00'
|
|
return bytes(body)
|
|
|
|
def test_selects_only_signed_archive_runtime_without_extracting_paths(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
entries = [('bin/pulse', self.elf(), tarfile.REGTYPE), ('./VERSION', b'6.4.6\n', tarfile.REGTYPE),
|
|
('../outside', b'untrusted path', tarfile.REGTYPE),
|
|
('scripts/install.sh', b'never executed', tarfile.REGTYPE),
|
|
('bin/pulse-agent', b'not installed', tarfile.REGTYPE)]
|
|
files = engine.runtime_members(self.archive(directory, entries), 'v6.4.6')
|
|
self.assertEqual(set(files), {'binary', 'version'})
|
|
self.assertEqual(files['binary'], self.elf())
|
|
self.assertEqual([p.name for p in Path(directory).iterdir()], ['release.tgz'])
|
|
|
|
def test_duplicate_link_wrong_version_or_architecture_is_rejected(self):
|
|
valid = [('bin/pulse', self.elf(), tarfile.REGTYPE), ('VERSION', b'6.4.6\n', tarfile.REGTYPE)]
|
|
cases = [valid + [('pulse', self.elf(), tarfile.REGTYPE)],
|
|
[valid[0], ('VERSION', b'', tarfile.SYMTYPE)],
|
|
[valid[0], ('VERSION', b'6.4.7\n', tarfile.REGTYPE)],
|
|
[('bin/pulse', b'#!/bin/sh\n', tarfile.REGTYPE), valid[1]], [valid[0]]]
|
|
for entries in cases:
|
|
with self.subTest(entries=[x[0] for x in entries]), tempfile.TemporaryDirectory() as directory:
|
|
with self.assertRaises(engine.Failure):
|
|
engine.runtime_members(self.archive(directory, entries), 'v6.4.6')
|
|
|
|
|
|
class NativeGateTest(unittest.TestCase):
|
|
def row(self, run_id=10, **fields):
|
|
return {'id': run_id, 'head_sha': 'a' * 40, 'run_attempt': 1,
|
|
'created_at': '2026-10-03T01:00:00Z', 'updated_at': '2026-10-03T01:10:00Z',
|
|
'path': '.github/workflows/demo-runtime-native.yml', 'event': 'push', 'head_branch': 'main',
|
|
'status': 'completed', 'conclusion': 'success', **fields}
|
|
|
|
def check(self, rows, history=None, read_exit=0, same=True):
|
|
calls = []
|
|
def command(argv, **kwargs):
|
|
calls.append(argv)
|
|
if argv[0] == 'gh':
|
|
payload = rows if 'created=' in argv[-1] else (history if history is not None else rows)
|
|
return subprocess.CompletedProcess(argv, read_exit, json.dumps({'workflow_runs': payload}).encode())
|
|
return subprocess.CompletedProcess(argv, 0 if argv[1] == 'merge-base' or same else 1)
|
|
with patch.object(dispatcher.subprocess, 'run', side_effect=command):
|
|
result = dispatcher.require_native({'GITHUB_SHA': 'b' * 40, 'GITHUB_REPOSITORY': 'rcourtman/Pulse'})
|
|
return result, calls
|
|
|
|
def test_matching_reviewed_native_source_passes_with_current_first_inventory(self):
|
|
result, calls = self.check([self.row()])
|
|
self.assertEqual(result['run_id'], 10)
|
|
reads = [c for c in calls if c[0] == 'gh']
|
|
self.assertEqual(len(reads), 2)
|
|
self.assertIn('created=', reads[0][-1]); self.assertNotIn('created=', reads[1][-1])
|
|
self.assertEqual(set(calls[-1][6:]), set(dispatcher.NATIVE_PATHS))
|
|
|
|
def test_newer_matching_failure_or_pending_run_cannot_use_older_green(self):
|
|
for fields in ({'conclusion': 'failure'}, {'status': 'in_progress', 'conclusion': None}):
|
|
with self.subTest(fields=fields), self.assertRaisesRegex(ValueError, 'not-passed'):
|
|
self.check([self.row(11, **fields), self.row(10)])
|
|
|
|
def test_latest_duplicate_attempt_retains_failure(self):
|
|
with self.assertRaisesRegex(ValueError, 'not-passed'):
|
|
self.check([self.row()], [self.row(run_attempt=2, conclusion='failure')])
|
|
|
|
def test_missing_changed_or_misidentified_proof_cannot_enable_mutation(self):
|
|
for rows, same in (([], True), ([self.row()], False), ([self.row(event='workflow_dispatch')], True)):
|
|
with self.subTest(rows=rows, same=same), self.assertRaises(ValueError):
|
|
self.check(rows, same=same)
|
|
|
|
def test_refused_read_has_no_history_fallback_or_ssh(self):
|
|
calls = []
|
|
def denied(argv, **kwargs):
|
|
calls.append(argv); return subprocess.CompletedProcess(argv, 1, b'')
|
|
with patch.object(dispatcher.subprocess, 'run', side_effect=denied), self.assertRaises(ValueError):
|
|
dispatcher.require_native({'GITHUB_SHA': 'b' * 40, 'GITHUB_REPOSITORY': 'rcourtman/Pulse'})
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertEqual(calls[0][0:2], ['gh', 'api'])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|