"""SUPER public HTTP/MCP acceptance using isolated persistence and synthetic upstream.""" import asyncio from copy import deepcopy import pytest from fastapi import HTTPException from sqlalchemy import func, select from app.backtests.contracts import Candidate from app.models import SimulationAttempt, SuperSelectionSnapshot from app.superalpha.evidence import parse_components from tests.test_backtests import PARAMS, candidate, execute, preview, setup, start from tests.test_mcp import credentials, invoke, mcp_app # noqa: F401 SETTINGS = PARAMS | {"selectionHandling": "POSITIVE", "selectionLimit": 100, "componentActivation": "IS"} def super_candidate(index=0, **changes): return {"client_item_id": f"super-{index}", "alpha_type": "SUPER", "selection": f"turnover < {0.1 + index / 10}", "combo": "alpha", "settings": SETTINGS} | changes def plan(**changes): return {"name": "Super 研究", "hypothesis": "降低组件换手", "selection": "turnover < {threshold}", "combo": "alpha", "variables": {"threshold": {"kind": "number", "values": [0.1, 0.2]}}, "settings": SETTINGS, "include_baseline": True} | changes async def test_plan_selection_build_versions_and_generic_run(app, logged_in): platform, lane = await setup(app) body = {"plan": plan(), "idempotency_key": "save1"} saved = (await logged_in.post("/api/v1/superalpha/plans", json=body)).json() assert saved["version"] == 1, saved assert (await logged_in.post("/api/v1/superalpha/plans", json=body)).json() == saved selection = {"selection": "turnover < 0.1", "settings": SETTINGS, "plan_id": saved["id"], "version": 1} job = (await logged_in.post("/api/v1/superalpha/selections", json=selection)).json() assert (await logged_in.post("/api/v1/superalpha/selections", json=selection)).json()["job_id"] == job["job_id"] await app.state.runner.run_next() snap = (await logged_in.get(f"/api/v1/superalpha/selections?job_id={job['job_id']}&limit=1")).json() assert snap["complete"] and snap["has_more"] and snap["total"] == 2, snap assert len(platform.selection_reads) == 1 and not platform.posts assert set(platform.selection_reads[0]) == {"selection", "instrumentType", "region", "delay", "selectionLimit", "selectionHandling"} build = {"plan_id": saved["id"], "version": 1, "selection_snapshot_ids": [snap["snapshot_id"]], "idempotency_key": "build1"} exp = (await logged_in.post("/api/v1/superalpha/candidates", json=build)).json() assert exp["total"] == 4, exp assert (await logged_in.post("/api/v1/superalpha/candidates", json=build)).json() == exp # A subset keeps the original experiment's evidence without claiming it applies to every candidate. p = await logged_in.post(f"/api/v1/superalpha/experiments/{exp['id']}/preview", json={"candidate_ids": ["super-2", "super-2-baseline"]}) assert p.status_code == 200, p.text assert p.json()["batch_count"] == 2 and not platform.posts rid = (await start(logged_in, p.json()))["backtest_run_id"] await execute(app, lane, rid) result = (await logged_in.get(f"/api/v1/backtests/runs/{rid}/results")).json() assert all(i["persistence_status"] == "saved" and i["alpha_type"] == "SUPER" for i in result["items"]), result assert len(platform.posts) == 2 and all(len(p) == 1 for p in platform.posts) item = result["items"][0] actual = (await logged_in.get(f"/api/v1/backtests/items/{item['id']}/artifact?kind=components")).json() assert actual["complete"] and actual["source"] == "actual" and actual["snapshot_id"] != snap["snapshot_id"] assert actual["component_hash"] == snap["component_hash"] alpha = (await logged_in.get(f"/api/v1/superalpha/alphas/{item['alpha_id']}")).json() assert alpha["descriptions"]["combo"] == "Combo rationale" assert alpha["sources"]["items"][0]["source"]["superalpha_plan_version"] == 1 changed = body | {"plan": plan(name="修订"), "plan_id": saved["id"], "version": 1, "idempotency_key": "save2"} assert (await logged_in.post("/api/v1/superalpha/plans", json=changed)).json()["version"] == 2 assert (await logged_in.post("/api/v1/superalpha/plans", json=changed | {"idempotency_key": "save3"})).status_code == 409 assert (await logged_in.delete(f"/api/v1/superalpha/plans/{saved['id']}?version=2")).status_code == 200 assert (await logged_in.get(f"/api/v1/superalpha/plans/{saved['id']}?version=1")).json()["content"]["name"] == "Super 研究" rows = (await logged_in.get(f"/api/v1/superalpha/experiments/{exp['id']}/results")).json() assert rows["total"] == 2 and rows["items"][0]["pnl_fetched_at"] is None async with app.state.sessions() as db: assert await db.scalar(select(func.count()).select_from(SuperSelectionSnapshot)) == 3 async def test_mixed_run_isolation_facets_export_and_strict_match(app, logged_in): platform, lane = await setup(app) p = await preview(logged_in, [candidate(0), candidate(1), super_candidate(), super_candidate(1)]) assert p["batch_count"] == 3 rid = (await start(logged_in, p))["backtest_run_id"] await execute(app, lane, rid) assert len(platform.posts) == 3 results = (await logged_in.get(f"/api/v1/backtests/runs/{rid}/results")).json()["items"] assert all(i["persistence_status"] == "saved" for i in results), results for scope, kind in (("super", "SUPER"), ("non_super", "REGULAR")): page = (await logged_in.get(f"/api/v1/alphas?management_scope={scope}")).json() assert page["total"] == 2 and all(i["alpha_type"] == kind for i in page["items"]) facets = (await logged_in.get(f"/api/v1/alphas/facets?management_scope={scope}")).json() assert facets["alpha_type"] == [kind], facets export = await logged_in.get(f"/api/v1/alphas/export?management_scope={scope}") assert export.status_code == 200 assert all(i["alpha_id"] in export.text for i in results if i["alpha_type"] == kind) assert all(i["alpha_id"] not in export.text for i in results if i["alpha_type"] != kind) rid2 = (await start(logged_in, await preview(logged_in, [super_candidate(9)]), "mismatch"))["backtest_run_id"] async with app.state.sessions() as db: aid = await db.scalar(select(SimulationAttempt.id).where(SimulationAttempt.run_id == rid2)) await lane.step(aid) next(reversed(platform.alphas.values()))["combo"]["code"] = "WRONG" await lane.step(aid) row = (await logged_in.get(f"/api/v1/backtests/runs/{rid2}/results")).json()["items"][0] assert row["persistence_status"] != "saved" assert (await logged_in.get("/api/v1/backtests/runs?alpha_type=SUPER")).json()["total"] == 2 @pytest.mark.parametrize("reject", ["unknown", "missing_location", "rate"]) async def test_super_reliability_no_unknown_resubmission(app, logged_in, reject): platform, lane = await setup(app) platform.reject = reject rid = (await start(logged_in, await preview(logged_in, [super_candidate()])))["backtest_run_id"] async with app.state.sessions() as db: aid = await db.scalar(select(SimulationAttempt.id).where(SimulationAttempt.run_id == rid)) for _ in range(app.state.settings.retry_attempts + 1): await lane.step(aid) await asyncio.sleep(0.02) if reject == "rate": assert len(platform.posts) == app.state.settings.retry_attempts else: await lane.start() await lane.stop() await logged_in.post(f"/api/v1/backtests/runs/{rid}/control", json={"action": "recover", "version": 1}) await lane.step(aid) assert len(platform.posts) == 1 async def test_unknown_components_and_recovery(app, logged_in): platform, lane = await setup(app) platform.actual_components = None platform.detail_fail = True rid = (await start(logged_in, await preview(logged_in, [super_candidate()])))["backtest_run_id"] ids = await execute(app, lane, rid) platform.detail_fail = False await logged_in.post(f"/api/v1/backtests/runs/{rid}/control", json={"action": "recover", "version": 1}) await lane.step(ids[0]) item = (await logged_in.get(f"/api/v1/backtests/runs/{rid}/results")).json()["items"][0] assert item["persistence_status"] == "saved" and len(platform.posts) == 1 components = (await logged_in.get(f"/api/v1/backtests/items/{item['id']}/artifact?kind=components")).json() assert components["complete"] is False and components["component_hash"] is None async def test_sampling_validation_and_component_evidence(app, logged_in): await setup(app) for invalid in (super_candidate(selection=" "), super_candidate(combo=""), super_candidate(expression="close"), super_candidate(settings=PARAMS)): with pytest.raises(ValueError): Candidate.model_validate(invalid) assert (await logged_in.get("/api/v1/superalpha/selections")).status_code == 422 assert parse_components({"count": 0, "results": []})["complete"] for raw in (["a"], {"count": 2, "results": ["a"]}, {"count": 2, "results": ["a", "a"]}, {"count": 1, "results": ["a"], "next": "next"}): assert not parse_components(raw)["complete"] complete = parse_components({"count": 1, "results": ["a"], "warnings": ["synthetic warning"]}) assert complete["complete"] and complete["warnings"] == ["synthetic warning"] args = {"plan": plan(variables={"threshold": {"kind": "number", "values": list(range(100))}}), "mode": "random", "limit": 5, "seed": 42, "idempotency_key": "random1"} a = (await logged_in.post("/api/v1/superalpha/candidates", json=args)).json() b = (await logged_in.post("/api/v1/superalpha/candidates", json=args | {"idempotency_key": "random2"})).json() assert a["candidates"] == b["candidates"] and a["total"] == 10 bad = deepcopy(args) bad["plan"]["setting_variants"] = {"selectionLimit": [0]} assert (await logged_in.post("/api/v1/superalpha/candidates", json=bad)).status_code == 422 async def test_mcp_interop_permissions_inline_and_sources(mcp_app): # noqa: F811 app = mcp_app principal, _ = await credentials(app, {"research:read", "research:write", "research:refresh", "backtests:execute"}) saved = await invoke(app, principal, "save_superalpha_plan", {"plan": plan(), "idempotency_key": "mcp-plan"}) built = await invoke(app, principal, "build_superalpha_candidates", {"plan_id": saved["id"], "version": 1, "idempotency_key": "mcp-build"}) assert not built["starts_backtests"] and built["total"] == 4 assert (await invoke(app, principal, "get_superalpha_plan", {"experiment_id": built["id"], "limit": 1}))["has_more"] submit = {"name": "MCP SUPER", "candidates": built["candidates"][:1], "source": {"research_id": built["id"], "superalpha_plan_id": saved["id"], "superalpha_plan_version": 1}, "duplicate_policy": "rerun", "idempotency_key": "mcp-run"} result = await invoke(app, principal, "submit_backtests", submit) assert result == await invoke(app, principal, "submit_backtests", submit) rid = result["backtest_run_id"] await execute(app, app.state.runner.backtests, rid) results = await invoke(app, principal, "get_backtest_results", {"run_id": rid}) item = results["items"][0] assert item["persistence_status"] == "saved", results alpha = await invoke(app, principal, "get_superalpha", {"alpha_id": item["alpha_id"]}) assert alpha["alpha_type"] == "SUPER" and "#superalphas?" in alpha["web_url"] forged = deepcopy(submit) forged["idempotency_key"] = "forged" forged["candidates"][0]["combo"] = "WRONG" assert (await app.state.mcp.invoke(principal, "submit_backtests", forged)).is_error direct = submit | {"source": {}, "candidates": [super_candidate(9)], "idempotency_key": "direct"} assert (await invoke(app, principal, "submit_backtests", direct))["source"]["superalpha_plan_id"] is None readonly, _ = await credentials(app, {"research:read"}) with pytest.raises(HTTPException) as exc: await app.state.mcp.invoke(readonly, "save_superalpha_plan", {"plan": plan(), "idempotency_key": "denied"}) assert exc.value.status_code == 403 async def test_metadata_stage_constraints_selection_recovery_and_cancel(app, logged_in): from app.catalog.research_metadata import ResearchMetadata from app.models import Job from app.superalpha.jobs import run_selection platform, _ = await setup(app) async with app.state.sessions.begin() as db: await ResearchMetadata(db).publish("settings", "settings", {"items": [{"instrument_type": "EQUITY", "region": "USA", "delay": 1, "universe": "TOP3000", "neutralizations": ["INDUSTRY"], "fields": {"selectionLimit": {"maximum": 50}}}]}) await ResearchMetadata(db).publish("operators", "operators", {"items": [ {"name": "combo_a", "category": "Combo", "scope": ["COMBO"]}, {"name": "not_combo", "category": "Other", "scope": ["UNKNOWN_COMBO"]}, {"name": "unspecified", "category": "Other", "scope": None}]}) assert (await logged_in.post("/api/v1/superalpha/plans", json={"plan": plan(), "idempotency_key": "bad-settings"})).status_code == 422 p = await logged_in.post("/api/v1/backtests/previews", json={"inline": {"name": "bad", "candidates": [super_candidate()]}}) assert p.status_code == 422 assert [o["name"] for o in (await logged_in.get("/api/v1/catalog/operators?stage=COMBO")).json()["items"]] == ["combo_a"] payload = {"selection": "turnover", "settings": SETTINGS | {"selectionLimit": 50}} job = (await logged_in.post("/api/v1/superalpha/selections", json=payload)).json() await app.state.runner.run_next() await run_selection(app.state.runner, job["job_id"], payload) # Restart after commit preserves evidence. assert len(platform.selection_reads) == 1 cancelled = (await logged_in.post("/api/v1/superalpha/selections", json=payload)).json() async with app.state.sessions.begin() as db: (await db.get(Job, cancelled["job_id"])).cancel_requested = True await app.state.runner.run_next() async with app.state.sessions() as db: assert (await db.get(Job, cancelled["job_id"])).status == "cancelled" assert await db.scalar(select(func.count()).select_from(SuperSelectionSnapshot)) == 1