Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/account-archive-history.md
Original file line number Diff line number Diff line change
@@ -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.

## Daily cached snapshot archive

When `FIRSTRADE_PERSIST_ACCOUNT_SNAPSHOT=true`, `/account-facts-sync` writes
Expand Down
6 changes: 6 additions & 0 deletions scripts/publish_runtime_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand Down
40 changes: 39 additions & 1 deletion tests/test_project_runtime_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading