|
@@ -0,0 +1,88 @@
|
|
|
|
|
+# M5D 一致性 + 墙钟验证 Implementation Brief
|
|
|
|
|
+
|
|
|
|
|
+状态:本 brief 是 M5 收尾。覆盖「新建 `test_concurrency_consistency.py`(并发==串行 + 配额 + 兜底)+ R9 固定 run_id 注入 + Jittered fake 暴露乱序 + 墙钟口径重述」。M5A/B/C 全落地后跑,全量 pytest 绿(并发不改结果)。
|
|
|
|
|
+
|
|
|
|
|
+## 目标
|
|
|
|
|
+
|
|
|
|
|
+把 M5 的验收硬门槛"并发结果与串行逐条完全一致"做成可重复跑的测试:同确定性 fake、固定 run_id、max_workers=1 vs 4 全 jsonl 逐字节相等;Jittered fake 让"乱序完成"成测试常态以暴露 offset 错位;cap 截断确定性;analyze 抛错兜底。顺带兑现 R9(固定 run_id 注入)并重述墙钟 baseline。
|
|
|
|
|
+
|
|
|
|
|
+## 现有证据
|
|
|
|
|
+
|
|
|
|
|
+- `tests/test_walk_profile_degradation.py:19` `_fingerprint(walk_actions)` = sorted([edge_id,from,to,walk_action,walk_status,budget_tier,reason_code])——7 字段组**不含 wa_id**(注释:run_id 随机);`tests/fixtures/snapshots/real_id45/walk_actions_fingerprint.json` 7 行基线。
|
|
|
|
|
+- `replay_case`(replay_harness.py:63):`replay_case(case_id, *, runtime_root, cases_dir=CASES_DIR, config_overrides=None, gemini_video_client=None)`;内部 `service.start_run(RunStartRequest(...))`,run_id 由 service 随机生成(run_service.py:99 `f"v1_run_{uuid4().hex[:12]}"`)——**不支持固定 run_id**(R9)。
|
|
|
|
|
+- `FakeGeminiVideoClient`(gemini_helpers.py):按 content_id 确定性返回(`result_by_content_id`/`default_result`);`self.calls.append`(48)**无锁**(并发下顺序非确定)。`fake_gemini_pool/review/fail` 工厂。
|
|
|
|
|
+- real_id45(4 item douyin)/sph_caihong(5 item shipinhao)fixture;`RunStartRequest`(schemas.py)。
|
|
|
|
|
+- 无 `test_concurrency_consistency.py`。
|
|
|
|
|
+
|
|
|
|
|
+## 修改范围
|
|
|
|
|
+
|
|
|
|
|
+- 新建 `tests/test_concurrency_consistency.py`(5 例)。
|
|
|
|
|
+- 改 `tests/replay_harness.py` + `content_agent/run_service.py`(`start_run`)+ `RunStartRequest`(schemas.py):加可选 `run_id`(最小 3 处,兑现 R9)。
|
|
|
|
|
+- 改 `tests/gemini_helpers.py`:`FakeGeminiVideoClient.calls.append` 加 `threading.Lock`;新增 `JitteredFakeGeminiVideoClient`。
|
|
|
|
|
+- (可选)R9 后给 `_fingerprint` 加 wa_id 字段(固定 run_id 下可跨 run 钉死)——若加则重钉快照。
|
|
|
|
|
+
|
|
|
|
|
+## 不修改范围
|
|
|
|
|
+
|
|
|
|
|
+- 不改 recall/walk_engine/gemini_quota 实现(M5B/C);本 brief 只测 + 测试基建。
|
|
|
|
|
+- 不削弱测试(无 assert True/skip/xfail);一致性断言用实跑产物。
|
|
|
|
|
+- 不改判定口径/DB schema;现有测试不得为迁就并发而改断言(并发==串行,应天然全绿)。
|
|
|
|
|
+
|
|
|
|
|
+## 涉及文件 / 函数 / 类(测试用例清单)
|
|
|
|
|
+
|
|
|
|
|
+- `tests/gemini_helpers.py`
|
|
|
|
|
+ - `FakeGeminiVideoClient.__init__` 加 `self._lock = threading.Lock()`;`analyze` 内 `with self._lock: self.calls.append(...)`(消并发 flaky)。
|
|
|
|
|
+ - `class JitteredFakeGeminiVideoClient(FakeGeminiVideoClient)`:`analyze` 内 `time.sleep((int(sha1(content_id)[:4],16)%10)/1000)` 后调 super——确定性制造"完成序≠提交序",返回值仍按 content_id 确定。
|
|
|
|
|
+- `content_agent/run_service.py` / `schemas.py` / `tests/replay_harness.py`(R9)
|
|
|
|
|
+ - `RunStartRequest` 加 `run_id: str | None = None`;`start_run`(99)`run_id = request.run_id or f"v1_run_{uuid4().hex[:12]}"`;`replay_case` 加 `run_id: str | None = None` 透传。
|
|
|
|
|
+- `tests/test_concurrency_consistency.py`(新建)
|
|
|
|
|
+ - `test_serial_vs_concurrent_recall_identical`:同 fake、同固定 run_id,`config_overrides` 设 max_workers=1 与 4 各跑一次,断言 discovered_content_items/pattern_recall_evidence/rule_decisions/walk_actions 全 jsonl 排序后逐条相等。
|
|
|
|
|
+ - `test_jittered_completion_preserves_offset_order`:用 `JitteredFakeGeminiVideoClient`,断言每条 recall_evidence_id ↔ platform_content_id 对齐(offset 未错位)。
|
|
|
|
|
+ - `test_quota_cap_deterministic_truncation`:cap=2、≥5 条内容,断言前 2 条有判定、其余 reason=`gemini_quota_exhausted`,且 max_workers=1/4 截断边界相同。
|
|
|
|
|
+ - `test_quota_exhaustion_is_observable`:命中 cap → evidence_summary 含 quota reason(+ run_event 若实现)。
|
|
|
|
|
+ - `test_analyze_exception_does_not_break_run`:注入 analyze 抛错的 fake → `_safe_analyze` 兜底 status=failed,run state status=success。
|
|
|
|
|
+ - **max_workers 注入(关键)**:`replay_case` 的 `config_overrides` 当前只注入 `policy_store`,**够不到 walk_policy 的 max_workers**(它由 recall_decision 从 `WalkGraphStore().load_policy()` 读磁盘)。故串/并行对照用 `monkeypatch.setattr(recall_decision, "_resolve_max_workers", lambda: 1/4)` 注入(确定、最小);不要依赖 config_overrides。
|
|
|
|
|
+
|
|
|
|
|
+## 数据合同
|
|
|
|
|
+
|
|
|
|
|
+- 固定 run_id + 确定性 fake 下,串行(workers=1)与并发(workers=4)产出的四类 jsonl 逐条相等;Jittered 变体亦相等(证明 offset 归位正确)。
|
|
|
|
|
+- cap 截断按 offset 确定,串/并行边界一致;命中可观测。
|
|
|
|
|
+- 现有全量测试绿(并发不改结果);快照(若加 wa_id)由实跑重钉,不手编。
|
|
|
|
|
+
|
|
|
|
|
+## 实施步骤
|
|
|
|
|
+
|
|
|
|
|
+1. R9:`RunStartRequest`/`start_run`/`replay_case` 加可选 run_id(3 处)。
|
|
|
|
|
+2. gemini_helpers:calls 加锁 + 写 `JitteredFakeGeminiVideoClient`。
|
|
|
|
|
+3. 写 `test_concurrency_consistency.py` 5 例(实跑取产物比对,不手编)。
|
|
|
|
|
+4. (可选)`_fingerprint` 加 wa_id + 重钉 `walk_actions_fingerprint.json`。
|
|
|
|
|
+5. `uv run pytest -q` 全绿 + 墙钟口径写入(见下)。
|
|
|
|
|
+
|
|
|
|
|
+## 墙钟口径(重述,M7 实测核)
|
|
|
|
|
+
|
|
|
|
|
+- 旧 V2 83min 是 decode 主导(M2 已删 decode),**作废,不作分母**。
|
|
|
|
|
+- 串/并行对照在测试里靠 monkeypatch `_resolve_max_workers`(=1 串行、=4 并发),不经 config_overrides。
|
|
|
|
|
+- M5 串行:`T_serial ≈ N × 24s`(Gemini ~24s/条实测,N=初始发现+翻页/作者增量,受 edge_budgets 翻页≤3/作者≤2×作品≤3 约束,N≈20–40)。
|
|
|
|
|
+- M5 并发:`T_concurrent ≈ ceil(N/max_workers) × 24s + 组装/落盘串行开销`(max_workers=4)。
|
|
|
|
|
+- 加速比目标 **分段报告**:判定段(应接近 ×4 上限)vs 整 run(被 12s 限流抓取段稀释);整 run 目标 < 10min(09 §10)。一致性是 must、墙钟是 should(M7 真跑核,写进 run_event raw_payload)。
|
|
|
|
|
+
|
|
|
|
|
+## 验证命令
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+uv run pytest tests/test_concurrency_consistency.py -q
|
|
|
|
|
+uv run pytest tests/test_walk_profile_degradation.py tests/test_case_replay.py -q # 指纹/回放全绿
|
|
|
|
|
+uv run pytest -q # 全量(并发不改结果)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 失败归因
|
|
|
|
|
+
|
|
|
|
|
+- 串/并行不等:offset 回收错位(M5B)或截断按完成序(M5C)——Jittered + cap 测试定位。
|
|
|
|
|
+- Jittered 测试 flaky:`FakeGeminiVideoClient.calls.append` 未加锁。
|
|
|
|
|
+- run_id 注入没生效:`start_run` 仍无条件随机(须 `request.run_id or ...`)。
|
|
|
|
|
+- 快照对不上:手编而非实跑产物;或加 wa_id 后未重钉。
|
|
|
|
|
+- 现有测试被迫改断言:说明并发引入了结果变化(真回归),应回查 M5B/C 而非改测试。
|
|
|
|
|
+
|
|
|
|
|
+## sub-agent 交叉验证要点
|
|
|
|
|
+
|
|
|
|
|
+- 确认一致性测试逐条比对四类产物 + Jittered 暴露乱序 + cap 确定性截断 + 兜底,全部实跑断言、无削弱。
|
|
|
|
|
+- 确认 R9 固定 run_id 三处最小接入、calls 加锁。
|
|
|
|
|
+- 确认现有全量测试绿(并发未改结果)、墙钟口径分段报告且作废旧 83min。
|
|
|
|
|
+- 确认未改 M5B/C 实现、未改判定口径/DB schema。
|