tandem-server 0.6.0

HTTP server for Tandem engine APIs
use super::*;

fn tenant_agent_create_request(
    automation_id: &str,
    org_id: &str,
    workspace_id: &str,
    actor_id: &str,
    agent_id: &str,
) -> Request<Body> {
    Request::builder()
        .method("POST")
        .uri("/automations/v2")
        .header("content-type", "application/json")
        .header("x-tandem-org-id", org_id)
        .header("x-tandem-workspace-id", workspace_id)
        .header("x-tandem-actor-id", actor_id)
        .header("x-tandem-request-source", "agent")
        .header("x-tandem-agent-id", agent_id)
        .body(Body::from(
            json!({
                "automation_id": automation_id,
                "name": format!("{automation_id} automation"),
                "status": "draft",
                "schedule": {
                    "type": "manual",
                    "timezone": "UTC",
                    "misfire_policy": { "type": "skip" }
                },
                "agents": [{
                    "agent_id": agent_id,
                    "display_name": "Agent Shared",
                    "skills": [],
                    "tool_policy": { "allowlist": ["read"], "denylist": [] },
                    "mcp_policy": { "allowed_servers": [] }
                }],
                "flow": {
                    "nodes": [{
                        "node_id": "node-1",
                        "agent_id": agent_id,
                        "objective": "Exercise tenant-scoped spend isolation",
                        "depends_on": []
                    }]
                },
                "execution": { "max_parallel_agents": 1 }
            })
            .to_string(),
        ))
        .expect("tenant agent create request")
}

async fn response_json(response: axum::response::Response) -> Value {
    let body = to_bytes(response.into_body(), usize::MAX)
        .await
        .expect("response body");
    serde_json::from_slice(&body).expect("response json")
}

#[tokio::test]
async fn tenant_spend_pause_does_not_cross_to_same_named_agent() {
    let state = test_state().await;
    {
        let mut guard = state.automation_governance.write().await;
        guard.limits.weekly_spend_cap_usd = Some(10.0);
        guard.limits.spend_warning_threshold_ratio = 0.8;
    }
    let app = app_router(state.clone());
    let agent_id = "agent-shared";

    let tenant_a_create = app
        .clone()
        .oneshot(tenant_agent_create_request(
            "tenant-a-spend-paused",
            "org-a",
            "workspace-a",
            "user-a",
            agent_id,
        ))
        .await
        .expect("tenant a create response");
    assert_eq!(tenant_a_create.status(), StatusCode::OK);
    let automation_a = state
        .get_automation_v2("tenant-a-spend-paused")
        .await
        .expect("tenant a automation");
    let tenant_a = automation_a.tenant_context();
    let run_a = state
        .create_automation_v2_run(&automation_a, "manual")
        .await
        .expect("tenant a run");

    state
        .record_automation_v2_spend(&run_a.run_id, 6_000, 6_000, 12_000, 12.0)
        .await
        .expect("tenant a cap spend");
    assert!(state
        .tenant_agent_spend_summary(&tenant_a, agent_id)
        .await
        .expect("tenant a spend summary")
        .paused_at_ms
        .is_some());
    {
        let mut guard = state.automation_governance.write().await;
        guard.spend_paused_agents.push(agent_id.to_string());
    }

    let tenant_a_retry = app
        .clone()
        .oneshot(tenant_agent_create_request(
            "tenant-a-spend-paused-retry",
            "org-a",
            "workspace-a",
            "user-a",
            agent_id,
        ))
        .await
        .expect("tenant a retry response");
    assert_eq!(tenant_a_retry.status(), StatusCode::TOO_MANY_REQUESTS);
    let retry_payload = response_json(tenant_a_retry).await;
    assert_eq!(
        retry_payload.get("code").and_then(Value::as_str),
        Some("AUTOMATION_V2_AGENT_SPEND_CAP_EXCEEDED")
    );

    let tenant_b_create = app
        .clone()
        .oneshot(tenant_agent_create_request(
            "tenant-b-same-agent",
            "org-b",
            "workspace-b",
            "user-b",
            agent_id,
        ))
        .await
        .expect("tenant b create response");
    assert_eq!(tenant_b_create.status(), StatusCode::OK);
    let automation_b = state
        .get_automation_v2("tenant-b-same-agent")
        .await
        .expect("tenant b automation");
    assert!(state
        .tenant_agent_spend_summary(&automation_b.tenant_context(), agent_id)
        .await
        .is_none());

    let run_b = state
        .create_automation_v2_run(&automation_b, "manual")
        .await
        .expect("tenant b run");
    let claimed_b = state.claim_specific_automation_v2_run(&run_b.run_id).await;
    assert!(
        claimed_b.is_some(),
        "tenant b run must not inherit tenant a cap"
    );
}