from __future__ import annotations import importlib.util import sys from pathlib import Path SCRIPT_ROOT = Path(__file__).resolve().parents[1] EXPORTER_PATH = SCRIPT_ROOT / "backup-health-textfile-exporter.py" def load_exporter(): spec = importlib.util.spec_from_file_location("backup_health_textfile_exporter", EXPORTER_PATH) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def test_default_gitea_bundle_expected_repos_cover_current_project_set() -> None: exporter = load_exporter() assert exporter.DEFAULT_GITEA_BUNDLE_EXPECTED_REPOS == [ "wooo/awoooi", "wooo/ewoooc", "wooo/2026FIFAWorldCup", "wooo/agent-bounty-protocol", "wooo/AwoooGo", "wooo/stockplatform-v2", "wooo/vibework", "wooo/momo-pro-system", "wooo/tsenyang-website", "wooo/clawbot-v5", "wooo/bitan-pharmacy", "wooo/vtuber", ] assert any( row["alertname"] == "GiteaRestoreDrillMissingOrFailed" for row in exporter.BACKUP_ALERT_RECEIPT_REQUIREMENTS ) def test_gitea_restore_drill_metrics_require_inventory_and_artifacts(tmp_path: Path, monkeypatch) -> None: exporter = load_exporter() status_file = tmp_path / "gitea-restore-drill.status" status_file.write_text( "\n".join( [ "timestamp=2000000000", "success=1", "reason=verified", "dump_size_bytes=6928031514", "backup_regular_repo_count=15", "backup_wiki_repo_count=0", "live_regular_repo_count=15", "live_wiki_repo_count=0", "database_dump_present=1", "config_present=2", "archive_crc_ok=1", "inventory_digest_match=1", ] ) + "\n", encoding="utf-8", ) monkeypatch.setattr(exporter, "GITEA_RESTORE_DRILL_STATUS_FILE", status_file) monkeypatch.setattr(exporter.time, "time", lambda: 2000000060) rendered = "\n".join(exporter._gitea_restore_drill_metric_lines("110")) assert 'awoooi_gitea_restore_drill_fresh{host="110",max_age_hours="744"} 1' in rendered assert 'awoooi_gitea_restore_drill_verified{host="110",max_age_hours="744"} 1' in rendered assert 'awoooi_gitea_restore_drill_inventory_match{host="110",max_age_hours="744"} 1' in rendered status_file.write_text( status_file.read_text(encoding="utf-8").replace("inventory_digest_match=1", "inventory_digest_match=0"), encoding="utf-8", ) rendered = "\n".join(exporter._gitea_restore_drill_metric_lines("110")) assert 'awoooi_gitea_restore_drill_fresh{host="110",max_age_hours="744"} 0' in rendered assert 'awoooi_gitea_restore_drill_verified{host="110",max_age_hours="744"} 0' in rendered def test_gitea_bundle_metrics_require_all_expected_repos(tmp_path: Path, monkeypatch) -> None: exporter = load_exporter() bundle_root = tmp_path / "latest-private-complete" bundle_root.mkdir() monkeypatch.setattr(exporter, "GITEA_BUNDLE_ROOT", bundle_root) monkeypatch.setattr(exporter, "GITEA_BUNDLE_MAX_AGE_HOURS", 25) monkeypatch.setenv("AIOPS_GITEA_BUNDLE_EXPECTED_REPOS", "wooo/awoooi,wooo/tsenyang-website") (bundle_root / "manifest.remote.tsv").write_text( "\n".join( [ "repo\tstatus\thead_count\tbundle\tchecksum", f"wooo/awoooi\tok\t2\t{bundle_root / 'wooo__awoooi.bundle'}\t{bundle_root / 'wooo__awoooi.bundle.sha256'}", ] ) + "\n", encoding="utf-8", ) (bundle_root / "wooo__awoooi.bundle").write_text("bundle", encoding="utf-8") (bundle_root / "wooo__awoooi.bundle.sha256").write_text( "a" * 64 + " wooo__awoooi.bundle\n", encoding="utf-8", ) (bundle_root / "tsenyang-website-local-20260701.bundle").write_text("bundle", encoding="utf-8") (bundle_root / "tsenyang-website-local-20260701.bundle.sha256").write_text( "b" * 64 + " tsenyang-website-local-20260701.bundle\n", encoding="utf-8", ) metrics, all_ok = exporter._gitea_bundle_metric_lines("188") rendered = "\n".join(metrics) assert all_ok == 1 assert 'awoooi_gitea_bundle_expected_repo_count{host="188"' in rendered assert 'awoooi_gitea_bundle_expected_repo_missing_count{host="188"' in rendered assert 'awoooi_gitea_bundle_failed_repo_count{host="188"' in rendered assert 'repo="wooo/tsenyang-website",status="bundle_file_only"' in rendered assert 'awoooi_gitea_bundle_all_expected_ok{host="188"' in rendered assert rendered.rstrip().endswith(" 1") def test_gitea_bundle_metrics_fail_when_checksum_missing(tmp_path: Path, monkeypatch) -> None: exporter = load_exporter() bundle_root = tmp_path / "latest-private-complete" bundle_root.mkdir() monkeypatch.setattr(exporter, "GITEA_BUNDLE_ROOT", bundle_root) monkeypatch.setenv("AIOPS_GITEA_BUNDLE_EXPECTED_REPOS", "wooo/awoooi") (bundle_root / "manifest.remote.tsv").write_text( "\n".join( [ "repo\tstatus\thead_count\tbundle\tchecksum", f"wooo/awoooi\tok\t2\t{bundle_root / 'wooo__awoooi.bundle'}\t{bundle_root / 'wooo__awoooi.bundle.sha256'}", ] ) + "\n", encoding="utf-8", ) (bundle_root / "wooo__awoooi.bundle").write_text("bundle", encoding="utf-8") metrics, all_ok = exporter._gitea_bundle_metric_lines("188") rendered = "\n".join(metrics) assert all_ok == 0 assert 'awoooi_gitea_bundle_checksum_missing_count{host="188"' in rendered assert rendered.rstrip().endswith(" 0") def test_dr_phase_does_not_regress_when_full_offsite_is_fresh_and_partial_is_stale( tmp_path: Path, monkeypatch ) -> None: exporter = load_exporter() offsite_dir = tmp_path / "offsite" escrow_dir = tmp_path / "escrow" offsite_dir.mkdir() escrow_dir.mkdir() now = 1_782_900_000 monkeypatch.setattr(exporter, "OFFSITE_STATUS_DIR", offsite_dir) monkeypatch.setattr(exporter, "ESCROW_EVIDENCE_DIR", escrow_dir) monkeypatch.setattr(exporter.time, "time", lambda: now) monkeypatch.setattr(exporter, "_b2_configured", lambda: False) monkeypatch.setattr(exporter, "_rclone_configured", lambda: True) (offsite_dir / "rclone-last-success").write_text(str(now - 3600), encoding="utf-8") (offsite_dir / "rclone-partial-last-success").write_text(str(now - 72 * 3600), encoding="utf-8") metrics = exporter._offsite_and_escrow_metric_lines("110") rendered = "\n".join(metrics) assert 'awoooi_backup_offsite_fresh{host="110",provider="rclone",max_age_hours="48"} 1' in rendered assert ( 'awoooi_backup_offsite_partial_fresh{host="110",provider="rclone",scope="partial",max_age_hours="48"} 0' in rendered ) assert 'awoooi_backup_dr_credential_escrow_missing_count{host="110"} 5' in rendered assert 'awoooi_backup_dr_phase{host="110",next_step="complete_credential_escrow_review"} 3' in rendered def test_dr_phase_ready_when_full_offsite_is_fresh_and_escrow_is_complete( tmp_path: Path, monkeypatch ) -> None: exporter = load_exporter() offsite_dir = tmp_path / "offsite" escrow_dir = tmp_path / "escrow" offsite_dir.mkdir() escrow_dir.mkdir() now = 1_782_900_000 monkeypatch.setattr(exporter, "OFFSITE_STATUS_DIR", offsite_dir) monkeypatch.setattr(exporter, "ESCROW_EVIDENCE_DIR", escrow_dir) monkeypatch.setattr(exporter.time, "time", lambda: now) monkeypatch.setattr(exporter, "_b2_configured", lambda: False) monkeypatch.setattr(exporter, "_rclone_configured", lambda: True) (offsite_dir / "rclone-last-success").write_text(str(now - 3600), encoding="utf-8") (offsite_dir / "rclone-partial-last-success").write_text(str(now - 72 * 3600), encoding="utf-8") for item in exporter.ESCROW_ITEMS: (escrow_dir / f"{item}.last_verified").write_text(str(now - 60), encoding="utf-8") metrics = exporter._offsite_and_escrow_metric_lines("110") rendered = "\n".join(metrics) assert 'awoooi_backup_offsite_fresh{host="110",provider="rclone",max_age_hours="48"} 1' in rendered assert ( 'awoooi_backup_offsite_partial_fresh{host="110",provider="rclone",scope="partial",max_age_hours="48"} 0' in rendered ) assert 'awoooi_backup_dr_credential_escrow_missing_count{host="110"} 0' in rendered assert 'awoooi_backup_dr_phase{host="110",next_step="offsite_and_escrow_ready"} 5' in rendered def test_backup_alert_receipt_metrics_require_all_stages(tmp_path: Path, monkeypatch) -> None: exporter = load_exporter() receipt_dir = tmp_path / "alert-receipts" receipt_dir.mkdir() now = 1_782_900_000 monkeypatch.setattr(exporter, "BACKUP_ALERT_RECEIPT_DIR", receipt_dir) monkeypatch.setattr(exporter, "BACKUP_ALERT_RECEIPT_MAX_AGE_HOURS", 168) monkeypatch.setattr(exporter.time, "time", lambda: now) for stage in exporter.BACKUP_ALERT_RECEIPT_STAGES[:-1]: (receipt_dir / f"BackupJobStale.{stage}.last_success").write_text( str(now - 60), encoding="utf-8", ) metrics = exporter._backup_alert_receipt_metric_lines("110") rendered = "\n".join(metrics) assert 'awoooi_backup_alert_receipt_expected_info{host="110",required_alert="BackupJobStale"' in rendered assert 'awoooi_backup_alert_receipt_stage_expected_info{host="110",required_alert="BackupJobStale"' in rendered assert 'stage="sent"' in rendered assert 'stage="received"' in rendered assert 'stage="dedup"' in rendered assert 'stage="escalation"' in rendered assert ( 'awoooi_backup_alert_receipt_stage_fresh{host="110",required_alert="BackupJobStale",scope="freshness",' 'required_notification_type="TYPE-3",receipt_channel="telegram_awooop",max_age_hours="168",stage="escalation"} 0' in rendered ) assert ( 'awoooi_backup_alert_receipt_fresh{host="110",required_alert="BackupJobStale",scope="freshness",' 'required_notification_type="TYPE-3",receipt_channel="telegram_awooop",max_age_hours="168"} 0' in rendered ) def test_backup_alert_receipt_shared_marker_can_satisfy_all_stages(tmp_path: Path, monkeypatch) -> None: exporter = load_exporter() receipt_dir = tmp_path / "alert-receipts" receipt_dir.mkdir() now = 1_782_900_000 monkeypatch.setattr(exporter, "BACKUP_ALERT_RECEIPT_DIR", receipt_dir) monkeypatch.setattr(exporter, "BACKUP_ALERT_RECEIPT_MAX_AGE_HOURS", 168) monkeypatch.setattr(exporter.time, "time", lambda: now) (receipt_dir / "GiteaPrivateBundleRestoreDryRunMissing.last_success").write_text( str(now - 60), encoding="utf-8", ) metrics = exporter._backup_alert_receipt_metric_lines("188") rendered = "\n".join(metrics) assert ( 'awoooi_backup_alert_receipt_fresh{host="188",required_alert="GiteaPrivateBundleRestoreDryRunMissing",' 'scope="gitea_bundle_restore",required_notification_type="TYPE-3",receipt_channel="telegram_awooop",' 'max_age_hours="168"} 1' in rendered )