feat: complete Epic 1 production foundation
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
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(®istry).await;
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for index in 0..32 {
|
||||
let registry = Arc::clone(®istry);
|
||||
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(®istry).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(®istry).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(®istry);
|
||||
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(®istry);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user