auto_agent/backend/tests/test_task_api.py
redbotu 5021c8c2ea feat: 补齐任务执行指标与结构化结果摘要
- 补齐 tool_call 和 edge 验证链路的 duration_ms 计算与返回
- 任务详情和任务报告新增 result_summary_detail 结构化摘要
- 摘要中补充最终状态、失败原因、software-a 摘要、审批摘要、验证摘要
- 软件A层术语统一为“最小能力实现”
- 同步更新 README、当前进度总结和相关设计文档
- 补充并通过对应自动化测试
2026-04-08 22:35:25 +08:00

615 lines
24 KiB
Python

import os
from fastapi.testclient import TestClient
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
from app.main import app
def test_task_create_confirm_get() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
headers={"X-Request-Id": "req-test-create-001"},
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-001",
"tenant_id": "tenant-demo",
"context": {"preferred_env": "test"},
},
)
assert create_response.status_code == 200
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
headers={"X-Request-Id": "req-test-confirm-001"},
json={"confirmed": True, "comment": "确认执行"},
)
assert confirm_response.status_code == 200
assert confirm_response.json()["data"]["software_a_task_id"] is not None
assert confirm_response.json()["data"]["software_a_task_status"] == "RUNNING"
get_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_response.status_code == 200
assert get_response.json()["data"]["task_id"] == task_id
assert get_response.json()["data"]["software_a_task_id"] is not None
assert get_response.json()["data"]["software_a_task_status"] == "SUCCEEDED"
assert get_response.json()["data"]["tool_calls"][0]["tool_name"] == "software_a_deploy"
assert get_response.json()["data"]["result_summary_detail"]["final_status"] == "RUNNING"
assert get_response.json()["data"]["result_summary_detail"]["software_a"]["task_status"] == "SUCCEEDED"
def test_high_risk_task_creates_approval_and_can_be_approved() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to prod",
"channel": "WEB",
"session_id": "sess-002",
"tenant_id": "tenant-demo",
"context": {},
},
)
assert create_response.status_code == 200
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "need approval"},
)
assert confirm_response.status_code == 200
approval_id = confirm_response.json()["data"]["approval_id"]
assert approval_id is not None
assert confirm_response.json()["data"]["task_status"] == "PENDING_APPROVAL"
approval_response = client.get(f"/api/demo/approval/requests/{approval_id}")
assert approval_response.status_code == 200
assert approval_response.json()["data"]["approval_status"] == "PENDING"
decision_response = client.post(
f"/api/demo/approval/requests/{approval_id}/decision",
json={
"decision": "APPROVED",
"comment": "approved",
"operator": {"user_id": "u2001", "user_name": "bob"},
},
)
assert decision_response.status_code == 200
assert decision_response.json()["data"]["approval_status"] == "APPROVED"
get_task_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_task_response.status_code == 200
assert get_task_response.json()["data"]["task_status"] == "RUNNING"
assert get_task_response.json()["data"]["approval_status"] == "APPROVED"
assert get_task_response.json()["data"]["software_a_task_id"] is not None
assert get_task_response.json()["data"]["software_a_task_status"] == "SUCCEEDED"
def test_demo_identity_and_software_a_endpoints() -> None:
with TestClient(app) as client:
login_response = client.post(
"/api/demo/identity/login",
json={"username": "alice", "password": "demo-password"},
)
assert login_response.status_code == 200
token = login_response.json()["data"]["access_token"]
me_response = client.get(
"/api/demo/identity/me",
headers={"Authorization": f"Bearer {token}"},
)
assert me_response.status_code == 200
assert me_response.json()["data"]["user_name"] == "alice"
deploy_response = client.post(
"/api/demo/software-a/deploy-tasks",
json={
"operator": {"user_id": "u1001", "user_name": "alice"},
"tenant_id": "tenant-demo",
"app_code": "order-service",
"env": "test",
"version": "1.2.3",
"target_nodes": ["10.0.0.12"],
"deploy_options": {"graceful": True},
},
)
assert deploy_response.status_code == 200
software_a_task_id = deploy_response.json()["data"]["software_a_task_id"]
query_response = client.get(f"/api/demo/software-a/deploy-tasks/{software_a_task_id}")
assert query_response.status_code == 200
assert query_response.json()["data"]["task_status"] == "SUCCEEDED"
def test_edge_heartbeat_pull_and_report_flow() -> None:
with TestClient(app) as client:
heartbeat_response = client.post(
"/api/agent/edge/heartbeat",
json={
"edge_id": "edge-shanghai-001",
"hostname": "customer-host-01",
"os_type": "WINDOWS",
"agent_version": "0.1.0",
"capabilities": ["http_health_check"],
},
)
assert heartbeat_response.status_code == 200
assert heartbeat_response.json()["data"]["node_status"] == "ONLINE"
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-003",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
assert confirm_response.status_code == 200
pull_response = client.post(
"/api/agent/edge/tasks/pull",
json={"edge_id": "edge-shanghai-001", "max_tasks": 5},
)
assert pull_response.status_code == 200
tasks = pull_response.json()["data"]["tasks"]
matched_tasks = [item for item in tasks if item["task_id"] == task_id]
assert len(matched_tasks) == 1
step_id = matched_tasks[0]["step_id"]
assert matched_tasks[0]["tool_name"] == "http_health_check"
report_response = client.post(
"/api/agent/edge/tasks/report",
json={
"edge_id": "edge-shanghai-001",
"task_id": task_id,
"step_id": step_id,
"tool_name": "http_health_check",
"success": True,
"code": "OK",
"message": "200 OK",
"data": {"status_code": 200, "latency_ms": 45},
"evidence": {"response_body": "{\"status\":\"UP\"}"},
"started_at": "2026-04-08 20:20:00.000",
"finished_at": "2026-04-08 20:20:00.100",
},
)
assert report_response.status_code == 200
assert report_response.json()["data"]["task_status"] == "SUCCEEDED"
get_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "SUCCEEDED"
assert get_response.json()["data"]["software_a_task_id"] is not None
assert get_response.json()["data"]["software_a_task_status"] == "SUCCEEDED"
assert get_response.json()["data"]["verification_result"]["http_ok"] is True
def test_edge_event_report_endpoint() -> None:
with TestClient(app) as client:
event_response = client.post(
"/api/agent/edge/events",
json={
"edge_id": "edge-shanghai-001",
"event_type": "AGENT_EXCEPTION",
"message": "executor timeout",
"detail": {"tool_name": "http_health_check", "timeout_ms": 3000},
},
)
assert event_response.status_code == 200
assert event_response.json()["data"]["accepted"] is True
assert event_response.json()["data"]["event_type"] == "AGENT_EXCEPTION"
def test_task_report_contains_traces() -> None:
with TestClient(app) as client:
client.post(
"/api/agent/edge/heartbeat",
json={
"edge_id": "edge-shanghai-001",
"hostname": "customer-host-01",
"os_type": "WINDOWS",
"agent_version": "0.1.0",
"capabilities": ["http_health_check"],
},
)
create_response = client.post(
"/api/agent/tasks",
headers={"X-Request-Id": "req-report-create-001"},
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-004",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
client.post(
f"/api/agent/tasks/{task_id}/confirm",
headers={"X-Request-Id": "req-report-confirm-001"},
json={"confirmed": True, "comment": "confirm"},
)
pull_response = client.post(
"/api/agent/edge/tasks/pull",
json={"edge_id": "edge-shanghai-001", "max_tasks": 5},
)
step = [item for item in pull_response.json()["data"]["tasks"] if item["task_id"] == task_id][0]
client.post(
"/api/agent/edge/tasks/report",
json={
"edge_id": "edge-shanghai-001",
"task_id": task_id,
"step_id": step["step_id"],
"tool_name": step["tool_name"],
"success": True,
"code": "OK",
"message": "200 OK",
"data": {"status_code": 200},
"evidence": {"response_body": "{\"status\":\"UP\"}"},
"started_at": "2026-04-08 20:20:00.000",
"finished_at": "2026-04-08 20:20:00.100",
},
)
report_response = client.get(f"/api/agent/tasks/{task_id}/report")
assert report_response.status_code == 200
payload = report_response.json()["data"]
assert payload["task_basic"]["task_id"] == task_id
assert len(payload["tool_trace"]) >= 2
assert len(payload["verification_trace"]) >= 1
assert len(payload["audit_trace"]) >= 3
assert payload["approval_trace"] == []
assert any(item["request_id"] == "req-report-confirm-001" for item in payload["tool_trace"])
assert any(item["operator_user_name"] == "alice" for item in payload["tool_trace"])
assert any(item["request_id"] == "req-report-create-001" for item in payload["audit_trace"])
assert payload["result_summary_detail"]["final_status"] == "SUCCEEDED"
assert payload["result_summary_detail"]["software_a"]["task_status"] == "SUCCEEDED"
assert payload["result_summary_detail"]["verification"]["success"] is True
deploy_trace = next(item for item in payload["tool_trace"] if item["tool_name"] == "software_a_deploy")
assert deploy_trace["duration_ms"] is not None
verification_trace = payload["verification_trace"][0]
assert verification_trace["duration_ms"] == 100
def test_cancel_running_task() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-005",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
cancel_response = client.post(
f"/api/agent/tasks/{task_id}/cancel",
json={"reason": "manual stop"},
)
assert cancel_response.status_code == 200
assert cancel_response.json()["data"]["task_status"] == "CANCELLED"
assert cancel_response.json()["data"]["software_a_task_status"] == "CANCELLED"
get_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "CANCELLED"
def test_confirm_twice_returns_conflict() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-006",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
first_confirm = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
assert first_confirm.status_code == 200
second_confirm = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm again"},
)
assert second_confirm.status_code == 409
assert second_confirm.json()["code"] == "CONFLICT"
def test_approval_decision_conflicts_after_task_cancelled() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to prod",
"channel": "WEB",
"session_id": "sess-007",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "need approval"},
)
approval_id = confirm_response.json()["data"]["approval_id"]
cancel_response = client.post(
f"/api/agent/tasks/{task_id}/cancel",
json={"reason": "manual stop before approval"},
)
assert cancel_response.status_code == 200
assert cancel_response.json()["data"]["task_status"] == "CANCELLED"
decision_response = client.post(
f"/api/demo/approval/requests/{approval_id}/decision",
json={
"decision": "APPROVED",
"comment": "approved too late",
"operator": {"user_id": "u2001", "user_name": "bob"},
},
)
assert decision_response.status_code == 409
assert decision_response.json()["code"] == "CONFLICT"
def test_duplicate_edge_report_returns_conflict() -> None:
with TestClient(app) as client:
client.post(
"/api/agent/edge/heartbeat",
json={
"edge_id": "edge-shanghai-001",
"hostname": "customer-host-01",
"os_type": "WINDOWS",
"agent_version": "0.1.0",
"capabilities": ["http_health_check"],
},
)
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-008",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
pull_response = client.post(
"/api/agent/edge/tasks/pull",
json={"edge_id": "edge-shanghai-001", "max_tasks": 5},
)
step = [item for item in pull_response.json()["data"]["tasks"] if item["task_id"] == task_id][0]
first_report = client.post(
"/api/agent/edge/tasks/report",
json={
"edge_id": "edge-shanghai-001",
"task_id": task_id,
"step_id": step["step_id"],
"tool_name": step["tool_name"],
"success": True,
"code": "OK",
"message": "200 OK",
"data": {"status_code": 200},
"evidence": {"response_body": "{\"status\":\"UP\"}"},
"started_at": "2026-04-08 20:20:00.000",
"finished_at": "2026-04-08 20:20:00.100",
},
)
assert first_report.status_code == 200
second_report = client.post(
"/api/agent/edge/tasks/report",
json={
"edge_id": "edge-shanghai-001",
"task_id": task_id,
"step_id": step["step_id"],
"tool_name": step["tool_name"],
"success": True,
"code": "OK",
"message": "200 OK",
"data": {"status_code": 200},
"evidence": {"response_body": "{\"status\":\"UP\"}"},
"started_at": "2026-04-08 20:20:00.000",
"finished_at": "2026-04-08 20:20:00.100",
},
)
assert second_report.status_code == 409
assert second_report.json()["code"] == "CONFLICT"
def test_task_fails_when_software_a_deploy_fails() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy fail-service 1.2.3-fail to test",
"channel": "WEB",
"session_id": "sess-009",
"tenant_id": "tenant-demo",
"context": {},
},
)
assert create_response.status_code == 200
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
assert confirm_response.status_code == 200
assert confirm_response.json()["data"]["task_status"] == "FAILED"
assert confirm_response.json()["data"]["software_a_task_status"] == "FAILED"
get_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "FAILED"
assert get_response.json()["data"]["software_a_task_status"] == "FAILED"
assert get_response.json()["data"]["result_summary_detail"]["final_status"] == "FAILED"
assert get_response.json()["data"]["result_summary_detail"]["software_a"]["error_detail"] is not None
pull_response = client.post(
"/api/agent/edge/tasks/pull",
json={"edge_id": "edge-shanghai-001", "max_tasks": 10},
)
assert pull_response.status_code == 200
assert all(item["task_id"] != task_id for item in pull_response.json()["data"]["tasks"])
def test_demo_software_a_endpoint_can_return_failed_task() -> None:
with TestClient(app) as client:
deploy_response = client.post(
"/api/demo/software-a/deploy-tasks",
json={
"operator": {"user_id": "u1001", "user_name": "alice"},
"tenant_id": "tenant-demo",
"app_code": "fail-service",
"env": "test",
"version": "1.2.3-fail",
"target_nodes": ["10.0.0.12"],
"deploy_options": {"graceful": True},
},
)
assert deploy_response.status_code == 200
assert deploy_response.json()["data"]["task_status"] == "FAILED"
software_a_task_id = deploy_response.json()["data"]["software_a_task_id"]
query_response = client.get(f"/api/demo/software-a/deploy-tasks/{software_a_task_id}")
assert query_response.status_code == 200
assert query_response.json()["data"]["task_status"] == "FAILED"
assert query_response.json()["data"]["error_detail"] is not None
def test_high_risk_task_can_be_rejected() -> None:
with TestClient(app) as client:
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to prod",
"channel": "WEB",
"session_id": "sess-010",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
confirm_response = client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "need approval"},
)
approval_id = confirm_response.json()["data"]["approval_id"]
decision_response = client.post(
f"/api/demo/approval/requests/{approval_id}/decision",
json={
"decision": "REJECTED",
"comment": "rejected",
"operator": {"user_id": "u2001", "user_name": "bob"},
},
)
assert decision_response.status_code == 200
assert decision_response.json()["data"]["approval_status"] == "REJECTED"
get_task_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_task_response.status_code == 200
assert get_task_response.json()["data"]["task_status"] == "CANCELLED"
assert get_task_response.json()["data"]["approval_status"] == "REJECTED"
def test_edge_failure_marks_task_failed() -> None:
with TestClient(app) as client:
client.post(
"/api/agent/edge/heartbeat",
json={
"edge_id": "edge-shanghai-001",
"hostname": "customer-host-01",
"os_type": "WINDOWS",
"agent_version": "0.1.0",
"capabilities": ["http_health_check"],
},
)
create_response = client.post(
"/api/agent/tasks",
json={
"input_text": "deploy order-service 1.2.3 to test",
"channel": "WEB",
"session_id": "sess-011",
"tenant_id": "tenant-demo",
"context": {},
},
)
task_id = create_response.json()["data"]["task_id"]
client.post(
f"/api/agent/tasks/{task_id}/confirm",
json={"confirmed": True, "comment": "confirm"},
)
pull_response = client.post(
"/api/agent/edge/tasks/pull",
json={"edge_id": "edge-shanghai-001", "max_tasks": 5},
)
step = [item for item in pull_response.json()["data"]["tasks"] if item["task_id"] == task_id][0]
report_response = client.post(
"/api/agent/edge/tasks/report",
json={
"edge_id": "edge-shanghai-001",
"task_id": task_id,
"step_id": step["step_id"],
"tool_name": step["tool_name"],
"success": False,
"code": "HTTP_500",
"message": "health check failed",
"data": {"status_code": 500},
"evidence": {"response_body": "{\"status\":\"DOWN\"}"},
"started_at": "2026-04-08 20:20:00.000",
"finished_at": "2026-04-08 20:20:00.100",
},
)
assert report_response.status_code == 200
assert report_response.json()["data"]["task_status"] == "FAILED"
get_response = client.get(f"/api/agent/tasks/{task_id}")
assert get_response.status_code == 200
assert get_response.json()["data"]["task_status"] == "FAILED"
assert get_response.json()["data"]["verification_result"]["http_ok"] is False
assert get_response.json()["data"]["result_summary_detail"]["verification"]["success"] is False
assert get_response.json()["data"]["result_summary_detail"]["final_reason"] == "health check failed"