# -*- coding: utf-8 -*- """ 按构建生成 collect 的回归测试(承 PROGRESS.md #34/#35/#36;AGENTS.md §5.9)。 固化的性质: 1. `outer_commas`: 只数**括号外**的逗号 —— 方括号内的逗号不构成顶层分隔 ('[]' / '[360,360]' / '["zh-CN","zh"]' 拼回去仍是一个值); 2. `SlotSpec` 推算出的槽数 == 真机明文 cd 的实际槽数; 3. 重排是**置换**:值集合不变(排序后逐项相同); 4. 加密/解密**回环一致**(明文逐字节还原); 5. 槽数不匹配的样本**必须报错**,不能静默错位。 样本: tcaptcha/tests/fixtures/tdc_slotmap_case.json = {seq, kinds, v0s, sample_cd},取自 2026-10-11 真机会话(37 项 -> 38 槽)。 运行: .venv/bin/python -m tcaptcha.tests.test_collectgen """ from __future__ import annotations import base64 import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) if str(ROOT / "tools") not in sys.path: sys.path.insert(0, str(ROOT / "tools")) from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402 import tdc_collectgen as cg # noqa: E402 FIX = ROOT / "tcaptcha" / "tests" / "fixtures" / "tdc_slotmap_case.json" # 固定密钥,仅用于回环自洽(不代表任何真实构建) KEY = [1497711441, 1213874789, 1799903316, 1835232858] _fails: list = [] def check(name: str, cond: bool, detail: str = "") -> None: print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" {detail}" if detail else "")) if not cond: _fails.append(name) def main() -> int: if not FIX.exists(): print(f"缺少 fixture: {FIX}") return 1 case = json.loads(FIX.read_text(encoding="utf-8")) seq, kinds, v0s, sample_cd = case["seq"], case["kinds"], case["v0s"], case["sample_cd"] print(f"fixture: 项 {len(seq)} / kind {len(kinds)} / v0 {len(v0s)} / 真机 cd {len(sample_cd)}") print("\n[1] outer_commas: 只数括号外的逗号") check("'[]' -> 0", cg.outer_commas("[]") == 0) check("'[360,360]' -> 0", cg.outer_commas("[360,360]") == 0) check("'[\"zh-CN\",\"zh\"]' -> 0", cg.outer_commas('["zh-CN","zh"]') == 0) check("'641547489,1791732001' -> 1", cg.outer_commas("641547489,1791732001") == 1) check("'a,b,c' -> 2", cg.outer_commas("a,b,c") == 2) print("\n[2] 槽数推算 == 真机明文槽数") spec = cg.SlotSpec(seq, kinds, v0s) print(f" {spec.describe()}") check("槽数一致", spec.n_slots == len(sample_cd), f"{spec.n_slots} vs {len(sample_cd)}") check("fixture 覆盖 kind=1", any(k == 1 for k in kinds)) check("fixture 覆盖 kind=2", any(k == 2 for k in kinds)) print("\n[3] 重排是置换(值集合不变)") out = cg.reorder(sample_cd, spec) key = lambda v: json.dumps(v, sort_keys=True, ensure_ascii=False) check("长度不变", len(out) == len(sample_cd), f"{len(out)} vs {len(sample_cd)}") check("值集合相同(排序后逐项相等)", sorted(map(key, out)) == sorted(map(key, sample_cd))) print("\n[4] 加密/解密回环") plain = cg.build_plain(out) b64 = cg.encrypt_plain(plain, KEY) back = xtea_decrypt_bytes(base64.b64decode(b64), KEY).decode("latin1").rstrip("\x00").strip() check("明文逐字节还原", back == plain) back_obj = json.loads(back) check("cd 还原", back_obj["cd"] == out) check("sd 还原", back_obj["sd"] == {"od": "C", "ft": "qf_7Pf__H"}) print("\n[5] 槽数不匹配必须报错") raised = False try: cg.reorder(sample_cd[:-1], spec) except ValueError: raised = True check("少一槽 -> ValueError", raised) print(f"\n{'=' * 62}") if _fails: print(f"❌ collectgen 测试失败: {_fails}") return 1 print("✅ collectgen 测试全部通过") return 0 if __name__ == "__main__": raise SystemExit(main())