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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Python craftsmanship guidance for naming, control flow, data structures, functio
- When implicit behavior becomes configurable, choose the product-wide default deliberately. Update regional examples to state their intended old behavior.
- Do not use `typing.cast()` in application or test code. Model type boundaries with protocols, typed test doubles, or runtime narrowing.
- Keep comments concise and in English. Do not retain compatibility paths for abandoned internal formats without a current requirement.
- Write application-owned log messages and operational alerts in English. Keep user-selected output and opaque provider or user data in their original language; do not translate payloads merely for logging.
- Preserve compatibility between build and runtime environments. Do not assume copied artifacts work across distributions or interpreter builds.

## Tools and workspace
Expand Down
23 changes: 21 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,25 @@ def test_precision_reduction_notice_contains_match_coordinates_and_action() -> N
assert "39.9113890" in notice
assert "116.3805560" in notice
assert "locations.json" in notice
assert "确认" in notice
assert "Confirm that this location is correct" in notice


def test_precision_reduction_notice_uses_english_fallback_for_missing_match() -> None:
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
location = ResolvedLocation(
"example",
"Test City",
1.0,
1.0,
"CN",
"Beijing",
"Asia/Shanghai",
True,
precision_reduced=True,
)

notice = _precision_reduction_notice(location, Path("locations.json"))

assert 'matched at reduced precision as "no matched name provided"' in notice


class TestHourInCron:
Expand Down Expand Up @@ -977,7 +995,8 @@ async def fake_service_run(kind: str, n: object, **kwargs: object) -> str:
logging.root.setLevel(original_root_level)

assert len(alerts) == 1
assert "位置匹配需要确认" in alerts[0][0]
assert alerts[0][0] == "Location match requires confirmation"
assert "Confirm that this location is correct" in alerts[0][1]


