Refactor project components and workflows
Deploy production / deploy (push) Successful in 51s

This commit is contained in:
yuxuanhui
2026-09-20 11:20:51 +08:00
parent 13a2168ca5
commit 69c19ed25f
23 changed files with 1146 additions and 550 deletions
+133 -2
View File
@@ -100,7 +100,7 @@ async def test_sdk_template_creation_frozen_evidence_and_web_expansion(app, logg
assert "submit_backtests" not in listed
assert not tool.annotations.read_only_hint and not tool.annotations.destructive_hint
assert tool.annotations.idempotent_hint and not tool.annotations.open_world_hint
assert {"template", "hypothesis", "source_item_ids", "idempotency_key"} <= set(tool.input_schema["required"])
assert {"template", "hypothesis", "idempotency_key"} <= set(tool.input_schema["required"])
assert tool.input_schema["additionalProperties"] is False
caps = await client.call_tool("get_research_capabilities", {})
assert caps.structured_content["templates"]["create_with"] == TOOL
@@ -169,7 +169,7 @@ async def test_template_invalid_inputs_and_missing_sources_are_atomic(app, compl
principal, _ = await credentials(app)
valid = template_request(completed_source["id"])
variants = [
{"source_item_ids": []}, {"source_item_ids": [completed_source["id"]] * 21},
{"source_item_ids": [completed_source["id"]] * 21},
{"source_item_ids": [completed_source["id"]] * 2}, {"hypothesis": " "},
{"force": True}, {"idempotency_key": ""},
{"template": valid["template"] | {"expression": "rank({missing})"}},
@@ -265,3 +265,134 @@ async def test_mcp_template_version_is_idempotent_and_preserves_history(app, com
with pytest.raises(HTTPException) as forbidden:
await app.state.mcp.invoke(denied, tool, update)
assert forbidden.value.status_code == 403
async def test_template_without_result_sources_can_be_created_and_versioned(app):
principal, _ = await credentials(app, {"research:read", "research:write"})
body = template_request("unused")
body.pop("source_item_ids")
created = await invoke(app, principal, TOOL, body)
assert created["provenance"]["source_items"] == []
assert created["validation"]["source_evidence"] == "not_provided"
revised = await invoke(app, principal, "create_research_template_version", {
**body, "template_id": created["id"], "expected_version": 1, "idempotency_key": "no-source-v2",
"source_item_ids": [],
})
assert revised["version"] == 2 and revised["provenance"]["source_items"] == []
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 0
async def template_collection(app, principal, research_input, count=130):
body = template_request("unused")
body.pop("source_item_ids")
body["template"]["variables"]["field"]["values"] = ["TEST_FIN_001"]
body["template"]["variables"]["offset"]["values"] = list(range(count))
saved = await invoke(app, principal, TOOL, body)
args = {
"template_id": saved["id"], "version": saved["version"],
"preparation_refs": [{"id": research_input["preparation_id"], "version": research_input["preparation_version"]}],
"settings": candidate()["settings"], "limit": count, "idempotency_key": "expand-collection",
}
return saved, args
async def test_template_collection_mcp_paging_execution_and_replay(app, logged_in, research_input):
from sqlalchemy import delete
from app.models import BacktestPreview, CatalogResource, ResearchExperiment
principal, _ = await credentials(app)
saved, args = await template_collection(app, principal, research_input)
async with app.state.sessions.begin() as db:
await db.execute(delete(CatalogResource))
app.state.runner.backtests.wake.clear()
first, retry = await asyncio.gather(*[invoke(app, principal, "expand_research_template", args) for _ in range(2)])
assert first == retry and first["total"] == 130 and len(first["items"]) == 25
assert first["has_more"] and not first["starts_backtests"]
assert first["template"]["id"] == saved["id"] and first["template"]["version"] == 1
assert not app.state.runner.backtests.wake.is_set()
collected = []
for offset in (0, 100):
result = await invoke(app, principal, "get_template_candidates", {
"experiment_id": first["experiment_id"], "limit": 100, "offset": offset,
})
collected += result["items"]
assert len(collected) == 130 and not result["has_more"]
assert all("validation" not in item for item in collected)
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(ResearchExperiment)) == 1
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 0
args = {"experiment_id": first["experiment_id"], "candidate_ids": [c["client_item_id"] for c in collected],
"idempotency_key": "execute-collection"}
run, replay = await asyncio.gather(*[invoke(app, principal, "start_template_backtest", args) for _ in range(2)])
assert run == replay and run["total"] == 130
assert run["source"]["kind"] == "template" and run["source"]["research_id"] == first["experiment_id"]
assert run["source"]["input_snapshot_ids"]
assert app.state.runner.backtests.wake.is_set()
rotated, _ = await credentials(app)
assert await invoke(app, rotated, "start_template_backtest", args) == run
conflict = await app.state.mcp.invoke(principal, "start_template_backtest", args | {"candidate_ids": ["c1"]})
assert conflict.is_error and conflict.structured_content["error"]["code"] == "IDEMPOTENCY_CONFLICT"
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 1
assert await db.scalar(select(func.count()).select_from(BacktestPreview)) == 1
row = await db.get(BacktestRun, run["backtest_run_id"])
assert row.ai_context["mcp_token_id"] == principal.token_id
audit = await db.scalar(select(MCPAudit).where(MCPAudit.tool == "expand_research_template"))
assert audit.business_id == first["experiment_id"]
record = (await logged_in.get(f"/api/v1/research/experiments/{first['experiment_id']}")).json()
assert record["backtest_run_ids"] == [run["backtest_run_id"]]
async def test_template_mcp_failures_are_atomic_and_do_not_consume_keys(app, research_input):
from app.models import BacktestPreview, ResearchExperiment
principal, _ = await credentials(app)
saved, args = await template_collection(app, principal, research_input, 2)
for changed in [args | {"settings": args["settings"] | {"region": "EUR"}},
args | {"preparation_refs": [{**args["preparation_refs"][0], "version": 999}]}]:
result = await app.state.mcp.invoke(principal, "expand_research_template", changed)
assert result.is_error
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(ResearchExperiment)) == 0
assert not await db.scalar(select(ResearchRequest).where(ResearchRequest.operation == "expand_research_template"))
collection = await invoke(app, principal, "expand_research_template", args)
conflict = await app.state.mcp.invoke(principal, "expand_research_template", args | {"seed": 1})
assert conflict.structured_content["error"]["code"] == "IDEMPOTENCY_CONFLICT"
request = {"experiment_id": collection["experiment_id"], "candidate_ids": ["c1"], "idempotency_key": "execute"}
for ids in [[], ["unknown"], ["c1", "c1"]]:
result = await app.state.mcp.invoke(principal, "start_template_backtest", request | {"candidate_ids": ids})
assert result.is_error
result = await app.state.mcp.invoke(principal, "start_template_backtest", request | {"expression": "rank(other)"})
assert result.is_error and result.structured_content["error"]["code"] == "INVALID_INPUT"
async with app.state.sessions() as db:
assert await db.scalar(select(func.count()).select_from(BacktestRun)) == 0
assert await db.scalar(select(func.count()).select_from(BacktestPreview)) == 0
assert not await db.scalar(select(ResearchRequest).where(ResearchRequest.operation == "start_template_backtest"))
await invoke(app, principal, "start_template_backtest", request)
async def test_template_tool_discovery_and_execute_permissions(app, research_input):
from fastapi import HTTPException
principal, secret = await credentials(app, {"research:read", "research:write"})
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver",
headers={"Authorization": f"Bearer {secret}", "Accept": "application/json, text/event-stream"}) as http:
listed = (await http.post(ENDPOINT, json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})).json()
tools = {t["name"]: t for t in listed["result"]["tools"]}
assert "expand_research_template" in tools and "get_template_candidates" in tools
assert "start_template_backtest" not in tools
assert "source_item_ids" not in tools[TOOL]["inputSchema"]["required"]
assert tools["expand_research_template"]["annotations"]["idempotentHint"]
_, args = await template_collection(app, principal, research_input, 2)
collection = await invoke(app, principal, "expand_research_template", args)
with pytest.raises(HTTPException) as denied:
await app.state.mcp.invoke(principal, "start_template_backtest", {
"experiment_id": collection["experiment_id"], "candidate_ids": ["c1"], "idempotency_key": "denied",
})
assert denied.value.status_code == 403
reader, _ = await credentials(app, {"research:read"})
await invoke(app, reader, "get_template_candidates", {"experiment_id": collection["experiment_id"]})
with pytest.raises(HTTPException):
await app.state.mcp.invoke(reader, "expand_research_template", args)