diff --git a/AGENTS.md b/AGENTS.md index 6083df00..adb8952c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index e117d283..1e50e925 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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: + 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: @@ -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: diff --git a/tests/test_service.py b/tests/test_service.py index 49907508..3c35ddb5 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -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: @@ -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: @@ -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)) @@ -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: @@ -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)) @@ -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) diff --git a/weather_briefing/cli.py b/weather_briefing/cli.py index 0fa5cde1..50d23f5c 100644 --- a/weather_briefing/cli.py +++ b/weather_briefing/cli.py @@ -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) @@ -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." ) diff --git a/weather_briefing/service.py b/weather_briefing/service.py index 741bd1e3..fb9b6b0e 100644 --- a/weather_briefing/service.py +++ b/weather_briefing/service.py @@ -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: @@ -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( @@ -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) @@ -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},