"""§12.6 补:策略代码版本快照(发起时快照/去重/防穿越/代码已变更标记)。""" import json import os import sqlite3 import pytest from fastapi.testclient import TestClient from sanguo_api import code_versions as CV from sanguo_api import instance_store from sanguo_api.app import create_app from sanguo_api.auth import create_token, set_jwt_config from sanguo_api.routes_paper import set_db_path as set_paper_db def test_snapshot_dedup_and_read(tmp_path, monkeypatch): """同内容只存一份(内容寻址去重);list/read 往返一致。""" monkeypatch.setattr(CV, "_DIR", str(tmp_path / "versions")) # 造一个假策略文件(monkeypatch registry 读) class _FakeFile: def __init__(self, src): self._src = src def __call__(self, name): return {"name": name, "code": self._src} monkeypatch.setattr("sanguo_api.strategy_registry.read_strategy_file", _FakeFile("print(1)\n")) s1 = CV.snapshot_code("demo.py") s2 = CV.snapshot_code("demo.py") assert s1["code_version"] == s2["code_version"] files = os.listdir(tmp_path / "versions") assert len(files) == 1 # 去重 vers = CV.list_versions("demo.py") assert len(vers) == 1 and vers[0]["code_version"] == s1["code_version"] assert CV.read_version("demo.py", s1["code_version"]) == "print(1)\n" def test_read_version_rejects_traversal(tmp_path, monkeypatch): """版本号只认 8 位 hex:路径穿越串直接拒绝。""" monkeypatch.setattr(CV, "_DIR", str(tmp_path / "versions")) assert CV.read_version("demo.py", "../../etc/passwd") is None assert CV.read_version("demo.py", "ZZZZZZZZ") is None def test_code_changed_flag(tmp_path, monkeypatch): monkeypatch.setattr(CV, "_DIR", str(tmp_path / "versions")) class _FakeFile: def __init__(self, src): self._src = src def __call__(self, name): return {"name": name, "code": self._src} fake = _FakeFile("v1") monkeypatch.setattr("sanguo_api.strategy_registry.read_strategy_file", fake) snap = CV.snapshot_code("demo.py") assert CV.code_changed("demo.py", snap["code_hash"]) is False fake._src = "v2-changed" # 文件改了 assert CV.code_changed("demo.py", snap["code_hash"]) is True assert CV.code_changed("demo.py", None) is None # 早期账户无哈希=不可判 def _client(tmp_path, monkeypatch): monkeypatch.setattr(instance_store, "_STORE_PATH", str(tmp_path / "inst.json")) monkeypatch.setattr(CV, "_DIR", str(tmp_path / "versions")) set_jwt_config(secret="t", expire_minutes=60) pdb = os.path.join(str(tmp_path), "p.db") app = create_app(db_path=pdb) set_paper_db(pdb) return TestClient(app), create_token("admin"), pdb def test_paper_create_stores_code_hash(tmp_path, monkeypatch): """发起模拟盘 → 账户落 code_hash + 快照文件存在(code_file 能解析到真实策略时)。""" c, token, pdb = _client(tmp_path, monkeypatch) r = c.post("/api/v1/paper/create", json={ "mode": "live", "symbols": ["600000"], "strategies": [{"name": "AShareDoubleMaStrategy", "symbol": "600000", "params": {"fast_window": 5}}], "start": "2024-01-01", "end": "", }, headers={"Authorization": f"Bearer {token}"}) assert r.status_code == 200 h = sqlite3.connect(pdb).execute( "SELECT code_hash FROM paper_accounts WHERE id=1").fetchone()[0] # 类名解析到 double_ma.py → 快照成功有哈希;解析不到也是 None 不崩 if h: assert len(h) == 32 assert any(f.startswith("double_ma.py.") for f in os.listdir(tmp_path / "versions")) def test_enriched_code_changed_flag(tmp_path, monkeypatch): """文件后续被改 → enriched 亮 code_changed。""" c, token, pdb = _client(tmp_path, monkeypatch) c.post("/api/v1/paper/create", json={ "mode": "live", "symbols": ["600000"], "strategies": [{"name": "AShareDoubleMaStrategy", "symbol": "600000", "params": {}}], "start": "2024-01-01", "end": "", }, headers={"Authorization": f"Bearer {token}"}) insts = c.get("/api/v1/strategy/instances/enriched").json()["instances"] assert insts[0]["code_changed"] in (False, None) # 刚发起必然一致/不可判 # 改当前文件哈希(模拟代码变更) monkeypatch.setattr(CV, "current_hash", lambda f: "deadbeef") insts = c.get("/api/v1/strategy/instances/enriched").json()["instances"] me = insts[0] if me["running_accounts"]: acc = me["running_accounts"][0] if acc.get("code_version"): # 有哈希的账户才可判 assert me["code_changed"] is True def test_code_version_endpoints(tmp_path, monkeypatch): """GET /strategy/code-versions 列表 + 单版本全文。""" c, token, _ = _client(tmp_path, monkeypatch) class _FakeFile: def __call__(self, name): return {"name": name, "code": "x = 1\n"} monkeypatch.setattr("sanguo_api.strategy_registry.read_strategy_file", _FakeFile()) snap = CV.snapshot_code("demo.py") lst = c.get("/api/v1/strategy/code-versions?file=demo.py").json() assert lst["versions"] and lst["versions"][0]["code_version"] == snap["code_version"] one = c.get(f"/api/v1/strategy/code-versions/demo.py/{snap['code_version']}").json() assert one["code"] == "x = 1\n" assert c.get("/api/v1/strategy/code-versions/demo.py/00000000").status_code == 404