test: enforce provider construction ownership
This commit is contained in:
@@ -220,6 +220,25 @@
|
|||||||
"runtime_role": "index observation fallback"
|
"runtime_role": "index observation fallback"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"provider_construction": [
|
||||||
|
{
|
||||||
|
"client": "TushareClient",
|
||||||
|
"owner": "backend/data/providers/tushare.py",
|
||||||
|
"compatibility_fallback": "backend/features/market/service.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "IfindHttpClient",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "MarketChartClient",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "WebRealtimeAggregator",
|
||||||
|
"owner": "backend/data/gateway.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
"numeric_normalization": [
|
"numeric_normalization": [
|
||||||
{
|
{
|
||||||
"function": "finite_number",
|
"function": "finite_number",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.data import (
|
from backend.data import (
|
||||||
DataPolicyError,
|
DataPolicyError,
|
||||||
@@ -45,8 +47,6 @@ class DataGatewayTests(unittest.TestCase):
|
|||||||
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
|
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
|
||||||
|
|
||||||
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
|
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
source = (
|
source = (
|
||||||
Path(__file__).resolve().parents[1]
|
Path(__file__).resolve().parents[1]
|
||||||
/ "backend"
|
/ "backend"
|
||||||
@@ -57,6 +57,33 @@ class DataGatewayTests(unittest.TestCase):
|
|||||||
self.assertEqual(source.count("TushareClient(self.token)"), 1)
|
self.assertEqual(source.count("TushareClient(self.token)"), 1)
|
||||||
self.assertIn("return gateway.tushare()", source)
|
self.assertIn("return gateway.tushare()", source)
|
||||||
|
|
||||||
|
def test_provider_construction_has_unique_declared_owners(self) -> None:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
owners = {
|
||||||
|
"EastmoneyChartClient": {"backend/data/gateway.py"},
|
||||||
|
"IfindHttpClient": {"backend/data/gateway.py"},
|
||||||
|
"IfindProvider": {"backend/data/gateway.py"},
|
||||||
|
"MarketChartClient": {"backend/data/gateway.py"},
|
||||||
|
"TushareClient": {"backend/features/market/service.py"},
|
||||||
|
"TushareProvider": {"backend/data/gateway.py"},
|
||||||
|
"WebRealtimeAggregator": {"backend/data/gateway.py"},
|
||||||
|
}
|
||||||
|
found = {name: set() for name in owners}
|
||||||
|
for path in (root / "backend").rglob("*.py"):
|
||||||
|
relative = path.relative_to(root).as_posix()
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, ast.Call):
|
||||||
|
continue
|
||||||
|
name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
|
||||||
|
if name in found:
|
||||||
|
found[name].add(relative)
|
||||||
|
self.assertEqual(found, owners)
|
||||||
|
provider_source = (root / "backend/data/providers/tushare.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertIn("client_factory: Callable[[str], TushareClient] = TushareClient", provider_source)
|
||||||
|
|
||||||
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
|
def test_quality_gate_accepts_matching_daily_evidence(self) -> None:
|
||||||
timezone = market_timezone()
|
timezone = market_timezone()
|
||||||
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
now = datetime(2026, 7, 29, 16, 0, tzinfo=timezone)
|
||||||
|
|||||||
@@ -155,6 +155,12 @@ def build() -> dict[str, Any]:
|
|||||||
{"provider": "eastmoney", "path": "backend/data/realtime.py", "runtime_role": "isolated realtime observation"},
|
{"provider": "eastmoney", "path": "backend/data/realtime.py", "runtime_role": "isolated realtime observation"},
|
||||||
{"provider": "tencent", "path": "backend/data/realtime.py", "runtime_role": "index observation fallback"},
|
{"provider": "tencent", "path": "backend/data/realtime.py", "runtime_role": "index observation fallback"},
|
||||||
],
|
],
|
||||||
|
"provider_construction": [
|
||||||
|
{"client": "TushareClient", "owner": "backend/data/providers/tushare.py", "compatibility_fallback": "backend/features/market/service.py"},
|
||||||
|
{"client": "IfindHttpClient", "owner": "backend/data/gateway.py"},
|
||||||
|
{"client": "MarketChartClient", "owner": "backend/data/gateway.py"},
|
||||||
|
{"client": "WebRealtimeAggregator", "owner": "backend/data/gateway.py"},
|
||||||
|
],
|
||||||
"numeric_normalization": [
|
"numeric_normalization": [
|
||||||
{"function": "finite_number", "path": "backend/data/numbers.py"},
|
{"function": "finite_number", "path": "backend/data/numbers.py"},
|
||||||
{"function": "non_nan_number", "path": "backend/data/numbers.py"},
|
{"function": "non_nan_number", "path": "backend/data/numbers.py"},
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
|
| CR-04 | 数值归一化策略 | 四个业务模块分别保留两组完全相同的数值转换函数体 | 由`backend/data/numbers.py`集中拥有两种既有语义,消费者保留原局部别名 | 已完成 |
|
||||||
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
|
| CR-05 | 根级兼容入口 | 正式后端仍有五处通过迁移兼容模块反向导入规范实现 | 正式代码改用规范路径;兼容入口只服务原公开导入契约 | 已完成 |
|
||||||
| CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 |
|
| CR-06 | 紧凑日期显示 | Tushare与选股引擎各保留一份完全相同的`YYYYMMDD`显示转换 | 由`bootstrap/config.py`拥有唯一格式策略,消费者保留原局部别名 | 已完成 |
|
||||||
|
| CR-07 | 数据Provider组装 | 核查网关、容器和业务服务是否重复创建外部数据客户端 | 固化唯一创建位置及兼容例外,不改数据源语义 | 已完成 |
|
||||||
|
|
||||||
## CR-01验收口径
|
## CR-01验收口径
|
||||||
|
|
||||||
@@ -162,6 +163,28 @@
|
|||||||
本批基线为`xiaobai-reduction-05-compatibility-boundaries-20260801`;检查点为
|
本批基线为`xiaobai-reduction-05-compatibility-boundaries-20260801`;检查点为
|
||||||
`xiaobai-reduction-06-date-formatting-20260801`。
|
`xiaobai-reduction-06-date-formatting-20260801`。
|
||||||
|
|
||||||
|
## CR-07验收口径
|
||||||
|
|
||||||
|
- iFinD、图表、实时观察器和Provider适配器必须只在`build_data_gateway`创建,并由容器共享。
|
||||||
|
- Tushare必须继续通过实时Token供应器按需创建,不能为了减少对象数量缓存过期Token。
|
||||||
|
- 市场服务中为原版隔离测试桩保留的一处`TushareClient(self.token)`是明确兼容例外,不得被误判为
|
||||||
|
第二条正式数据链路。
|
||||||
|
- 不得合并Tushare、iFinD、东方财富和腾讯的传输、缓存、重试或降级逻辑。
|
||||||
|
|
||||||
|
## CR-07结果
|
||||||
|
|
||||||
|
- 全后端构造点扫描确认iFinD、MarketChart、东方财富图表和实时观察器均只有网关一个创建位置;
|
||||||
|
`ApplicationContainer`暴露的是同一对象引用,没有第二份客户端。
|
||||||
|
- Tushare Provider使用动态Token供应器,市场服务只有一处已登记测试兼容回退;本批没有发现可安全
|
||||||
|
删除的生产实现,因此不为追求行数强行修改运行代码。
|
||||||
|
- 架构清单新增Provider创建所有权,自动测试会在未来出现第二个未登记构造点时失败。
|
||||||
|
- 候选322项、纯`app/`导出259项、24个JavaScript文件、API/架构注册表、Git空白检查和SQLite
|
||||||
|
完整性检查通过;本批不涉及页面、CSS或浏览器行为。
|
||||||
|
- 本批不修改请求频率、缓存、重试、Token更新、数据源选择、计算口径、API、数据库或前端。
|
||||||
|
|
||||||
|
本批基线为`xiaobai-reduction-06-date-formatting-20260801`;检查点为
|
||||||
|
`xiaobai-reduction-07-provider-ownership-20260801`。
|
||||||
|
|
||||||
## 人工验收记录
|
## 人工验收记录
|
||||||
|
|
||||||
- 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与
|
- 2026-08-01:用户检查CR-02与CR-03运行结果,确认未发现明显异常。本记录仅表示本轮可见功能与
|
||||||
|
|||||||
Reference in New Issue
Block a user