Skip to content

Commit cdfd42b

Browse files
committed
fix(review-feedback-1709): 补一个未命中敏感规则的异常回归测试
1 parent 5898c0d commit cdfd42b

7 files changed

Lines changed: 46 additions & 8 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ SEARXNG_PUBLIC_INSTANCES_ENABLED=true
354354
# NEWS_INTEL_FETCH_TIMEOUT_SEC=8
355355
# 单次每个资讯源最多采集条数
356356
# NEWS_INTEL_MAX_ITEMS_PER_SOURCE=50
357-
# NewsNow HTTP API 基地址;可改为自建 NewsNow 实例,默认使用公开示例实例
357+
# NewsNow HTTP API 基地址;默认使用公开示例实例(可能存在不可用/限流),生产建议改为自建实例
358358
# NEWSNOW_BASE_URL=https://newsnow.busiyi.world
359359
# 注:本节仅控制资讯源采集与清理行为,不会改变 LLM / provider / base URL / 兼容回退语义。
360360
# 乖离率阈值(%),偏离 MA5 超过此值提示不追高;强势趋势股自动放宽到 1.5 倍

api/v1/endpoints/intelligence.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ def _not_found(message: str) -> HTTPException:
3939
def _internal_error(message: str, exc: Exception) -> HTTPException:
4040
sanitized_error = sanitize_diagnostic_text(str(exc), max_length=300) or "internal intelligence error"
4141
logger.error("%s: %s", message, sanitized_error)
42-
return HTTPException(status_code=500, detail={"error": "internal_error", "message": f"{message}: {sanitized_error}"})
42+
return HTTPException(
43+
status_code=500,
44+
detail={"error": "internal_error", "message": f"{message}: internal intelligence service error"},
45+
)
4346

4447

