fix(data): constituent_unified中证1000(000852)全0修复+current锚点三重防线——000852的1233条成分全in_current=0(因子池端点显示0只);根因=08-16 idx-monthly STEP0的fetch_akshare_current(000852)瞬时网络失败被「非致命warning」吞→announce_union重写丢current锚点→migrate全标was_removed,而snapshot兜底对两指数均为csindex超时placeholder从未起作用;数据修复(已执行)=akshare现拉1000只append进announce_union+正规migrate/merge重建,VPS主库1673条cur=1000精确达标,全21指数diff仅000852,NAS sync_tables已同步同态,全0扫描为空;代码防线=①fetch重试3次②空解析raise大声(列定位失败不再静默返空)③_carry_forward_current失败时沿用旧parquet current行锚点只增不丢(000852/932000两块接线);6新测试钉死三防线 [vps]
CI/CD / test (push) Successful in 10s
CI/CD / nas-deploy (push) Successful in 12s
CI/CD / nas-verify (push) Successful in 12s

This commit is contained in:
2026-08-30 12:49:13 +08:00
parent 81b5b558a6
commit 4ab83097ec
2 changed files with 164 additions and 3 deletions
@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""current 锚点防线测试(2026-08-30 数据session, 000852 全 0 事故加固)。
事故链: 08-16 idx-monthly STEP0 的 fetch_akshare_current("000852") 瞬时网络
失败 -> 「非致命 warning」被吞 -> announce_union 重写无 current 行 -> migrate
把 1233 只全标 was_removed -> 因子池端点中证1000 显示 0 只。
三重防线: ①fetch 重试 3 次 ②空解析结果 raise(大声) ③carry-forward 沿用旧
parquet current 行(锚点只增不丢)。"""
import sys
from unittest.mock import MagicMock
import pandas as pd
import pytest
if "parse_csindex_announce" not in sys.modules:
# 模块顶层 import pdfplumber/akshare(仅 VPS 生产装了), Mac 测试 stub 掉
for _mod in ("pdfplumber", "akshare"):
sys.modules.setdefault(_mod, MagicMock())
from scripts.data_platform import parse_csindex_announce as pc
else:
from scripts.data_platform import parse_csindex_announce as pc
def _valid_df():
return pd.DataFrame({
"日期": ["2026-08-30"] * 2,
"成分券代码": ["000012", "600519"],
"成分券名称": ["南玻A", "贵州茅台"],
})
@pytest.fixture(autouse=True)
def _no_sleep(monkeypatch):
monkeypatch.setattr("time.sleep", lambda s: None)
# ---------- 防线①: 重试 ----------
def test_fetch_retries_then_succeeds(monkeypatch):
"""瞬时抖动: 前两次 raise 第三次成功 -> 正常返回, 不抛。"""
import akshare as ak
calls = {"n": 0}
def flaky(symbol):
calls["n"] += 1
if calls["n"] < 3:
raise ConnectionError("csindex hiccup")
return _valid_df()
monkeypatch.setattr(ak, "index_stock_cons_csindex", flaky)
rows = pc.fetch_akshare_current("000852")
assert calls["n"] == 3
assert len(rows) == 2
assert rows[0]["adjust_type"] == "current"
assert rows[0]["code"] == "000012"
def test_fetch_raises_after_all_attempts(monkeypatch):
import akshare as ak
ak.index_stock_cons_csindex = MagicMock(
side_effect=ConnectionError("down"))
with pytest.raises(ConnectionError):
pc.fetch_akshare_current("000852", attempts=3)
# ---------- 防线②: 空结果大声 ----------
def test_fetch_empty_parse_raises(monkeypatch):
"""返表结构异常(定位不到代码列)->0 行: 必须 raise 交 carry-forward, 不得静默返空。"""
import akshare as ak
ak.index_stock_cons_csindex = MagicMock(
return_value=pd.DataFrame({"日期": ["2026-08-30"]}))
with pytest.raises(RuntimeError, match="0 只"):
pc.fetch_akshare_current("000852")
# ---------- 防线③: carry-forward ----------
def _old_parquet_with_current(tmp_path):
df = pd.DataFrame([
{"updateDate": "current", "index_code": "000852", "code": "000012",
"code_name": "南玻A", "adjust_type": "current", "notice_id": 0,
"source": "akshare.index_stock_cons_csindex"},
])
p = tmp_path / "000852_announce_union.parquet"
df.to_parquet(p, index=False)
return p
def test_carry_forward_when_current_missing(tmp_path):
"""本次失败(records 无 current) + 旧 parquet 有 -> 沿用旧行, 锚点不丢。"""
p = _old_parquet_with_current(tmp_path)
records = [{"code": "002766", "adjust_type": "remove"}]
out = pc._carry_forward_current(p, records, "000852")
assert any(r["adjust_type"] == "current" for r in out)
assert out[-1]["code"] == "000012"
def test_no_carry_when_current_present(tmp_path):
"""本次成功 -> 原样返回, 不叠加旧行。"""
p = _old_parquet_with_current(tmp_path)
records = [{"code": "000012", "adjust_type": "current"}]
out = pc._carry_forward_current(p, records, "000852")
assert out == records
def test_no_carry_when_no_old_file(tmp_path):
"""无旧锚点也不编造: 原样放行(error 提示由 migrate 侧可见)。"""
p = tmp_path / "never_exists.parquet"
records = [{"code": "002766", "adjust_type": "add"}]
out = pc._carry_forward_current(p, records, "000852")
assert out == records