Fix date framing in briefings and make Blogger posts idempotent

The 9/1 briefing called the US 8/31 session "9월 첫 거래일" - the LLM
was anchoring on the KRX date. Both prompts now pin every temporal
phrase (weekday, first/last trading day, month boundaries) to the US
session date, and the verifier treats a mismatch as a major issue.
BloggerDeliverer now updates the existing post for the same KRX date
instead of creating a duplicate on resends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-09-01 14:13:01 +09:00
co-authored by Claude Fable 5
parent b75bf0b887
commit 830e4e0522
3 changed files with 131 additions and 29 deletions
+67 -15
View File
@@ -64,8 +64,8 @@ class FakeHttp:
self._responses = list(responses)
self.calls: list[dict[str, Any]] = []
def post(self, url: str, **kwargs: Any) -> Any:
self.calls.append({"url": url, **kwargs})
def request(self, method: str, url: str, **kwargs: Any) -> Any:
self.calls.append({"method": method, "url": url, **kwargs})
if not self._responses:
raise AssertionError("unexpected extra HTTP call")
item = self._responses.pop(0)
@@ -73,6 +73,9 @@ class FakeHttp:
raise item
return item
def post(self, url: str, **kwargs: Any) -> Any:
return self.request("POST", url, **kwargs)
def make_deliverer(responses: list[Any]) -> tuple[BloggerDeliverer, FakeHttp]:
client = FakeHttp(responses)
@@ -97,23 +100,59 @@ def test_post_title_with_and_without_headline():
# ------------------------------------------------------------------ delivery
def test_deliver_refreshes_token_posts_and_returns_url():
deliverer, client = make_deliverer([_Resp(200, TOKEN_OK), _Resp(200, POST_OK)])
EMPTY_LIST = {"kind": "blogger#postList"} # no items key on an empty blog
def test_deliver_refreshes_token_lists_then_inserts():
deliverer, client = make_deliverer(
[_Resp(200, TOKEN_OK), _Resp(200, EMPTY_LIST), _Resp(200, POST_OK)]
)
receipt = deliverer.deliver(make_result())
assert receipt == POST_OK["url"]
token_call, post_call = client.calls
token_call, list_call, insert_call = client.calls
assert token_call["data"]["grant_type"] == "refresh_token"
assert "refresh_token" not in post_call["url"] # secrets never in URLs
assert post_call["url"].endswith("/blogs/12345/posts/")
assert post_call["headers"]["Authorization"] == "Bearer at-1"
body = post_call["json"]
assert list_call["method"] == "GET" and list_call["params"]["fetchBodies"] == "false"
assert insert_call["method"] == "POST"
assert "refresh_token" not in insert_call["url"] # secrets never in URLs
assert insert_call["url"].endswith("/blogs/12345/posts/")
assert insert_call["headers"]["Authorization"] == "Bearer at-1"
body = insert_call["json"]
assert body["title"].startswith("미국 마감 브리핑 2026-09-01")
assert body["labels"] == ["미국증시"] and "<table>" in body["content"]
def test_deliver_updates_existing_post_for_same_day():
listing = {"items": [{"id": "42", "title": "미국 마감 브리핑 2026-09-01 — 이전 헤드라인"}]}
deliverer, client = make_deliverer(
[_Resp(200, TOKEN_OK), _Resp(200, listing), _Resp(200, POST_OK)]
)
receipt = deliverer.deliver(make_result())
assert receipt == POST_OK["url"]
update_call = client.calls[-1]
assert update_call["method"] == "PUT"
assert update_call["url"].endswith("/blogs/12345/posts/42")
def test_deliver_other_day_posts_do_not_match():
listing = {"items": [{"id": "41", "title": "미국 마감 브리핑 2026-08-29 — 다른 날"}]}
deliverer, client = make_deliverer(
[_Resp(200, TOKEN_OK), _Resp(200, listing), _Resp(200, POST_OK)]
)
deliverer.deliver(make_result())
assert client.calls[-1]["method"] == "POST" # inserted, not updated
def test_deliver_reuses_cached_token():
deliverer, client = make_deliverer([_Resp(200, TOKEN_OK), _Resp(200, POST_OK), _Resp(200, POST_OK)])
deliverer, client = make_deliverer(
[
_Resp(200, TOKEN_OK),
_Resp(200, EMPTY_LIST),
_Resp(200, POST_OK),
_Resp(200, EMPTY_LIST),
_Resp(200, POST_OK),
]
)
deliverer.deliver(make_result())
deliverer.deliver(make_result())
token_calls = [c for c in client.calls if "oauth2" in c["url"]]
@@ -124,8 +163,9 @@ def test_deliver_retries_once_on_401_with_fresh_token():
deliverer, client = make_deliverer(
[
_Resp(200, TOKEN_OK),
_Resp(401, {"error": "invalid token"}),
_Resp(401, {"error": "invalid token"}), # list call rejected
_Resp(200, {"access_token": "at-2", "expires_in": 3600}),
_Resp(200, EMPTY_LIST),
_Resp(200, POST_OK),
]
)
@@ -134,20 +174,32 @@ def test_deliver_retries_once_on_401_with_fresh_token():
assert client.calls[-1]["headers"]["Authorization"] == "Bearer at-2"
def test_deliver_lookup_failure_still_publishes(monkeypatch):
monkeypatch.setattr("us2kr.deliver.blogger.time.sleep", lambda _s: None)
deliverer, client = make_deliverer(
[_Resp(200, TOKEN_OK)] + [_Resp(503, {"error": "backend"})] * 3 + [_Resp(200, POST_OK)]
)
receipt = deliverer.deliver(make_result())
assert receipt == POST_OK["url"]
assert client.calls[-1]["method"] == "POST"
def test_deliver_retries_5xx_then_raises(monkeypatch):
monkeypatch.setattr("us2kr.deliver.blogger.time.sleep", lambda _s: None)
deliverer, _client = make_deliverer(
[_Resp(200, TOKEN_OK)] + [_Resp(503, {"error": "backend"})] * 3
[_Resp(200, TOKEN_OK), _Resp(200, EMPTY_LIST)] + [_Resp(503, {"error": "backend"})] * 3
)
with pytest.raises(DeliveryError, match="HTTP 503"):
deliverer.deliver(make_result())
def test_deliver_does_not_retry_4xx():
deliverer, client = make_deliverer([_Resp(200, TOKEN_OK), _Resp(403, {"error": "forbidden"})])
deliverer, client = make_deliverer(
[_Resp(200, TOKEN_OK), _Resp(200, EMPTY_LIST), _Resp(403, {"error": "forbidden"})]
)
with pytest.raises(DeliveryError, match="HTTP 403"):
deliverer.deliver(make_result())
assert len(client.calls) == 2 # no extra attempts
assert len(client.calls) == 3 # no extra attempts
def test_token_failure_raises_delivery_error():
@@ -159,7 +211,7 @@ def test_token_failure_raises_delivery_error():
def test_network_error_wrapped(monkeypatch):
monkeypatch.setattr("us2kr.deliver.blogger.time.sleep", lambda _s: None)
deliverer, _ = make_deliverer(
[_Resp(200, TOKEN_OK)] + [httpx.ConnectError("boom")] * 3
[_Resp(200, TOKEN_OK), _Resp(200, EMPTY_LIST)] + [httpx.ConnectError("boom")] * 3
)
with pytest.raises(DeliveryError, match="ConnectError"):
deliverer.deliver(make_result())