feat(data): T4 backfill lane ①ann_meta ②news_meta 落地 [nas]
CI/CD / test (push) Successful in 23s
CI/CD / nas-deploy (push) Successful in 26s
CI/CD / nas-verify (push) Successful in 6s

spec §18.7 T4(unit marker 断点+--limit 冒烟不落marker+墙钟rc3 全链复用):
- ①ann_meta 回补: unit=(stock,year) 2000→今年逐年按池序, 空年=合法done
  (探测优先);今年窗口右端=today 相对;~15万 unit @1s≈3-6 跑日
- ②news_meta 回补: np-listapi unit=(stock,page) 翻到空页止;空页+page≥200
  双止条件都落 news_end 哨兵(到头股零重扫);mkt前缀 6→1/其余→0
- 共用 _absorb_new(daily/backfill 同一条 id 去重+追加管道)
- 测试 46 个;修 wiring 网络泄漏:_wire_backfill_ann 曾漏锁
  fetch_news_backfill_page 致 ann 测试真打 np-listapi(40s/真出网,
  零网络铁律违反)——补锁后套件 159s→1.3s;全套 263 绿
This commit is contained in:
2026-09-06 23:19:50 +08:00
parent e22f0e0aa8
commit 10c486c64d
2 changed files with 257 additions and 13 deletions
+151
View File
@@ -532,3 +532,154 @@ def test_main_daily_exit0(root, monkeypatch):
with pytest.raises(SystemExit) as e:
cd.main()
assert e.value.code == 0
# ---------- backfill lane ①ann_meta ②news_meta(T4) ----------
def _wire_backfill_ann(monkeypatch, rows_by_unit):
"""rows_by_unit: {(code, start, end): rows} — 按 unit 窗口回放。
必须同时锁死 news 段 fetch(零网络铁律——曾漏锁致真打 np-listapi 40s)。"""
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: POOL)
monkeypatch.setattr(cd, "fetch_news_backfill_page", MagicMock(return_value=[]))
calls = []
def fake(client, code, org, start, end, category=""):
calls.append((code, start, end))
return rows_by_unit.get((code, start, end), [])
monkeypatch.setattr(cd, "fetch_cninfo_announcements", fake)
return calls
def test_backfill_ann_units_walk_years_asc_from_2000(root, monkeypatch):
"""unit=(stock,year), 2000→今年逐年, 每年内按池序; 空年=合法 done。"""
import pandas as pd
calls = _wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
[_ann_raw(aid="b1")]})
rc = cd.run_lane("backfill")
assert rc == 0
t = dt.date.today()
n_years = t.year - 2000 + 1
assert len(calls) == n_years * 2 # 每股每年 1 req(空窗 1 页即返)
assert calls[0] == ("000001", "2000-01-01", "2000-12-31")
assert calls[-1][1].startswith(t.isoformat()[:4]) # 今年
# 今年窗口右端=今天(相对), 历年=12-31
assert calls[-1][2] == t.isoformat()
assert cd.is_done("backfill", "ann", "000001_2001")
assert cd.is_done("backfill", "ann", "000001_2000") # 空年也 done
today = t.isoformat()
ann = pd.read_parquet(root / "ann_meta" / f"dt={today}" / "part-0.parquet")
assert len(ann) == 1 and ann.iloc[0]["announcement_id"] == "b1"
def test_backfill_ann_marker_skip_on_rerun(root, monkeypatch):
"""done unit 零重拉(断点续传实证之 mock 版);数据幂等零重复。"""
import pandas as pd
_wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
[_ann_raw(aid="b1")]})
cd.run_lane("backfill")
calls2 = _wire_backfill_ann(monkeypatch, {("000001", "2001-01-01", "2001-12-31"):
[_ann_raw(aid="b1")]})
cd.run_lane("backfill")
assert calls2 == [] # 全部 done → 零请求
today = dt.date.today().isoformat()
ann = pd.read_parquet(root / "ann_meta" / f"dt={today}" / "part-0.parquet")
assert len(ann) == 1
def test_backfill_ann_resume_from_oldest_undone(root, monkeypatch):
"""手动标 2000/2001 done → 重跑只碰 2002 起。"""
cd.mark_done("backfill", "ann", "000001_2000")
cd.mark_done("backfill", "ann", "000001_2001")
cd.mark_done("backfill", "ann", "600519_2000")
cd.mark_done("backfill", "ann", "600519_2001")
calls = _wire_backfill_ann(monkeypatch, {})
cd.run_lane("backfill")
starts = {c[1] for c in calls}
assert "2000-01-01" not in starts and "2001-01-01" not in starts
def test_backfill_ann_unit_fail_not_marked(root, monkeypatch):
calls = _wire_backfill_ann(monkeypatch, {})
monkeypatch.setattr(cd, "fetch_cninfo_announcements",
MagicMock(side_effect=cd.TransportError("boom")))
rc = cd.run_lane("backfill")
assert rc == 1
assert not cd.is_done("backfill", "ann", "000001_2000")
def _wire_news_backfill(monkeypatch, pages):
"""pages: {page_idx: rows} — 翻页回放。"""
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: [("600519", "o1")])
monkeypatch.setattr(cd, "fetch_cninfo_announcements",
MagicMock(return_value=[]))
urls = []
def fake(client, code, mkt, page):
urls.append((code, mkt, page))
return pages.get(page, [])
monkeypatch.setattr(cd, "fetch_news_backfill_page", fake)
return urls
def test_backfill_news_walks_pages_until_empty(root, monkeypatch):
import pandas as pd
np_rows = [{"Art_Code": f"a{i}", "Art_ShowTime": f"2024-01-0{i} 00:00:00",
"Art_Title": f"t{i}", "Art_Url": f"http://x/a{i}.html",
"Np_dst": "CMS"} for i in (1, 2, 3)]
urls = _wire_news_backfill(monkeypatch, {1: np_rows[:2], 2: np_rows[2:]})
rc = cd.run_lane("backfill")
assert rc == 0
assert urls[:2] == [("600519", "1", 1), ("600519", "1", 2)]
assert urls[2] == ("600519", "1", 3) # 第3页空 → 止
assert len(urls) == 3
assert cd.is_done("backfill", "news", "600519_p1")
assert cd.is_done("backfill", "news_end", "600519") # 空页=股票到头
today = dt.date.today().isoformat()
news = pd.read_parquet(root / "news_meta" / f"dt={today}" / "part-0.parquet")
assert len(news) == 3
assert news.iloc[0]["summary"] is None # np 段无摘要如实
def test_backfill_news_end_sentinel_skips_stock(root, monkeypatch):
cd.mark_done("backfill", "news_end", "600519")
urls = _wire_news_backfill(monkeypatch, {1: ["x"]})
cd.run_lane("backfill")
assert urls == [] # 到头股零请求
def test_backfill_news_page_cap_200(root, monkeypatch):
"""page≥200 止(实测硬顶), 到 200 即算该股完结。"""
row = {"Art_Code": "a", "Art_ShowTime": "2020-01-01 00:00:00",
"Art_Title": "t", "Art_Url": "http://x/a.html", "Np_dst": "CMS"}
urls = _wire_news_backfill(monkeypatch,
{p: [dict(row)] for p in range(1, 201)})
cd.run_lane("backfill")
assert len(urls) == 200
assert cd.is_done("backfill", "news_end", "600519")
def test_backfill_news_mkt_prefix_mapping(root, monkeypatch):
urls = _wire_news_backfill(monkeypatch, {})
cd.run_lane("backfill")
assert urls[0][1] == "1" # 600519 → SH=1
urls2 = _wire_news_backfill(monkeypatch, {}) # 只换 fetch 窗口
monkeypatch.setattr(cd, "load_stock_pool", lambda **kw: [("000001", "o")])
cd.run_lane("backfill")
assert urls2[0][1] == "0" # 000001 → SZ=0
def test_backfill_limit_smoke_no_markers(root, monkeypatch):
_wire_backfill_ann(monkeypatch, {})
rc = cd.run_lane("backfill", limit=3)
assert rc == 0
assert not any((root / "state" / "markers" / "backfill").rglob("*.done"))
def test_backfill_wallclock_rc3(root, monkeypatch):
_wire_backfill_ann(monkeypatch, {})
past = (dt.datetime.now() - dt.timedelta(minutes=1)).strftime("%H:%M")
rc = cd.run_lane("backfill", until=past)
assert rc == 3
assert cd.is_done("backfill", "ann", "000001_2000") # 首 unit 完成后停