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:
+67
-15
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user