async def test_run_logs_skipped_when_no_content(monkeypatch, capsys) -> None:
Expand Down
20 changes: 14 additions & 6 deletions tests/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,11 @@ async def test_context_budget_alert_is_deduplicated_until_recovery(tmp_path: Pat
await service._publish_context_budget_alert((overflow,), now.add(hours=3))

assert len(publisher.messages) == 2
assert all("private-source" in message.body for message, _, _ in publisher.messages)
assert all(
message.body.startswith("Weather history exceeds the LLM input budget")
and "deterministic compaction: private-source" in message.body
for message, _, _ in publisher.messages
)


async def test_context_budget_alert_delivery_failure_is_retried(tmp_path: Path, caplog) -> None:
Expand Down Expand Up @@ -1465,7 +1469,8 @@ async def test_task_failure_alert_is_sent_only_on_first_consecutive_failure(
await service.run("briefing", now)
assert error.value.__notes__ == ["Briefing run failed"]
assert len(publisher.messages) == 1
assert "任务执行失败" in publisher.messages[0][0].body
assert publisher.messages[0][0].body.startswith("Weather briefing task failed")
assert "Check the application logs" in publisher.messages[0][0].body

# Second consecutive failure: no new alert
with pytest.raises(WeatherContextError, match="weather context unavailable") as error:
Expand Down Expand Up @@ -1516,7 +1521,8 @@ async def test_task_failure_alert_delivery_failure_is_retried(
with pytest.raises(WeatherContextError, match="weather context unavailable"):
await service.run("briefing", now.add(hours=1))
assert len(ops_publisher.messages) == 1
assert "任务执行失败" in ops_publisher.messages[0][0].body
assert ops_publisher.messages[0][0].body.startswith("Weather briefing task failed")
assert "Check the application logs" in ops_publisher.messages[0][0].body

with pytest.raises(WeatherContextError, match="weather context unavailable"):
await service.run("briefing", now.add(hours=2))
Expand Down Expand Up @@ -1944,7 +1950,8 @@ async def test_stale_feed_triggers_ops_alert(tmp_path: Path) -> None:
await service.run("briefing", now)

assert len(ops_publisher.messages) >= 1
assert "长时间无更新" in ops_publisher.messages[0][0].body
assert ops_publisher.messages[0][0].body.startswith("Weather RSS sources have not updated")
assert "no new articles within the configured 1-hour threshold" in ops_publisher.messages[0][0].body


class FailingOnceLLM:
Expand Down Expand Up @@ -2349,7 +2356,8 @@ async def test_rss_failure_alert_is_sent_after_threshold(
# Second failure: alert should trigger
await service.run("briefing", now.add(hours=1))
assert len(ops_publisher.messages) == 1
assert "已连续至少 2 个调度轮次获取失败" in ops_publisher.messages[0][0].body
assert ops_publisher.messages[0][0].body.startswith("Weather RSS sources repeatedly failed")
assert "failed for at least 2 consecutive scheduled runs" in ops_publisher.messages[0][0].body

# Third failure: no new alert (already alerted)
await service.run("briefing", now.add(hours=2))
Expand Down Expand Up @@ -2400,4 +2408,4 @@ async def test_failed_rss_alert_delivery_is_retried(
assert state.rss_sources_requiring_failure_alert(("fail-feed",), 1) == []

assert "Failed to publish or record RSS health alert" in caplog.text
assert any("持续获取失败" in message.body for message, _, _ in ops_publisher.messages)
assert any("Weather RSS sources repeatedly failed" in message.body for message, _, _ in ops_publisher.messages)
11 changes: 6 additions & 5 deletions weather_briefing/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ async def run(
location = resolution.location
if location.precision_reduced and not resolution.from_cache:
await delivery.publish_alert(
"位置匹配需要确认",
"Location match requires confirmation",
_precision_reduction_notice(location, settings.locations_path),
)
locations = tuple(resolution.location for resolution in resolutions)
Expand Down Expand Up @@ -588,11 +588,12 @@ def _location_state_path(base_path: Path, location: ResolvedLocation, location_c


def _precision_reduction_notice(location: ResolvedLocation, locations_path: Path) -> str:
matched_name = location.matched_name or "未提供匹配名称"
matched_name = location.matched_name or "no matched name provided"
return (
f"配置地点“{location.name}”无法直接解析,已降低精度匹配为“{matched_name}”(纬度 "
f"{location.latitude:.7f},经度 {location.longitude:.7f})。请确认该位置是否正确;确认后将坐标写入 "
f"{locations_path},可避免后续再次查询和猜测。"
f'The configured location "{location.name}" could not be resolved exactly and was matched at reduced '
f'precision as "{matched_name}" (latitude {location.latitude:.7f}, longitude {location.longitude:.7f}). '
f"Confirm that this location is correct. Add the coordinates to {locations_path} to avoid future lookups "
"and approximation."
)


Expand Down
21 changes: 12 additions & 9 deletions weather_briefing/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,9 @@ async def run(
)
else:
await self._ops_delivery.publish_alert(
"天气简报任务执行失败",
"任务执行失败,请检查运行日志、天气 API 及私密源配置。",
"Weather briefing task failed",
"The task failed. Check the application logs, weather APIs, and private source "
"configuration.",
)
self._state.mark_task_failure_alerted(current_time)
except Exception:
Expand Down Expand Up @@ -293,9 +294,9 @@ async def _run(
)
if rss_failure_alert_ids:
await self._publish_rss_health_alert(
"天气 RSS 源持续获取失败",
f"以下 RSS 源已连续至少 {self._settings.rss_failure_threshold} 个调度轮次获取失败:"
f"{', '.join(rss_failure_alert_ids)}",
"Weather RSS sources repeatedly failed",
f"The following RSS sources failed for at least {self._settings.rss_failure_threshold} "
f"consecutive scheduled runs: {', '.join(rss_failure_alert_ids)}",
lambda: self._state.mark_rss_failure_alerted(tuple(rss_failure_alert_ids), now),
)
stale = self._state.stale_sources_requiring_alert(
Expand All @@ -306,8 +307,9 @@ async def _run(
if stale:
_LOGGER.warning("Stale RSS source(s): %s", ", ".join(stale))
await self._publish_rss_health_alert(
"天气 RSS 源长时间无更新",
f"以下源超过 {self._settings.rss_stale_hours} 小时无新文章:{', '.join(stale)}",
"Weather RSS sources have not updated",
f"The following sources have no new articles within the configured "
f"{self._settings.rss_stale_hours}-hour threshold: {', '.join(stale)}",
lambda: self._state.mark_stale_sources_alerted(tuple(stale), now),
)
local_now = now.in_timezone(self._settings.timezone)
Expand Down Expand Up @@ -519,8 +521,9 @@ async def _publish_context_budget_alert(
if not source_ids:
return
await self._ops_delivery.publish_alert(
"天气历史上下文超出 LLM 输入预算",
"以下来源的最新值或窗口基线在确定性压缩后仍无法纳入输入:" + ", ".join(source_ids),
"Weather history exceeds the LLM input budget",
"The latest value or window baseline for the following sources still cannot fit after "
"deterministic compaction: " + ", ".join(source_ids),
)
self._state.mark_context_budget_alerted(
{source_id: fingerprints[source_id] for source_id in source_ids},
Expand Down