feat(api+web): 任务列表管理(验收四轮批1):历史任务→任务列表改名;DELETE /task/{id}单/批删除(进行中拒删);GET /task/{id}/params参数回放+点任务ID跳参数页预填;真创建时间+耗时列(提交即记,进行中活计时);进行中禁查看;组合任务params全量存 [nas]
CI/CD / test (push) Failing after 10m39s
CI/CD / nas-deploy (push) Has been skipped
CI/CD / nas-verify (push) Has been skipped

This commit is contained in:
2026-08-14 08:29:20 +08:00
parent 94de7191f5
commit 5850a60561
10 changed files with 499 additions and 27 deletions
+174
View File
@@ -0,0 +1,174 @@
"""任务列表管理:删除 + 真创建时间/耗时 + 参数回放 (验收第四轮 批1)。
Direct route-call 风格与 test_portfolio_live.py 一致(不打 JWT)。
"""
import os
import sqlite3
import tempfile
from unittest.mock import Mock, patch
import pandas as pd
import pytest
from fastapi.testclient import TestClient
from sanguo_api.app import create_app
from sanguo_api.auth import set_jwt_config, create_token
from sanguo_backtest.result_store import (
BacktestResult,
record_submission,
save_result,
)
@pytest.fixture()
def client_env():
"""Temp DB + app + token + mock orchestrator(真实 dict 注册表)。"""
set_jwt_config(secret="test", expire_minutes=60)
with tempfile.TemporaryDirectory() as tmp:
db_path = os.path.join(tmp, "test.db")
app = create_app(db_path=db_path, file_dir=tmp)
token = create_token("admin")
orch = Mock()
orch.db_path = db_path
orch.pool._tasks = {}
orch.pool.get_task.return_value = None # Mock 默认非 None 会挡 404 分支
orch._pending = {}
with patch("sanguo_api.routes.get_orchestrator", return_value=orch):
yield TestClient(app), token, orch, db_path, tmp
def _save_done_result(task_id: str, db_path: str, file_dir: str) -> None:
save_result(BacktestResult(
task_id=task_id, type="cta", status="done",
strategy="AShareDoubleMaStrategy", symbol="600000",
params={"fast_window": 10, "slow_window": 30},
start="2024-01-01", end="2024-06-30",
statistics={"total_return": 0.1},
equity_curve=pd.DataFrame([{"date": "2024-01-02", "balance": 1_000_000}]),
trades=pd.DataFrame([{"datetime": "2024-01-05", "price": 10.0, "volume": 100}]),
), db_path=db_path, file_dir=file_dir)
# ===== 真创建时间 + 耗时 =====
def test_list_tasks_uses_submission_time_and_duration(client_env):
client, token, orch, db_path, tmp = client_env
# 提交时间 = 2 小时前(直接写表,避免 sleep)
_save_done_result("cta_abcd1234", db_path, tmp)
conn = sqlite3.connect(db_path)
conn.execute(
"INSERT OR REPLACE INTO task_submissions (task_id, submitted_at) "
"VALUES ('cta_abcd1234', '2024-01-01 06:00:00')"
)
# backtest_stats.created_at 固定为结果插入时间 → 手动改成 08:00 模拟耗时 2h
conn.execute("UPDATE backtest_stats SET created_at='2024-01-01 08:00:00'")
conn.commit()
conn.close()
resp = client.get("/api/v1/task", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
row = next(t for t in resp.json()["tasks"] if t["task_id"] == "cta_abcd1234")
assert row["created_at"].startswith("2024-01-01 14:00") # UTC+8
assert row["finished_at"].startswith("2024-01-01 16:00")
assert row["duration_s"] == 7200
def test_list_tasks_old_row_without_submission_degrades(client_env):
client, token, orch, db_path, tmp = client_env
_save_done_result("cta_old9999", db_path, tmp)
resp = client.get("/api/v1/task", headers={"Authorization": f"Bearer {token}"})
row = next(t for t in resp.json()["tasks"] if t["task_id"] == "cta_old9999")
# 旧行为:created_at=落库时间,duration 缺失
assert row["created_at"] == row["finished_at"]
assert row["duration_s"] is None
def test_record_submission_writes_utc_row(client_env):
client, token, orch, db_path, tmp = client_env
ts = record_submission("cta_new0001", db_path)
conn = sqlite3.connect(db_path)
row = conn.execute(
"SELECT submitted_at FROM task_submissions WHERE task_id='cta_new0001'"
).fetchone()
conn.close()
assert row is not None
assert row[0] == ts
# ===== 删除 =====
def test_delete_task_removes_row_and_files(client_env):
client, token, orch, db_path, tmp = client_env
_save_done_result("cta_del0001", db_path, tmp)
conn = sqlite3.connect(db_path)
eq_path, tr_path = conn.execute(
"SELECT equity_path, trades_path FROM backtest_stats WHERE task_id='cta_del0001'"
).fetchone()
conn.close()
assert eq_path and os.path.exists(eq_path)
assert tr_path and os.path.exists(tr_path)
resp = client.delete("/api/v1/task/cta_del0001", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
assert resp.json()["deleted"] == 1
assert not os.path.exists(eq_path)
assert not os.path.exists(tr_path)
# 列表里也没了
resp = client.get("/api/v1/task", headers={"Authorization": f"Bearer {token}"})
assert all(t["task_id"] != "cta_del0001" for t in resp.json()["tasks"])
def test_delete_running_task_refused(client_env):
client, token, orch, db_path, tmp = client_env
task = Mock()
task.status.value = "running"
orch.pool.get_task.return_value = task
resp = client.delete("/api/v1/task/cta_run0001", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 400
assert "进行中" in resp.json()["detail"]
def test_delete_unknown_task_404(client_env):
client, token, orch, db_path, tmp = client_env
resp = client.delete("/api/v1/task/cta_none001", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 404
# ===== 参数回放 =====
def test_task_params_returns_saved_params(client_env):
client, token, orch, db_path, tmp = client_env
_save_done_result("cta_par0001", db_path, tmp)
resp = client.get("/api/v1/task/cta_par0001/params", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
data = resp.json()
assert data["type"] == "cta"
assert data["strategy"] == "AShareDoubleMaStrategy"
assert data["symbol"] == "600000"
assert data["params"] == {"fast_window": 10, "slow_window": 30}
assert data["start"] == "2024-01-01"
def test_task_params_running_reads_pending_spec(client_env):
client, token, orch, db_path, tmp = client_env
task = Mock()
task.status.value = "running"
task.task_type = "portfolio"
orch.pool.get_task.return_value = task
orch._pending["portfolio_run01"] = {
"strategy": "all_weather", "benchmark": "000300.XSHG",
"start": "2024-01-01", "end": "2024-12-31", "max_pool": 30,
}
resp = client.get("/api/v1/task/portfolio_run01/params", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
data = resp.json()
assert data["strategy"] == "all_weather"
assert data["symbol"] == "000300.XSHG" # portfolio spec 用 benchmark 兜底
assert data["status"] == "running"
def test_task_params_unknown_404(client_env):
client, token, orch, db_path, tmp = client_env
resp = client.get("/api/v1/task/cta_none002/params", headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 404