Files
awoooi/scripts/reboot-recovery/tests/test_host112_recovery_runtime_harness.py
ogt a6307e785d
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 3m41s
CD Pipeline / build-and-deploy (push) Successful in 7m17s
CD Pipeline / post-deploy-checks (push) Successful in 1m59s
feat(automation): harden D037 runtime closure receipts
2026-07-14 14:32:43 +08:00

680 lines
23 KiB
Python

from __future__ import annotations
import os
import signal
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
READINESS = ROOT / "scripts/reboot-recovery/host112-guest-readiness.sh"
def _write_executable(path: Path, body: str) -> None:
path.write_text(body, encoding="utf-8")
path.chmod(0o755)
def _runtime_harness(
tmp_path: Path,
*,
ledger_capacity: int = 128,
) -> tuple[Path, dict[str, str], Path]:
source = READINESS.read_text(encoding="utf-8")
root_gate = 'if [ "${EUID:-$(id -u)}" -ne 0 ]; then'
assert source.count(root_gate) == 1
assert source.count("timer_ledger_capacity=128") == 1
source = source.replace(root_gate, "if false; then", 1)
source = source.replace(
"timer_ledger_capacity=128",
f"timer_ledger_capacity={ledger_capacity}",
1,
)
script = tmp_path / "host112-runtime-harness.sh"
_write_executable(script, source)
fake_bin = tmp_path / "bin"
fake_bin.mkdir()
fake_state = tmp_path / "fake-host"
fake_state.mkdir()
_write_executable(
fake_bin / "timeout",
"""#!/usr/bin/env bash
set -u
while [ "$#" -gt 0 ]; do
case "$1" in
--signal=*|--kill-after=*) shift ;;
*) break ;;
esac
done
[ "$#" -gt 0 ] || exit 64
shift
"$@"
""",
)
_write_executable(
fake_bin / "date",
"""#!/usr/bin/env bash
case "${1:-}" in
--iso-8601=seconds) printf '%s\n' '2026-07-14T12:00:00+08:00' ;;
+%s) printf '%s\n' '1000' ;;
*) /bin/date "$@" ;;
esac
""",
)
_write_executable(
fake_bin / "flock",
"""#!/usr/bin/env bash
exit 0
""",
)
_write_executable(
fake_bin / "mv",
"""#!/usr/bin/env bash
set -u
destination=""
for argument in "$@"; do destination="$argument"; done
if [ -n "${FAKE_RECEIPT_MV_MARKER:-}" ] && [[ "$destination" == *.receipt.txt ]]; then
: >"$FAKE_RECEIPT_MV_MARKER"
sleep "${FAKE_RECEIPT_MV_SLEEP_SECONDS:-0}"
fi
/bin/mv "$@"
""",
)
_write_executable(
fake_bin / "ln",
"""#!/usr/bin/env bash
set -u
destination=""
for argument in "$@"; do destination="$argument"; done
if [ "${FAKE_RECEIPT_LINK_FAIL:-0}" = "1" ] && [[ "$destination" == */receipts/*.txt ]]; then
exit 1
fi
if [ "${FAKE_READBACK_LINK_FAIL:-0}" = "1" ] && [[ "$destination" == */readbacks/*.txt ]] && [[ "$destination" != *.signal.txt ]]; then
exit 1
fi
/bin/ln "$@"
""",
)
_write_executable(
fake_bin / "pgrep",
"""#!/usr/bin/env bash
case "$*" in
*wazuh-analysisd*) printf '%s\n' '1'; exit 0 ;;
*Xorg*|*Xwayland*) exit 0 ;;
*) exit 1 ;;
esac
""",
)
_write_executable(
fake_bin / "ss",
"""#!/usr/bin/env bash
case "$*" in
*-ltn*)
for port in 22 1514 1515 55000; do
printf 'LISTEN 0 128 0.0.0.0:%s 0.0.0.0:*\n' "$port"
done
;;
*-lun*) printf '%s\n' 'UNCONN 0 0 0.0.0.0:1514 0.0.0.0:*' ;;
esac
""",
)
_write_executable(
fake_bin / "df",
"""#!/usr/bin/env bash
printf '%s\n' 'Filesystem 1024-blocks Used Available Capacity Mounted on'
printf '%s\n' '/dev/fake 10000000 1000 9999000 1% /'
""",
)
_write_executable(
fake_bin / "systemctl",
"""#!/usr/bin/env bash
set -u
state="${FAKE_HOST_STATE_DIR:?}"
require_wal() {
if [ ! -f "${HOST112_RECOVERY_STATE_DIR:?}/active-artifact-transaction.txt" ]; then
: >"$state/mutation_without_wal"
fi
}
cmd="${1:-}"
[ "$#" -gt 0 ] && shift
case "$cmd" in
show)
unit="${1:-}"; shift || true
property=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-p" ]; then property="${2:-}"; shift 2; else shift; fi
done
case "$property" in
LoadState) printf '%s\n' 'loaded' ;;
ActiveState) printf '%s\n' 'active' ;;
SubState) printf '%s\n' 'running' ;;
Result) printf '%s\n' 'success' ;;
*) printf '%s\n' 'unknown' ;;
esac
;;
is-active)
unit="${1:-}"
case "$unit" in
lightdm.service|display-manager.service)
if [ -f "$state/lightdm_active" ]; then printf '%s\n' 'active'; else printf '%s\n' 'inactive'; fi
;;
*) printf '%s\n' 'active' ;;
esac
;;
is-enabled) printf '%s\n' 'enabled' ;;
is-system-running)
if [ -n "${FAKE_FINALIZE_MARKER:-}" ]; then
: >"$FAKE_FINALIZE_MARKER"
sleep "${FAKE_FINALIZE_SLEEP_SECONDS:-0}"
fi
printf '%s\n' 'running'
;;
get-default) printf '%s\n' 'graphical.target' ;;
set-default)
require_wal
printf 'set-default:%s\n' "${1:-}" >>"$state/mutations.log"
exit "${FAKE_SET_DEFAULT_RC:-0}"
;;
start)
require_wal
unit="${1:-}"
printf 'start:%s\n' "$unit" >>"$state/mutations.log"
if [ "$unit" = "lightdm.service" ]; then
if [ "${FAKE_PARTIAL_LIGHTDM_START:-0}" = "1" ]; then : >"$state/lightdm_active"; fi
rc="${FAKE_LIGHTDM_START_RC:-0}"
if [ "$rc" -eq 0 ]; then : >"$state/lightdm_active"; fi
exit "$rc"
fi
;;
enable)
require_wal
printf 'enable:%s\n' "${1:-}" >>"$state/mutations.log"
;;
reset-failed|stop) require_wal ;;
*) exit 1 ;;
esac
""",
)
env = os.environ.copy()
env.update(
{
"PATH": f"{fake_bin}:{env['PATH']}",
"FAKE_HOST_STATE_DIR": str(fake_state),
"HOST112_RECOVERY_STATE_DIR": str(tmp_path / "state"),
"HOST112_INDEXER_RETRY_MARKER": str(tmp_path / "indexer.epoch"),
"HOST112_MANAGER_RETRY_MARKER": str(tmp_path / "manager.epoch"),
"HOST112_MANAGER_LOCK_FILE": str(tmp_path / "manager.lock"),
}
)
return script, env, fake_state
def _apply(script: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
"bash",
str(script),
"--apply",
"--trace-id",
"trace:runtime-harness",
"--run-id",
"run:runtime-harness",
"--work-item-id",
"HOST112-RUNTIME-HARNESS",
],
text=True,
capture_output=True,
env=env,
check=False,
timeout=15,
)
def _dry_run(script: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
"bash",
str(script),
"--dry-run",
"--trace-id",
"trace:runtime-harness",
"--run-id",
"run:runtime-harness",
"--work-item-id",
"HOST112-RUNTIME-HARNESS",
],
text=True,
capture_output=True,
env=env,
check=False,
timeout=15,
)
def test_post_apply_dry_run_read_only_verifies_fixed_wal_and_immutable_pair(
tmp_path: Path,
) -> None:
script, env, _ = _runtime_harness(tmp_path)
applied = _apply(script, env)
assert applied.returncode == 0, applied.stdout + applied.stderr
transaction = tmp_path / "state" / "active-artifact-transaction.txt"
transaction_before = transaction.read_bytes()
verified = _dry_run(script, env)
output = verified.stdout + verified.stderr
assert verified.returncode == 0, output
assert "durable_readback_status=loaded_immutable_verified " in output
assert "artifact_transaction_status=existing_pair_verified" in output
assert "artifact_transaction_started=0" in output
assert "artifact_transaction_blocked=0" in output
assert "artifact_transaction_write_performed=0" in output
assert "artifact_transaction_pair_verified=1" in output
assert "artifact_transaction_prior_pair_verified=1" in output
assert transaction.read_bytes() == transaction_before
def test_pre_apply_dry_run_blocks_unresolved_prior_transaction(
tmp_path: Path,
) -> None:
script, env, fake_state = _runtime_harness(tmp_path)
state = tmp_path / "state"
state.mkdir()
transaction = state / "active-artifact-transaction.txt"
unresolved = (
"schema_version=host112_active_artifact_transaction_v1 "
"updated_at=2026-07-14T12:01:00+08:00 "
"status=pending_artifact_commit phase=prior_runtime_mutation "
"trace_id=trace:prior run_id=run:prior "
"work_item_id=HOST112-PRIOR boot_id=boot:prior "
f"receipt_path={state / 'receipts' / 'prior.txt'} "
f"readback_path={state / 'readbacks' / 'prior.txt'} "
"pair_verified=0\n"
)
transaction.write_text(unresolved, encoding="utf-8")
Path(env["HOST112_MANAGER_LOCK_FILE"]).touch()
dry_run = _dry_run(script, env)
output = dry_run.stdout + dry_run.stderr
assert dry_run.returncode == 1, output
assert "artifact_transaction_status=blocked_unresolved_prior_transaction" in output
assert "artifact_transaction_blocked=1" in output
assert "runtime_write_performed=0" in output
assert "receipt_write_performed=0" in output
assert transaction.read_text(encoding="utf-8") == unresolved
assert not (fake_state / "mutations.log").exists()
def test_failed_general_run_cannot_be_washed_green_by_external_recovery(
tmp_path: Path,
) -> None:
script, env, fake_state = _runtime_harness(tmp_path)
env["FAKE_LIGHTDM_START_RC"] = "124"
env["FAKE_PARTIAL_LIGHTDM_START"] = "1"
failed = _apply(script, env)
failed_output = failed.stdout + failed.stderr
assert failed.returncode == 1
assert "runtime_write_performed=1" in failed_output
assert "receipt_status=written_immutable" in failed_output
assert "durable_readback_status=written_immutable " in failed_output
assert (
"durable_receipt_terminal=execution_failed_or_unattributed_"
"idempotent_already_verified_healthy"
) in failed_output
assert "guest_ready=1" in failed_output
assert "action_failure_observed=1" in failed_output
assert "artifact_transaction_status=committed_verified_pair" in failed_output
assert "artifact_transaction_pair_verified=1" in failed_output
assert not (fake_state / "mutation_without_wal").exists()
receipt = next((tmp_path / "state" / "receipts").glob("*.txt"))
readback = next((tmp_path / "state" / "readbacks").glob("*.txt"))
stored_readback = readback.read_text(encoding="utf-8")
for derived in (
"durable_readback_present=1",
"durable_readback_identity_match=1",
"durable_readback_boot_match=1",
"durable_readback_receipt_link_match=1",
"durable_readback_terminal_match=1",
):
assert derived not in stored_readback
receipt_before = receipt.read_bytes()
readback_before = readback.read_bytes()
(fake_state / "lightdm_active").touch()
env.pop("FAKE_LIGHTDM_START_RC")
env.pop("FAKE_PARTIAL_LIGHTDM_START")
replay = _apply(script, env)
replay_output = replay.stdout + replay.stderr
assert replay.returncode == 1
assert "guest_ready=1" in replay_output
assert "receipt_status=reused_immutable" in replay_output
assert "durable_readback_status=reused_immutable_verified" in replay_output
assert "terminal=immutable_receipt_replay" in replay_output
assert receipt.read_bytes() == receipt_before
assert readback.read_bytes() == readback_before
def test_immutable_replay_cannot_ignore_newer_unresolved_fixed_transaction(
tmp_path: Path,
) -> None:
script, env, fake_state = _runtime_harness(tmp_path)
first = _apply(script, env)
assert first.returncode == 0, first.stdout + first.stderr
state = tmp_path / "state"
receipt = next((state / "receipts").glob("*.txt"))
readback = next((state / "readbacks").glob("*.txt"))
receipt_before = receipt.read_bytes()
readback_before = readback.read_bytes()
transaction = state / "active-artifact-transaction.txt"
unresolved = (
"schema_version=host112_active_artifact_transaction_v1 "
"updated_at=2026-07-14T12:01:00+08:00 "
"status=pending_artifact_commit phase=newer_runtime_mutation "
"trace_id=trace:newer run_id=run:newer "
"work_item_id=HOST112-NEWER boot_id=boot:newer "
f"receipt_path={state / 'receipts' / 'newer.txt'} "
f"readback_path={state / 'readbacks' / 'newer.txt'} "
"pair_verified=0\n"
)
transaction.write_text(unresolved, encoding="utf-8")
mutations = fake_state / "mutations.log"
mutations_before = mutations.read_bytes()
replay = _apply(script, env)
output = replay.stdout + replay.stderr
assert replay.returncode == 1, output
assert "receipt_status=reused_immutable" in output
assert "artifact_transaction_status=blocked_unresolved_prior_transaction" in output
assert "artifact_transaction_blocked=1" in output
assert "artifact_transaction_pair_verified=0" in output
assert transaction.read_text(encoding="utf-8") == unresolved
assert receipt.read_bytes() == receipt_before
assert readback.read_bytes() == readback_before
assert mutations.read_bytes() == mutations_before
def test_receipt_failure_leaves_durable_partial_without_readback_and_blocks_retry(
tmp_path: Path,
) -> None:
script, env, fake_state = _runtime_harness(tmp_path)
env.update(
{
"FAKE_LIGHTDM_START_RC": "124",
"FAKE_RECEIPT_LINK_FAIL": "1",
}
)
failed = _apply(script, env)
output = failed.stdout + failed.stderr
assert failed.returncode == 1
assert "runtime_write_performed=1" in output
assert "receipt_status=write_failed" in output
assert "artifact_transaction_status=partial_pending_public_artifact_commit" in output
transaction = tmp_path / "state" / "active-artifact-transaction.txt"
stored = transaction.read_text(encoding="utf-8")
assert "status=partial_pending_public_artifact_commit" in stored
assert "phase=receipt_write_or_disk_verification_failed" in stored
assert not (tmp_path / "state" / "readbacks").exists()
assert not (fake_state / "mutation_without_wal").exists()
mutations = fake_state / "mutations.log"
mutations.write_text("", encoding="utf-8")
retry_env = env.copy()
retry_env.pop("FAKE_RECEIPT_LINK_FAIL")
retry = _apply(script, retry_env)
retry_output = retry.stdout + retry.stderr
assert retry.returncode == 1
assert "schema_version=host112_guest_recovery_v2" in retry_output
assert "artifact_transaction_status=blocked_unresolved_prior_transaction" in (
retry_output
)
assert "terminal=execution_failed_or_unattributed_" in retry_output
assert "artifact_transaction_blocked" in retry_output
assert "receipt_write_performed=0" in retry_output
assert mutations.read_text(encoding="utf-8") == ""
def test_readback_failure_keeps_receipt_and_durable_partial_not_false_success(
tmp_path: Path,
) -> None:
script, env, fake_state = _runtime_harness(tmp_path)
env.update(
{
"FAKE_LIGHTDM_START_RC": "124",
"FAKE_READBACK_LINK_FAIL": "1",
}
)
failed = _apply(script, env)
output = failed.stdout + failed.stderr
assert failed.returncode == 1
assert "receipt_status=written_immutable" in output
assert "durable_readback_status=write_failed" in output
assert "artifact_transaction_pair_verified=0" in output
receipt = next((tmp_path / "state" / "receipts").glob("*.txt"))
assert "terminal=" in receipt.read_text(encoding="utf-8")
assert not list((tmp_path / "state" / "readbacks").glob("*.txt"))
transaction = tmp_path / "state" / "active-artifact-transaction.txt"
stored = transaction.read_text(encoding="utf-8")
assert "status=partial_pending_public_artifact_commit" in stored
assert "phase=durable_readback_write_or_disk_verification_failed" in stored
assert not (fake_state / "mutation_without_wal").exists()
def test_repeated_timer_noop_and_mutation_artifacts_are_actually_bounded(
tmp_path: Path,
) -> None:
noop_root = tmp_path / "noop"
noop_root.mkdir()
script, env, fake_state = _runtime_harness(noop_root, ledger_capacity=4)
(fake_state / "lightdm_active").touch()
for index in range(5):
run_env = env | {"INVOCATION_ID": f"noop-{index}"}
result = subprocess.run(
["bash", str(script), "--apply-timer"],
text=True,
capture_output=True,
env=run_env,
check=False,
timeout=15,
)
assert result.returncode == 0, result.stdout + result.stderr
state = noop_root / "state"
assert [path.relative_to(state) for path in state.rglob("*") if path.is_file()] == [
Path("timer-observation.latest")
]
assert "repeat_count=5" in (state / "timer-observation.latest").read_text(
encoding="utf-8"
)
mutation_root = tmp_path / "mutation"
mutation_root.mkdir()
script, env, _ = _runtime_harness(mutation_root, ledger_capacity=4)
env["FAKE_LIGHTDM_START_RC"] = "124"
mutation_slots: list[str] = []
# These five identities cover every four-slot ring position and then reuse
# one slot. That proves both capacity coverage and rollover without twelve
# expensive full shell-harness invocations.
for index in range(5):
run_env = env | {"INVOCATION_ID": f"slot-{index}"}
result = subprocess.run(
["bash", str(script), "--apply-timer"],
text=True,
capture_output=True,
env=run_env,
check=False,
timeout=15,
)
output = result.stdout + result.stderr
assert result.returncode == 1
assert "runtime_write_performed=1" in output
assert "receipt_status=written_bounded_timer_slot" in output
assert "durable_readback_status=written_bounded_timer_slot " in output
mutation_slots.append(
next(
token.split("=", 1)[1]
for token in output.split()
if token.startswith("timer_ledger_slot=")
)
)
for field in (
"durable_readback_present",
"durable_readback_identity_match",
"durable_readback_boot_match",
"durable_readback_receipt_link_match",
"durable_readback_terminal_match",
):
assert f"{field}=1" in output
assert f"{field}=0" not in output
assert "timer_no_runtime_mutation_aggregated" not in output
assert len(set(mutation_slots)) == 4
assert len(mutation_slots) > len(set(mutation_slots))
ledger_files = [
path
for path in (mutation_root / "state" / "timer-ledger").iterdir()
if path.is_file()
]
assert len(ledger_files) == 8
assert not (mutation_root / "state" / "receipts").exists()
assert not (mutation_root / "state" / "readbacks").exists()
for readback in (mutation_root / "state" / "timer-ledger").glob(
"*.readback.txt"
):
stored = readback.read_text(encoding="utf-8")
for field in (
"durable_readback_present",
"durable_readback_identity_match",
"durable_readback_boot_match",
"durable_readback_receipt_link_match",
"durable_readback_terminal_match",
):
assert f"{field}=1" not in stored
assert f"{field}=0" not in stored
transaction = mutation_root / "state" / "active-artifact-transaction.txt"
assert "status=committed_verified_pair" in transaction.read_text(encoding="utf-8")
def test_timer_sigkill_during_receipt_commit_leaves_only_bounded_slot_temps(
tmp_path: Path,
) -> None:
script, env, _ = _runtime_harness(tmp_path, ledger_capacity=4)
env["FAKE_LIGHTDM_START_RC"] = "124"
marker = tmp_path / "receipt-mv.entered"
run_env = env | {
"INVOCATION_ID": "sigkill-0",
"FAKE_RECEIPT_MV_MARKER": str(marker),
"FAKE_RECEIPT_MV_SLEEP_SECONDS": "5",
}
process = subprocess.Popen(
["bash", str(script), "--apply-timer"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=run_env,
start_new_session=True,
)
deadline = time.monotonic() + 8
while not marker.exists() and time.monotonic() < deadline:
time.sleep(0.01)
assert marker.exists()
os.killpg(process.pid, signal.SIGKILL)
process.communicate(timeout=5)
assert process.returncode == -signal.SIGKILL
ledger = tmp_path / "state" / "timer-ledger"
files = [path for path in ledger.iterdir() if path.is_file()]
assert len(files) == 1
assert all(path.name.endswith(".receipt.txt.tmp") for path in files)
transaction = tmp_path / "state" / "active-artifact-transaction.txt"
assert "status=pending_artifact_commit" in transaction.read_text(encoding="utf-8")
mutations = tmp_path / "fake-host" / "mutations.log"
before = mutations.read_bytes()
retry = subprocess.run(
["bash", str(script), "--apply-timer"],
text=True,
capture_output=True,
env=env | {"INVOCATION_ID": "sigkill-retry"},
check=False,
timeout=15,
)
assert retry.returncode == 1
assert "artifact_transaction_status=blocked_unresolved_prior_transaction" in (
retry.stdout + retry.stderr
)
assert mutations.read_bytes() == before
def test_signal_during_final_verifier_writes_correlated_failure_chain(
tmp_path: Path,
) -> None:
script, env, _ = _runtime_harness(tmp_path)
marker = tmp_path / "final-verifier.entered"
env.update(
{
"FAKE_FINALIZE_MARKER": str(marker),
"FAKE_FINALIZE_SLEEP_SECONDS": "2",
}
)
process = subprocess.Popen(
[
"bash",
str(script),
"--apply",
"--trace-id",
"trace:runtime-harness",
"--run-id",
"run:runtime-harness",
"--work-item-id",
"HOST112-RUNTIME-HARNESS",
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
deadline = time.monotonic() + 8
while not marker.exists() and time.monotonic() < deadline:
time.sleep(0.02)
assert marker.exists()
os.kill(process.pid, signal.SIGTERM)
stdout, stderr = process.communicate(timeout=12)
assert process.returncode == 1, stdout + stderr
receipt = next((tmp_path / "state" / "receipts").glob("*.txt"))
signal_readback = next((tmp_path / "state" / "readbacks").glob("*.signal.txt"))
receipt_text = receipt.read_text(encoding="utf-8")
signal_text = signal_readback.read_text(encoding="utf-8")
assert "terminal=interrupted_signal_TERM" in receipt_text
assert "action_count=1 actions=start_lightdm" in receipt_text
assert f"receipt_path={receipt}" in signal_text
assert "terminal=interrupted_signal_TERM" in signal_text
assert "schema_version=host112_guest_recovery_signal_readback_v1" in signal_text
transaction = tmp_path / "state" / "active-artifact-transaction.txt"
transaction_text = transaction.read_text(encoding="utf-8")
assert "status=partial_pending_public_artifact_commit" in transaction_text
assert "phase=interrupted_before_durable_readback_TERM" in transaction_text
assert not [
path
for path in (tmp_path / "state" / "readbacks").glob("*.txt")
if not path.name.endswith(".signal.txt")
]
replay = _apply(script, env | {"FAKE_FINALIZE_MARKER": ""})
assert replay.returncode == 65
assert "BLOCKER durable_readback_missing_or_mismatched_for_receipt" in (
replay.stdout + replay.stderr
)