fix(agent99): deploy backup freshness exporter atomically

This commit is contained in:
Your Name
2026-07-18 21:33:50 +08:00
parent 4b2b73f1a7
commit 98b0682a6e
5 changed files with 122 additions and 41 deletions

View File

@@ -64,6 +64,7 @@ def test_windows99_broker_fetches_exact_gitea_revision_and_is_bounded() -> None:
assert '"checkout", "--quiet", $SourceRevision, "--"' in source
assert 'executorSha256 = $executorDigest' in source
assert 'verifierSha256 = $verifierDigest' in source
assert '"scripts/ops/backup-health-textfile-exporter.py"' in source
assert "/home/wooo/awoooi" not in source
assert '"StrictHostKeyChecking=yes"' in source
assert '"NumberOfPasswordPrompts=0"' in source
@@ -98,6 +99,9 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
assert 'readonly EXPECTED_HOST_IP="192.168.0.110"' in source
assert 'readonly EXPECTED_USER="wooo"' in source
assert 'readonly DEST_ROOT="/backup/scripts"' in source
assert 'readonly EXPORTER_ROOT="/home/wooo/scripts"' in source
assert 'readonly EXPORTER_FILE="backup-health-textfile-exporter.py"' in source
assert 'readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")' in source
assert '--source-stage' in source
assert 'document.get("executorSha256"' in source
assert 'document.get("verifierSha256"' in source
@@ -105,16 +109,20 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
assert 'verifier_identity_failed' in source
assert 'flock -x -w 30 9' in source
assert 'legacy_backup_runtime_active' in source
assert source.index("for name in \"${RUNTIME_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
assert source.index("for name in \"${PAYLOAD_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
assert 'rollback_unverified' in source
assert 'failed_rolled_back' in source
assert 'working_digest "$dest_path"' in source
assert 'PREVIOUS_METADATA' in source
assert "stat -c '%u:%g:%a'" in source
assert 'os.link(temporary, path)' in source
assert 'run_identity_receipt_exists' in source
assert 'run_identity_stage_exists' in source
assert 'run_identity_rollback_exists' in source
assert 'verify-host110-backup-runtime.py' in source
assert 'selfIdentityVerified' in source
assert '"backupScriptFileCount": 17' in source
assert '"backupHealthExporterIncluded": True' in source
assert '"productionServiceRestarted": False' in source
assert '"secretValuesRead": False' in source
assert "sudo " not in source
@@ -139,12 +147,16 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
verifier = _load_verifier()
destination = tmp_path / "scripts"
destination.mkdir()
exporter_destination = tmp_path / verifier.EXPORTER_FILE
rows = []
for name in verifier.EXPECTED_FILES:
for name in verifier.EXPECTED_BACKUP_FILES:
path = destination / name
path.write_text(f"runtime:{name}\n", encoding="utf-8")
path.chmod(0o755)
rows.append({"name": name, "sha256": _sha256(path)})
exporter_destination.write_text("exporter\n", encoding="utf-8")
exporter_destination.chmod(0o755)
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
verifier_digest = _sha256(VERIFIER)
source_revision = "a" * 40
run_id = "unit-exact"
@@ -159,8 +171,12 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
}
manifest_path = tmp_path / "manifest.json"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
before = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
before = {
path.name: (path.stat().st_mtime_ns, _sha256(path))
for path in [*destination.iterdir(), exporter_destination]
}
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
monkeypatch.setattr(
sys,
"argv",
@@ -170,6 +186,8 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
str(manifest_path),
"--destination",
str(destination),
"--exporter-destination",
str(exporter_destination),
"--source-revision",
source_revision,
"--run-id",
@@ -182,8 +200,14 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
verifier.main()
result = json.loads(capsys.readouterr().out)
after = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
after = {
path.name: (path.stat().st_mtime_ns, _sha256(path))
for path in [*destination.iterdir(), exporter_destination]
}
assert result["ok"] is True
assert result["fileCount"] == 18
assert result["backupScriptFileCount"] == 17
assert result["backupHealthExporterIncluded"] is True
assert result["remoteWritePerformed"] is False
assert result["selfIdentityVerified"] is True
assert before == after
@@ -193,13 +217,17 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
verifier = _load_verifier()
destination = tmp_path / "scripts"
destination.mkdir()
exporter_destination = tmp_path / verifier.EXPORTER_FILE
rows = []
for name in verifier.EXPECTED_FILES:
for name in verifier.EXPECTED_BACKUP_FILES:
path = destination / name
path.write_text(f"runtime:{name}\n", encoding="utf-8")
path.chmod(0o755)
rows.append({"name": name, "sha256": _sha256(path)})
(destination / verifier.EXPECTED_FILES[0]).write_text("drifted\n", encoding="utf-8")
exporter_destination.write_text("exporter\n", encoding="utf-8")
exporter_destination.chmod(0o755)
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
(destination / verifier.EXPECTED_BACKUP_FILES[0]).write_text("drifted\n", encoding="utf-8")
verifier_digest = _sha256(VERIFIER)
source_revision = "d" * 40
run_id = "unit-drift"
@@ -214,6 +242,7 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
}
encoded = base64.b64encode(json.dumps(manifest).encode()).decode()
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
monkeypatch.setattr(
sys,
"argv",
@@ -223,6 +252,8 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
encoded,
"--destination",
str(destination),
"--exporter-destination",
str(exporter_destination),
"--source-revision",
source_revision,
"--run-id",