from datetime import datetime, timezone import io import json from pathlib import Path import sys SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" sys.path.insert(0, str(SCRIPTS)) import security_flow_daily as daily # noqa: E402 import weekly_allocation_check as check # noqa: E402 UTC = timezone.utc def test_due_week_turns_over_monday_noon_berlin(): assert check.due_week(datetime(2026, 10, 19, 9, 59, tzinfo=UTC)) == "2026-W42" # 11:59 CEST assert check.due_week(datetime(2026, 10, 19, 10, 0, tzinfo=UTC)) == "2026-W43" # 12:00 CEST assert check.due_week(datetime(2026, 11, 2, 10, 59, tzinfo=UTC)) == "2026-W44" # 11:59 CET assert check.due_week(datetime(2026, 11, 2, 11, 0, tzinfo=UTC)) == "2026-W45" assert check.due_week(datetime(2026, 10, 25, 20, 0, tzinfo=UTC)) == "2026-W43" # Sunday def test_missing_failed_and_ok(): now = datetime(2026, 10, 19, 12, 0, tzinfo=UTC) assert check.evaluate([], now)["flags"][0]["flag"] == "missing" ok = [{"week": "2026-W43", "status": "retained", "run_at": "2026-10-19T06:00:30Z"}] assert check.evaluate(ok, now)["status"] == "ok" failed = ok + [{"week": "2026-W43", "status": "failed", "run_at": "2026-10-19T07:00:00Z", "error": "x"}] assert [f["flag"] for f in check.evaluate(failed, now)["flags"]] == ["failed"] only_failed = [failed[1]] assert [f["flag"] for f in check.evaluate(only_failed, now)["flags"]] == ["failed", "missing"] def test_inactive_without_source_and_file_and_hub_sources(tmp_path): assert check.check(None, "http://hub")["status"] == "inactive" receipts = tmp_path / "receipts.jsonl" receipts.write_text(json.dumps({"week": "2026-W43", "status": "already-retained", "run_at": "a"}) + "\npartial{\n") now = datetime(2026, 10, 20, tzinfo=UTC) assert check.check(str(receipts), "http://hub", now)["status"] == "ok" urls = [] def opener(url, timeout): urls.append(url) return io.BytesIO(json.dumps([{"body": json.dumps({"week": "2026-W42"}), "created_at": "z"}]).encode()) result = check.check("hub", "http://hub", now, opener) assert "from_agent=custodian-allocation-review" in urls[0] assert result["flags"][0] == {"flag": "missing", "week": "2026-W43", "detail": "no retained weekly allocation report for the due ISO week"} def test_daily_notice_is_deduplicated_and_cleared(): flagged = {"status": "flagged", "flags": [{"flag": "missing", "week": "2026-W43"}]} state = {} assert daily.allocation_notice(flagged, state) == {"WEEKLY_ALLOCATION": flagged} state["allocation"] = flagged["flags"] assert daily.allocation_notice(flagged, state) is None assert daily.allocation_notice({"status": "ok"}, state) is None and "allocation" not in state assert daily.allocation_notice({"status": "inactive"}, {}) is None def test_daily_flow_posts_allocation_flag_once(tmp_path, monkeypatch): report = {"READY": [], "UNBLOCKED": [], "ATTENDED": []} receipts = tmp_path / "receipts.jsonl" receipts.write_text(json.dumps({"week": "2020-W01", "status": "failed", "run_at": "x"}) + "\n") monkeypatch.setattr(sys, "argv", ["d", "--apply", "--state-dir", str(tmp_path), "--allocation-receipts", str(receipts)]) monkeypatch.setattr(daily.subprocess, "run", lambda *a, **kw: None) monkeypatch.setattr(daily.subprocess, "check_output", lambda *a, **kw: json.dumps(report)) sent = [] monkeypatch.setattr(daily, "post", lambda hub, to, payload, subject="Daily security flow": sent.append((to, subject, payload)) or "m1") daily.main() daily.main() allocation = [s for s in sent if s[1] == "Weekly allocation assessment check"] assert len(allocation) == 1 and allocation[0][0] == "the-custodian" assert json.loads((tmp_path / "delivery.json").read_text())["allocation_message_id"] == "m1"