4548
@router.post("/sources", response_model=IntelligenceSourceItem, responses={400: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, summary="Create intelligence source")

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4545
- [改进] #1707 补齐内置 RSS/Atom 资讯源模板后端入口,修复本地资讯拉取的 requests 参数兼容、请求阶段 DNS 校验、共享地址段拒绝、坏条目跳过与港股短代码匹配。
4646
- [文档] #1707 阐明情报池接入仅追加本地资讯消费,不改模型名/provider/base URL/默认模型策略/回退策略/保存前清理逻辑或运行时配置迁移;结构化风险提示若出现为关键词误报;回滚可采用 `revert` 本 PR 或停用/移除本地资讯接入入口与数据。
4747
- [文档] 本次 #1707 情报源基线仅新增 `NEWS_INTEL_*` 配置,不变更 LITELLM / ANSPIRE / Base URL / provider/model 清理与回退语义;回退策略为移除或禁用这三项情报源变量后恢复既有 LLM 配置行为。
48+
- [修复] #1707 默认禁用 `/api/v1/intelligence/sources/defaults` 新建源(未传 `enabled`),避免公开示例 NewsNow 实例被默认启用,同时统一 500 响应细节仅入日志、响应返回通用错误信息。
4849

4950
- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。
5051
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。

docs/intelligence-sources.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ NEWS_INTEL_MAX_ITEMS_PER_SOURCE=50
3737
NEWSNOW_BASE_URL=https://newsnow.busiyi.world
3838
```
3939

40-
`NEWSNOW_BASE_URL` 用于拼出 `GET {NEWSNOW_BASE_URL}/api/s?id=<source_id>`。默认值来自 NewsNow MCP Server 示例配置;生产环境建议改成自建 NewsNow 实例,以减少公开示例实例不可用或限流带来的影响。
40+
`NEWSNOW_BASE_URL` 用于拼出 `GET {NEWSNOW_BASE_URL}/api/s?id=<source_id>`。当前默认值为公开示例实例 `https://newsnow.busiyi.world`,官方/上游链路与形态请以你实际接入实例文档为准;生产环境建议改成自建 NewsNow 实例,以避免公开示例实例不可用或限流。
41+
42+
兼容性核验建议(非 mock):
43+
44+
- `curl -sS "${NEWSNOW_BASE_URL}/api/s?id=cls-hot" | python -c "import sys, json; data=json.load(sys.stdin); assert isinstance(data, dict); assert isinstance(data.get('items'), list); print('ok')"`
45+
- 若要手工核验 `status``id``items[].title``items[].url`/`mobileUrl``items[].pubDate`/`items[].extra.date` 这类字段,可与测试回归组合使用:`test_newsnow_source_fetches_json_items`
4146

4247
## API
4348

@@ -47,7 +52,7 @@ NEWSNOW_BASE_URL=https://newsnow.busiyi.world
4752
- `GET /sources`:查询资讯源。
4853
- `GET /sources/templates?market=hk`:查询内置资讯源模板。
4954
- `POST /sources/templates/{template_id}`:从内置模板创建资讯源,可覆盖名称、启用状态、作用域和说明。
50-
- `POST /sources/defaults`:一键创建全部内置默认源;接口幂等,已存在的同名源会返回 `created=false`,不会重复插入。请求体可传 `{ "enabled": false }` 控制创建后的启用状态
55+
- `POST /sources/defaults`:一键创建全部内置默认源;接口幂等,已存在的同名源会返回 `created=false`,不会重复插入。默认不传 `enabled` 时以 `false` 创建;如需默认启用可传 `{ "enabled": true }`
5156
- `POST /sources/test`:测试 payload,不落库。
5257
- `POST /sources/{source_id}/fetch?dry_run=false`:拉取单个源。
5358
- `POST /sources/fetch-enabled`:fail-open 拉取全部启用源。

src/services/intelligence_service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ def create_source_from_template(self, template_id: str, overrides: Optional[Dict
166166
return self.create_source(payload)
167167

168168
def create_default_sources(self, overrides: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
169-
request_fields = overrides or {}
169+
request_fields = dict(overrides or {})
170+
request_fields.setdefault("enabled", False)
170171
created_count = 0
171172
items = []
172173
for template in self._builtin_source_templates():

tests/test_intelligence_api.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,30 @@ def test_fetch_source_internal_error_is_sanitized(self) -> None:
128128
self.assertEqual(fetch_resp.status_code, 500)
129129
body = fetch_resp.json()
130130
self.assertEqual(body["error"], "internal_error")
131-
self.assertTrue(body["message"].startswith("Fetch intelligence source failed"))
132-
self.assertNotIn("token=secret", body["message"])
133-
self.assertNotIn("abc12345", body["message"])
131+
self.assertEqual(body["message"], "Fetch intelligence source failed: internal intelligence service error")
132+
133+
def test_fetch_source_internal_error_without_sensitive_pattern_is_generic(self) -> None:
134+
create_resp = self.client.post("/api/v1/intelligence/sources", json={"name": "api-feed", "url": "https://feeds.example.com/rss.xml", "source_type": "rss", "scope_type": "market", "market": "cn"})
135+
self.assertEqual(create_resp.status_code, 200)
136+
source_id = create_resp.json()["id"]
137+
with patch("src.services.intelligence_service.IntelligenceService.fetch_source", side_effect=RuntimeError("unexpected runtime assertion failure: pipeline context exhausted")):
138+
fetch_resp = self.client.post(f"/api/v1/intelligence/sources/{source_id}/fetch")
139+
140+
self.assertEqual(fetch_resp.status_code, 500)
141+
body = fetch_resp.json()
142+
self.assertEqual(body["error"], "internal_error")
143+
self.assertEqual(body["message"], "Fetch intelligence source failed: internal intelligence service error")
144+
self.assertNotIn("pipeline context exhausted", body["message"])
145+
146+
def test_create_builtin_default_sources_are_disabled_by_default(self) -> None:
147+
default_resp = self.client.post("/api/v1/intelligence/sources/defaults")
148+
self.assertEqual(default_resp.status_code, 200)
149+
newsnow_sources = [
150+
item["source"] for item in default_resp.json()["items"]
151+
if item["source"]["source_type"] == "newsnow"
152+
]
153+
self.assertGreaterEqual(len(newsnow_sources), 5)
154+
self.assertTrue(all(not item["enabled"] for item in newsnow_sources))
134155

135156
def test_upstream_fetch_errors_do_not_expose_query_secret(self) -> None:
136157
secret_url = "https://feeds.example.com/rss.xml?token=super-secret"

tests/test_intelligence_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,13 @@ def test_create_default_sources_is_idempotent(self) -> None:
296296
self.assertGreaterEqual(sources["total"], 3)
297297
self.assertTrue(all(not item["source"]["enabled"] for item in first["items"]))
298298

299+
def test_create_default_sources_are_disabled_by_default(self) -> None:
300+
first = self.service.create_default_sources()
301+
sources = self.service.list_sources()
302+
self.assertEqual(first["created_count"], first["total"])
303+
self.assertEqual(sources["total"], first["total"])
304+
self.assertTrue(all(not item["enabled"] for item in sources["items"]))
305+
299306
def test_same_url_can_be_saved_for_different_scopes(self) -> None:
300307
market = self.service.create_source({
301308
"name": "market-feed",

0 commit comments

Comments
 (0)