tandem-server 0.6.5

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

use crate::app::state::governance::UnavailableGovernanceEngine;
use tandem_enterprise_contract::{
    DataClass, EnterprisePolicyEffect, EnterprisePolicyRule, EnterprisePolicyScopeLevel,
};
use tandem_types::{PolicyDecisionEffect, PolicyDecisionRecord, TenantContext};

fn tenant(org_id: &str, workspace_id: &str, actor_id: &str) -> TenantContext {
    TenantContext::explicit_user_workspace(org_id, workspace_id, None, actor_id)
}

fn policy_decision(
    decision_id: &str,
    tenant_context: TenantContext,
    run_id: &str,
    created_at_ms: u64,
) -> PolicyDecisionRecord {
    PolicyDecisionRecord {
        decision_id: decision_id.to_string(),
        tenant_context,
        actor_id: Some("agent-policy-test".to_string()),
        session_id: Some(format!("session-{decision_id}")),
        message_id: Some(format!("message-{decision_id}")),
        run_id: Some(run_id.to_string()),
        automation_id: Some("automation-policy-test".to_string()),
        node_id: None,
        tool: Some("mcp.bank.release_funds".to_string()),
        resource: None,
        data_classes: Vec::new(),
        risk_tier: Some("money_movement".to_string()),
        decision: PolicyDecisionEffect::ApprovalRequired,
        reason_code: "approval_required_unverified".to_string(),
        reason: "approval required".to_string(),
        policy_id: Some("fintech_strict".to_string()),
        grant_id: None,
        approval_id: None,
        audit_event_id: None,
        created_at_ms,
        metadata: json!({}),
    }
}

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 policy_decisions_route_filters_tenant_before_limit() {
    let state = test_state().await;
    let tenant_a = tenant("org-a", "workspace-a", "user-a");
    let tenant_b = tenant("org-b", "workspace-b", "user-b");
    state
        .record_policy_decision(policy_decision(
            "decision-a-older",
            tenant_a.clone(),
            "run-shared",
            100,
        ))
        .await
        .expect("record tenant a decision");
    state
        .record_policy_decision(policy_decision(
            "decision-b-newer",
            tenant_b,
            "run-shared",
            300,
        ))
        .await
        .expect("record tenant b decision");

    let response = app_router(state)
        .oneshot(
            Request::builder()
                .method("GET")
                .uri("/governance/policy-decisions?limit=1")
                .header("x-tandem-org-id", tenant_a.org_id.as_str())
                .header("x-tandem-workspace-id", tenant_a.workspace_id.as_str())
                .header("x-tandem-actor-id", "user-a")
                .body(Body::empty())
                .expect("policy decisions list request"),
        )
        .await
        .expect("policy decisions list response");

    assert_eq!(response.status(), StatusCode::OK);
    let payload = response_json(response).await;
    assert_eq!(payload["count"], json!(1));
    assert_eq!(
        payload["policy_decisions"][0]["decision_id"],
        json!("decision-a-older")
    );
}

#[tokio::test]
async fn policy_decisions_route_filters_run_and_tenant_before_limit() {
    let state = test_state().await;
    let tenant_a = tenant("org-a", "workspace-a", "user-a");
    let tenant_b = tenant("org-b", "workspace-b", "user-b");
    state
        .record_policy_decision(policy_decision(
            "decision-a-target-run",
            tenant_a.clone(),
            "run-target",
            100,
        ))
        .await
        .expect("record tenant a decision");
    state
        .record_policy_decision(policy_decision(
            "decision-b-target-run-newer",
            tenant_b,
            "run-target",
            300,
        ))
        .await
        .expect("record tenant b decision");

    let response = app_router(state)
        .oneshot(
            Request::builder()
                .method("GET")
                .uri("/governance/policy-decisions?run_id=run-target&limit=1")
                .header("x-tandem-org-id", tenant_a.org_id.as_str())
                .header("x-tandem-workspace-id", tenant_a.workspace_id.as_str())
                .header("x-tandem-actor-id", "user-a")
                .body(Body::empty())
                .expect("policy decisions run list request"),
        )
        .await
        .expect("policy decisions run list response");

    assert_eq!(response.status(), StatusCode::OK);
    let payload = response_json(response).await;
    assert_eq!(payload["count"], json!(1));
    assert_eq!(
        payload["policy_decisions"][0]["decision_id"],
        json!("decision-a-target-run")
    );
}

