Files

315 lines
11 KiB
Rust

use std::sync::Arc;
use crank_core::{
AgentId, AgentOperationBinding, AgentStatus, ApprovalRequest, ApprovalRequestId,
ApprovalRequestStatus, OperationApprovalRiskLevel, OperationId, PlatformApiKey,
PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope, PlatformApiKeyStatus, WorkspaceId,
};
use crank_registry::{
CreateAgentRequest, CreateApprovalRequest, CreatePlatformApiKeyRequest, DecideApprovalRequest,
FinishApprovalRequest,
};
use serde_json::{Value, json};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use super::common::{TestDatabase, test_agent, test_agent_version, test_operation};
fn timestamp(value: &str) -> OffsetDateTime {
OffsetDateTime::parse(value, &Rfc3339).unwrap()
}
fn approval_request(id: &str, payload: Value) -> ApprovalRequest {
ApprovalRequest {
id: ApprovalRequestId::new(id),
workspace_id: WorkspaceId::new("ws_default"),
agent_id: AgentId::new("agent_approval_atomic"),
operation_id: OperationId::new("operation_approval_atomic"),
operation_version: 1,
status: ApprovalRequestStatus::Pending,
risk_level: OperationApprovalRiskLevel::Dangerous,
request_id: None,
trace_id: None,
request_payload: payload,
response_payload: None,
created_at: timestamp("2026-03-25T12:01:00Z"),
expires_at: timestamp("2027-03-25T12:06:00Z"),
decided_at: None,
decided_by_key_id: None,
decision_note: None,
}
}
fn approval_key() -> PlatformApiKey {
PlatformApiKey {
id: PlatformApiKeyId::new("approval_atomic_key"),
workspace_id: WorkspaceId::new("ws_default"),
agent_id: Some(AgentId::new("agent_approval_atomic")),
key_kind: PlatformApiKeyKind::Approval,
name: "approval atomic key".to_owned(),
prefix: "crk_appr".to_owned(),
scopes: vec![PlatformApiKeyScope::Approve, PlatformApiKeyScope::Deny],
status: PlatformApiKeyStatus::Active,
created_at: timestamp("2026-03-25T12:00:00Z"),
last_used_at: None,
expires_at: None,
allowed_origins: Vec::new(),
}
}
async fn seed_approval_graph(registry: &crank_registry::PostgresRegistry) {
let workspace_id = WorkspaceId::new("ws_default");
let operation = test_operation(
"operation_approval_atomic",
1,
crank_core::OperationStatus::Published,
);
registry
.create_operation(&workspace_id, &operation, None)
.await
.unwrap();
let agent = test_agent("agent_approval_atomic", AgentStatus::Draft);
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
registry
.create_agent(CreateAgentRequest {
agent: &agent,
version: &version,
bindings: &[AgentOperationBinding {
operation_id: operation.id.clone(),
operation_version: 1,
tool_name: "dangerous_write".to_owned(),
tool_title: "Dangerous write".to_owned(),
tool_description_override: None,
enabled: true,
}],
})
.await
.unwrap();
let key = approval_key();
registry
.create_platform_api_key(CreatePlatformApiKeyRequest {
api_key: &key,
secret_hash: "approval-key-secret-hash",
})
.await
.unwrap();
}
#[tokio::test]
async fn concurrent_equal_scope_creates_one_pending_approval_without_losing_raw_execution_payload()
{
let database = TestDatabase::new().await;
let registry = Arc::new(database.registry().await);
seed_approval_graph(&registry).await;
let mut tasks = Vec::new();
for index in 0..32 {
let registry = Arc::clone(&registry);
tasks.push(tokio::spawn(async move {
let mut approval = approval_request(
&format!("approval_concurrent_{index}"),
json!({
"email": "customer@example.com",
"token": "SECRET_APPROVAL_CANARY",
"_crank_confirmation_token": format!("ct_{index}")
}),
);
approval.created_at += time::Duration::milliseconds(index);
registry
.create_approval_request(CreateApprovalRequest {
approval: &approval,
})
.await
.unwrap()
}));
}
let mut ids = std::collections::BTreeSet::new();
for task in tasks {
ids.insert(task.await.unwrap().approval.id.as_str().to_owned());
}
assert_eq!(
ids.len(),
1,
"all callers must receive one canonical pending approval"
);
let pending = registry
.list_pending_approval_requests_for_agent(
&WorkspaceId::new("ws_default"),
&AgentId::new("agent_approval_atomic"),
)
.await
.unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(
pending[0].approval.request_payload["token"], "SECRET_APPROVAL_CANARY",
"registry keeps the raw payload for the eventual approved execution"
);
assert!(
pending[0].approval.request_payload["_crank_confirmation_token"]
.as_str()
.is_some_and(|value| value.starts_with("ct_")),
"only projections may redact control data; execution input remains intact"
);
let safe_preview =
crank_core::sanitize_invocation_preview(&pending[0].approval.request_payload);
let preview = safe_preview.to_string();
assert!(!preview.contains("SECRET_APPROVAL_CANARY"));
database.cleanup().await;
}
#[tokio::test]
async fn nested_business_control_like_fields_are_part_of_approval_fingerprint() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
seed_approval_graph(&registry).await;
let first = approval_request(
"approval_nested_control_a",
json!({
"line": {
"_crank_approval_id": "business-a"
},
"_crank_confirmation_token": "transport-a"
}),
);
registry
.create_approval_request(CreateApprovalRequest { approval: &first })
.await
.unwrap();
let second = approval_request(
"approval_nested_control_b",
json!({
"line": {
"_crank_approval_id": "business-b"
},
"_crank_confirmation_token": "transport-b"
}),
);
let created_second = registry
.create_approval_request(CreateApprovalRequest { approval: &second })
.await
.unwrap();
assert_eq!(created_second.approval.id, second.id);
let pending = registry
.list_pending_approval_requests_for_agent(
&WorkspaceId::new("ws_default"),
&AgentId::new("agent_approval_atomic"),
)
.await
.unwrap();
assert_eq!(pending.len(), 2);
database.cleanup().await;
}
#[tokio::test]
async fn decision_claim_finish_and_replay_are_single_winner_transitions() {
let database = TestDatabase::new().await;
let registry = Arc::new(database.registry().await);
seed_approval_graph(&registry).await;
let approval = approval_request(
"approval_single_winner",
json!({"email":"lead@example.com"}),
);
let created = registry
.create_approval_request(CreateApprovalRequest {
approval: &approval,
})
.await
.unwrap();
assert_eq!(created.approval.status, ApprovalRequestStatus::Pending);
let mut decisions = Vec::new();
for index in 0..16 {
let registry = Arc::clone(&registry);
let operation_id = approval.operation_id.clone();
let operation_version = approval.operation_version;
let request_payload = approval.request_payload.clone();
decisions.push(tokio::spawn(async move {
let key_id = PlatformApiKeyId::new("approval_atomic_key");
registry
.decide_approval_request(DecideApprovalRequest {
workspace_id: &WorkspaceId::new("ws_default"),
agent_id: &AgentId::new("agent_approval_atomic"),
approval_id: &ApprovalRequestId::new("approval_single_winner"),
operation_id: &operation_id,
operation_version,
request_payload: &request_payload,
status: if index % 2 == 0 {
ApprovalRequestStatus::Approved
} else {
ApprovalRequestStatus::Denied
},
decided_at: timestamp("2026-03-25T12:02:00Z"),
decided_by_key_id: Some(&key_id),
response_payload: Some(json!({ "decision": index })),
decision_note: Some("race decision"),
})
.await
.unwrap()
}));
}
let mut winners = 0;
for decision in decisions {
if decision.await.unwrap().is_some() {
winners += 1;
}
}
assert_eq!(winners, 1);
let mut claims = Vec::new();
for _ in 0..16 {
let registry = Arc::clone(&registry);
claims.push(tokio::spawn(async move {
registry
.claim_approval_request(
&WorkspaceId::new("ws_default"),
&AgentId::new("agent_approval_atomic"),
&ApprovalRequestId::new("approval_single_winner"),
timestamp("2026-03-25T12:02:01Z"),
)
.await
.unwrap()
}));
}
let mut claim_winners = 0;
for claim in claims {
if claim.await.unwrap().is_some() {
claim_winners += 1;
}
}
assert_eq!(claim_winners, 1);
let first_finish = registry
.finish_approval_request(FinishApprovalRequest {
workspace_id: &WorkspaceId::new("ws_default"),
agent_id: &AgentId::new("agent_approval_atomic"),
approval_id: &ApprovalRequestId::new("approval_single_winner"),
status: ApprovalRequestStatus::Completed,
response_payload: Some(json!({"ok":true})),
decision_note: None,
})
.await
.unwrap();
assert!(first_finish.is_some());
let replay_finish = registry
.finish_approval_request(FinishApprovalRequest {
workspace_id: &WorkspaceId::new("ws_default"),
agent_id: &AgentId::new("agent_approval_atomic"),
approval_id: &ApprovalRequestId::new("approval_single_winner"),
status: ApprovalRequestStatus::Completed,
response_payload: Some(json!({"ok":"replay"})),
decision_note: None,
})
.await
.unwrap();
assert!(replay_finish.is_none());
database.cleanup().await;
}