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" 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"]) 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" 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