#[tokio::test]
async fn policy_decision_records_use_enterprise_policy_resolver() {
    let state = test_state().await;
    let tenant_a = tenant("org-a", "workspace-a", "user-a");
    state.enterprise.policy_rules.write().await.insert(
        "enterprise-finance-deny".to_string(),
        EnterprisePolicyRule::new(
            "enterprise-finance-deny",
            "enterprise-finance-floor",
            EnterprisePolicyScopeLevel::Enterprise,
            EnterprisePolicyEffect::Deny,
        )
        .with_tenant_context(tenant_a.clone())
        .with_tool_patterns(vec!["mcp.bank.*".to_string()])
        .with_reason(
            "enterprise_finance_floor",
            "enterprise policy denies finance tool access",
        ),
    );

    let mut decision = policy_decision("decision-local-allow", tenant_a.clone(), "run-target", 100);
    decision.decision = PolicyDecisionEffect::Allow;
    decision.reason_code = "local_policy_allow".to_string();
    decision.reason = "local tool authority allowed the request".to_string();

    let recorded = state
        .record_policy_decision(decision)
        .await
        .expect("record policy decision");

    assert_eq!(recorded.decision, PolicyDecisionEffect::Deny);
    assert_eq!(
        recorded.policy_id.as_deref(),
        Some("enterprise-finance-floor")
    );
    assert_eq!(recorded.reason_code, "enterprise_finance_floor");
    let snapshot = recorded
        .effective_policy_snapshot()
        .expect("effective policy snapshot");
    assert_eq!(snapshot.effect, EnterprisePolicyEffect::Deny);
    assert_eq!(snapshot.inherited_sources.len(), 2);
    assert_eq!(
        snapshot
            .decision_source
            .as_ref()
            .map(|source| source.rule_id.as_str()),
        Some("enterprise-finance-deny")
    );
    assert!(
        snapshot
            .inherited_sources
            .iter()
            .any(|source| source.rule_id == "fintech_strict:decision-local-allow"),
        "runtime fallback source should be retained for replay"
    );
}

#[tokio::test]
async fn policy_decision_resolver_matches_every_recorded_data_class() {
    let state = test_state().await;
    let tenant_a = tenant("org-a", "workspace-a", "user-a");
    state.enterprise.policy_rules.write().await.insert(
        "enterprise-financial-record-deny".to_string(),
        EnterprisePolicyRule::new(
            "enterprise-financial-record-deny",
            "enterprise-data-floor",
            EnterprisePolicyScopeLevel::Enterprise,
            EnterprisePolicyEffect::Deny,
        )
        .with_tenant_context(tenant_a.clone())
        .with_data_classes(vec![DataClass::FinancialRecord])
        .with_reason(
            "enterprise_financial_record_floor",
            "enterprise policy denies financial record access",
        ),
    );

    let mut decision = policy_decision("decision-multi-class", tenant_a, "run-target", 100);
    decision.decision = PolicyDecisionEffect::Allow;
    decision.data_classes = vec![DataClass::CustomerData, DataClass::FinancialRecord];
    decision.reason_code = "local_policy_allow".to_string();
    decision.reason = "local tool authority allowed the request".to_string();

    let recorded = state
        .record_policy_decision(decision)
        .await
        .expect("record policy decision");

    assert_eq!(recorded.decision, PolicyDecisionEffect::Deny);
    assert_eq!(recorded.reason_code, "enterprise_financial_record_floor");
    let snapshot = recorded
        .effective_policy_snapshot()
        .expect("effective policy snapshot");
    assert_eq!(
        snapshot
            .decision_source
            .as_ref()
            .map(|source| source.rule_id.as_str()),
        Some("enterprise-financial-record-deny")
    );
}

#[tokio::test]
async fn policy_decisions_route_requires_premium_governance() {
    let mut state = test_state().await;
    state.governance_engine = Arc::new(UnavailableGovernanceEngine);

    let response = app_router(state)
        .oneshot(
            Request::builder()
                .method("GET")
                .uri("/governance/policy-decisions")
                .body(Body::empty())
                .expect("policy decisions list request"),
        )
        .await
        .expect("policy decisions list response");

    assert_eq!(response.status(), StatusCode::NOT_IMPLEMENTED);
    let payload = response_json(response).await;
    assert_eq!(
        payload.get("code").and_then(Value::as_str),
        Some("PREMIUM_FEATURE_REQUIRED")
    );
}