From fead8efa12dfb227efbae939a41d0fbc97bb917a Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 1 Nov 2026 12:40:18 +0800 Subject: [PATCH] fix: distinguish daily source read failures from absent runs Co-Authored-By: Codex --- docs/account-archive-history.md | 15 +++++++++++ scripts/publish_runtime_daily.py | 6 +++++ tests/test_project_runtime_daily.py | 40 ++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/account-archive-history.md b/docs/account-archive-history.md index 80ea0c6..53edbba 100644 --- a/docs/account-archive-history.md +++ b/docs/account-archive-history.md @@ -1,5 +1,20 @@ # Saved account history +## Runtime daily projection diagnostics + +`scripts.publish_runtime_daily` sends no request when it has no attributable +runs. Its status distinguishes archive listing failure (`source_list_failed`), +report read failure (`source_read_error`) and the bounded read limit +(`source_read_truncated`) from a readable source with no matching runs +(`no_attributable_runs`). Unrecognized source errors become +`source_read_unavailable`; provider messages and private source locations are +never included. All these outcomes retain a failing exit status. + +These statuses occur before a persistence request. They do not establish a +receiver or storage write failure. A partial read with attributable runs retains +the existing incomplete projection and its read errors; acknowledgement matching +and the single-request policy are unchanged. + ## Manual session renewal Manual session renewal selects the configured account, or the sole returned diff --git a/scripts/publish_runtime_daily.py b/scripts/publish_runtime_daily.py index bc965fa..ea336a6 100644 --- a/scripts/publish_runtime_daily.py +++ b/scripts/publish_runtime_daily.py @@ -118,6 +118,12 @@ def publish_once(env, *, now=None, opener=None): if error: projection["read_errors"].append(error) if not projection["records"][0]["runs"]: + if error: + return { + "list_failed": "source_list_failed", + "report_read_error": "source_read_error", + "read_truncated": "source_read_truncated", + }.get(error, "source_read_unavailable") return "no_attributable_runs" request = Request(origin + "/api/runtime-daily/sync", method="POST", data=json.dumps(projection, separators=(",", ":")).encode(), headers={ diff --git a/tests/test_project_runtime_daily.py b/tests/test_project_runtime_daily.py index 506fd0d..cd4ec55 100644 --- a/tests/test_project_runtime_daily.py +++ b/tests/test_project_runtime_daily.py @@ -112,7 +112,7 @@ def open(self, *_args, **_kwargs): self.calls += 1 raise AssertionError("must not send") opener = Opener() - for reports, error in (([], None), ([], "list_failed"), ([{"platform": PLATFORM}], None)): + for reports, error in (([], None), ([{"platform": PLATFORM}], None)): monkeypatch.setattr(publisher, "_read_reports", lambda *_args: (reports, error)) assert publisher.publish_once(env, now=now, opener=opener) == "no_attributable_runs" assert opener.calls == 0 @@ -122,6 +122,44 @@ def open(self, *_args, **_kwargs): assert opener.calls == 0 +def test_unreadable_empty_source_keeps_safe_failure_category(monkeypatch, capsys): + from scripts import publish_runtime_daily as publisher + + env = {"RUNTIME_TARGET_JSON": json.dumps({"service_name": "synthetic-service", + "strategy_profile": "synthetic-profile", "account_scope": "live"}), + "FIRSTRADE_ACCOUNT_FACTS_ACCOUNT_KEY": "synthetic-key", + "FIRSTRADE_ACCOUNT_FACTS_SOURCE_BINDING_ID": "a" * 64, + "FIRSTRADE_ACCOUNT_FACTS_ACCOUNT_SCOPE": "live", + "EXECUTION_EVIDENCE_SYNC_TOKEN": "synthetic-token", + "EXECUTION_EVIDENCE_SYNC_URL": "https://example.test/api/execution-evidence/sync", + "RUNTIME_DAILY_BUSINESS_DATE": "2026-10-29"} + monkeypatch.setattr(publisher, "_FACTS_URL", "https://example.test/api/account-facts/sync") + monkeypatch.setattr(publisher.os, "environ", env) + monkeypatch.setattr(publisher.sys, "argv", ["publish_runtime_daily"]) + monkeypatch.setattr(publisher, "build_opener", lambda *_: (_ for _ in ()).throw( + AssertionError("unreadable empty source must not POST"))) + for error, expected in ( + ("list_failed", "source_list_failed"), + ("report_read_error", "source_read_error"), + ("read_truncated", "source_read_truncated"), + ("synthetic-private-provider-body", "source_read_unavailable"), + ): + monkeypatch.setattr(publisher, "_read_reports", lambda *_: ([], error)) + assert publisher.main() == 1 + assert json.loads(capsys.readouterr().out) == {"status": expected} + + +def test_archive_listing_exception_is_redacted_and_preserved(monkeypatch, capsys): + from scripts import publish_runtime_daily as publisher + + monkeypatch.setattr(publisher.heartbeat, "_report_globs", lambda *_: ["gs://synthetic/archive/*"]) + monkeypatch.setattr(publisher.heartbeat, "_list_gcs_objects", lambda *_, **__: (_ for _ in ()).throw( + RuntimeError("synthetic-private-provider-body"))) + now = datetime(2026, 10, 29, 20, tzinfo=timezone.utc) + assert publisher._read_reports(now.date(), now, "synthetic-project") == ([], "list_failed") + assert capsys.readouterr() == ("", "") + + def test_redirect_is_not_followed_and_unknown_is_not_retried(monkeypatch): from urllib.error import HTTPError from scripts import publish_runtime_daily as publisher