705 lines
23 KiB
Rust
705 lines
23 KiB
Rust
#![allow(dead_code, unused_imports)]
|
|
|
|
use super::common::*;
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use crank_core::{
|
|
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
|
|
ApprovalRequest, ApprovalRequestId, ApprovalRequestStatus, AuthConfig, AuthKind, AuthProfile,
|
|
ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod,
|
|
InvocationLog, MembershipRole, OperationApprovalRiskLevel, OperationId, OperationSecurityLevel,
|
|
OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyKind, PlatformApiKeyScope,
|
|
PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId, Target,
|
|
ToolDescription, ToolExample, User, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
|
};
|
|
use crank_mapping::{MappingRule, MappingSet};
|
|
use crank_schema::{Schema, SchemaKind};
|
|
use serde_json::json;
|
|
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
|
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
|
|
|
use crank_registry::{
|
|
CreateAgentRequest, CreateApprovalRequest, CreateInvocationLogRequest,
|
|
CreatePlatformApiKeyRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
|
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
|
FinishApprovalRequest, ListApprovalRequestsQuery, OperationSampleMetadata,
|
|
PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest, PublishRequest, RegistryError,
|
|
RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
|
SaveSampleMetadataRequest, UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJobCompletion,
|
|
YamlImportJobId, YamlImportJobStatus,
|
|
};
|
|
|
|
fn test_workspace_id() -> WorkspaceId {
|
|
WorkspaceId::new("ws_default")
|
|
}
|
|
|
|
fn timestamp(value: &str) -> OffsetDateTime {
|
|
OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stores_and_finishes_yaml_import_jobs() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let job_id = YamlImportJobId::new("job_yaml_01");
|
|
let operation = test_operation("op_rest_04", 1, OperationStatus::Draft);
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
|
|
registry
|
|
.create_yaml_import_job(CreateYamlImportJobRequest {
|
|
id: &job_id,
|
|
source_sample_id: None,
|
|
format_version: "v1",
|
|
mode: ExportMode::Portable,
|
|
created_at: ×tamp("2026-03-25T12:00:00Z"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
registry
|
|
.finish_yaml_import_job(
|
|
&job_id,
|
|
&YamlImportJobCompletion {
|
|
status: YamlImportJobStatus::Completed,
|
|
result_operation_id: Some(operation.id.clone()),
|
|
result_version: Some(2),
|
|
error_text: None,
|
|
finished_at: timestamp("2026-03-25T12:05:00Z"),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let job = registry
|
|
.get_yaml_import_job(&job_id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(job.status, YamlImportJobStatus::Completed);
|
|
assert_eq!(job.result_version, Some(2));
|
|
assert_eq!(job.mode, ExportMode::Portable);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_workspace_read_paths() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let workspace = Workspace {
|
|
id: WorkspaceId::new("ws_extra_01"),
|
|
slug: "extra".to_owned(),
|
|
display_name: "Extra Workspace".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({"region":"eu"}),
|
|
created_at: timestamp("2026-03-25T12:00:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
|
};
|
|
let mut user = User {
|
|
id: UserId::new("user_extra_01"),
|
|
email: "owner@example.com".to_owned(),
|
|
display_name: "Owner".to_owned(),
|
|
status: crank_core::UserStatus::Active,
|
|
created_at: timestamp("2026-03-25T11:00:00Z"),
|
|
};
|
|
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let user_id = registry
|
|
.upsert_bootstrap_user(&user.email, &user.display_name, "hashed-password")
|
|
.await
|
|
.unwrap();
|
|
user.id = user_id;
|
|
registry
|
|
.ensure_membership(&workspace.id, &user.id, MembershipRole::Owner)
|
|
.await
|
|
.unwrap();
|
|
|
|
let all_workspaces = registry.list_workspaces().await.unwrap();
|
|
let user_workspaces = registry.list_workspaces_for_user(&user.id).await.unwrap();
|
|
let memberships = registry.list_memberships(&workspace.id).await.unwrap();
|
|
let loaded = registry
|
|
.get_workspace(&workspace.id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert!(
|
|
all_workspaces
|
|
.iter()
|
|
.any(|record| record.workspace == workspace)
|
|
);
|
|
assert_eq!(user_workspaces.len(), 1);
|
|
assert_eq!(user_workspaces[0].workspace, workspace);
|
|
assert_eq!(user_workspaces[0].role, MembershipRole::Owner);
|
|
assert_eq!(memberships.len(), 1);
|
|
assert_eq!(memberships[0].workspace_id, workspace.id);
|
|
assert_eq!(memberships[0].user.id, user.id);
|
|
assert_eq!(memberships[0].user.email, user.email);
|
|
assert_eq!(memberships[0].user.display_name, user.display_name);
|
|
assert_eq!(memberships[0].user.status, user.status);
|
|
assert_eq!(memberships[0].role, MembershipRole::Owner);
|
|
assert_eq!(loaded, WorkspaceRecord { workspace });
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn lists_default_workspace_first_for_community_sessions() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let default_workspace = Workspace {
|
|
id: WorkspaceId::new("ws_default"),
|
|
slug: "solo".to_owned(),
|
|
display_name: "Solo".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:00:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
|
};
|
|
let legacy_workspace = Workspace {
|
|
id: WorkspaceId::new("ws_growth_lab"),
|
|
slug: "growth-lab".to_owned(),
|
|
display_name: "Growth Lab".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:01:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:01:00Z"),
|
|
};
|
|
let user_id = registry
|
|
.upsert_bootstrap_user("community-owner@example.com", "Owner", "hashed-password")
|
|
.await
|
|
.unwrap();
|
|
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &legacy_workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.update_workspace(UpdateWorkspaceRequest {
|
|
workspace: &default_workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.ensure_membership(&legacy_workspace.id, &user_id, MembershipRole::Owner)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.ensure_membership(&default_workspace.id, &user_id, MembershipRole::Owner)
|
|
.await
|
|
.unwrap();
|
|
|
|
let workspaces = registry.list_workspaces_for_user(&user_id).await.unwrap();
|
|
|
|
assert_eq!(workspaces[0].workspace.id, WorkspaceId::new("ws_default"));
|
|
assert_eq!(workspaces[0].workspace.slug, "solo");
|
|
|
|
let session_id = UserSessionId::new("sess_workspace_default_first");
|
|
let secret_hash = "test-secret-hash";
|
|
registry
|
|
.create_user_session(
|
|
&session_id,
|
|
&user_id,
|
|
Some(&legacy_workspace.id),
|
|
secret_hash,
|
|
×tamp("2027-03-25T13:00:00Z"),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let session = registry
|
|
.get_user_session(&session_id, secret_hash)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
session.current_workspace_id,
|
|
Some(WorkspaceId::new("ws_default"))
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_user_profile_and_workspace_access_reads() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let workspace = Workspace {
|
|
id: WorkspaceId::new("ws_profile_01"),
|
|
slug: "profile".to_owned(),
|
|
display_name: "Profile Workspace".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:00:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
|
};
|
|
let other_workspace = Workspace {
|
|
id: WorkspaceId::new("ws_profile_02"),
|
|
slug: "profile-other".to_owned(),
|
|
display_name: "Other Workspace".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:01:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:01:00Z"),
|
|
};
|
|
let user_id = registry
|
|
.upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password")
|
|
.await
|
|
.unwrap();
|
|
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &other_workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
|
|
.await
|
|
.unwrap();
|
|
|
|
let updated = registry
|
|
.update_user_profile(&user_id, "updated@example.com", "Updated Owner")
|
|
.await
|
|
.unwrap();
|
|
let has_access = registry
|
|
.user_has_workspace_access(&user_id, &workspace.id)
|
|
.await
|
|
.unwrap();
|
|
let lacks_access = registry
|
|
.user_has_workspace_access(&user_id, &other_workspace.id)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(updated.id, user_id);
|
|
assert_eq!(updated.email, "updated@example.com");
|
|
assert_eq!(updated.display_name, "Updated Owner");
|
|
assert!(has_access);
|
|
assert!(!lacks_access);
|
|
assert!(updated.created_at.unix_timestamp() > 0);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn creates_and_loads_user_sessions_with_typed_expiration() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let workspace = Workspace {
|
|
id: WorkspaceId::new("ws_session_01"),
|
|
slug: "session".to_owned(),
|
|
display_name: "Session Workspace".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:00:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
|
};
|
|
let user_id = registry
|
|
.upsert_bootstrap_user("session@example.com", "Owner", "hashed-password")
|
|
.await
|
|
.unwrap();
|
|
let session_id = UserSessionId::new("sess_01");
|
|
let expires_at = timestamp("2030-04-06T12:05:00Z");
|
|
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_user_session(
|
|
&session_id,
|
|
&user_id,
|
|
Some(&workspace.id),
|
|
"secret-hash-01",
|
|
&expires_at,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let session = registry
|
|
.get_user_session(&session_id, "secret-hash-01")
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(session.session_id, session_id);
|
|
assert_eq!(session.current_workspace_id, Some(workspace.id.clone()));
|
|
assert_eq!(session.user.id, user_id);
|
|
assert!(session.user.created_at.unix_timestamp() > 0);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_platform_api_key_read_paths() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let workspace = Workspace {
|
|
id: WorkspaceId::new("ws_keys_01"),
|
|
slug: "keys".to_owned(),
|
|
display_name: "Keys Workspace".to_owned(),
|
|
status: crank_core::WorkspaceStatus::Active,
|
|
settings: json!({}),
|
|
created_at: timestamp("2026-03-25T12:00:00Z"),
|
|
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
|
};
|
|
let agent = test_agent("agent_keys_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let api_key = PlatformApiKey {
|
|
id: PlatformApiKeyId::new("key_01"),
|
|
workspace_id: workspace.id.clone(),
|
|
agent_id: Some(agent.id.clone()),
|
|
key_kind: PlatformApiKeyKind::McpClient,
|
|
name: "Primary".to_owned(),
|
|
prefix: "crk_live".to_owned(),
|
|
scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
|
|
status: PlatformApiKeyStatus::Active,
|
|
created_at: timestamp("2026-03-25T12:01:00Z"),
|
|
last_used_at: None,
|
|
expires_at: None,
|
|
allowed_origins: Vec::new(),
|
|
};
|
|
let secret_hash = "secret_hash_01";
|
|
|
|
registry
|
|
.create_workspace(CreateWorkspaceRequest {
|
|
workspace: &workspace,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: &[],
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_platform_api_key(CreatePlatformApiKeyRequest {
|
|
api_key: &api_key,
|
|
secret_hash,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let listed = registry
|
|
.list_platform_api_keys(&workspace.id)
|
|
.await
|
|
.unwrap();
|
|
let listed_for_agent = registry
|
|
.list_platform_api_keys_for_agent(&workspace.id, &agent.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
listed,
|
|
vec![PlatformApiKeyRecord {
|
|
api_key: api_key.clone()
|
|
}]
|
|
);
|
|
assert_eq!(
|
|
listed_for_agent,
|
|
vec![PlatformApiKeyRecord {
|
|
api_key: api_key.clone()
|
|
}]
|
|
);
|
|
|
|
let resolved = registry
|
|
.get_platform_api_key_by_secret_for_agent_slug(&workspace.slug, &agent.slug, secret_hash)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(resolved.api_key, api_key);
|
|
|
|
registry
|
|
.touch_platform_api_key(
|
|
&workspace.id,
|
|
&PlatformApiKeyId::new("key_01"),
|
|
×tamp("2026-03-25T12:05:00Z"),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let touched = registry
|
|
.list_platform_api_keys(&workspace.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
touched[0].api_key.last_used_at,
|
|
Some(timestamp("2026-03-25T12:05:00Z"))
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_approval_request_lifecycle() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let workspace_id = test_workspace_id();
|
|
let operation = test_operation("op_approval_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_approval_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let approval_key = PlatformApiKey {
|
|
id: PlatformApiKeyId::new("key_approval_01"),
|
|
workspace_id: workspace_id.clone(),
|
|
agent_id: Some(agent.id.clone()),
|
|
key_kind: PlatformApiKeyKind::Approval,
|
|
name: "Approval 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(),
|
|
};
|
|
let approval = ApprovalRequest {
|
|
id: ApprovalRequestId::new("approval_01"),
|
|
workspace_id: workspace_id.clone(),
|
|
agent_id: agent.id.clone(),
|
|
operation_id: operation.id.clone(),
|
|
operation_version: 1,
|
|
status: ApprovalRequestStatus::Pending,
|
|
risk_level: OperationApprovalRiskLevel::Dangerous,
|
|
request_payload: json!({"amount": 100}),
|
|
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,
|
|
};
|
|
|
|
registry
|
|
.create_operation(&workspace_id, &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: &[],
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_platform_api_key(CreatePlatformApiKeyRequest {
|
|
api_key: &approval_key,
|
|
secret_hash: "approval-secret-hash",
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let created = registry
|
|
.create_approval_request(CreateApprovalRequest {
|
|
approval: &approval,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(created.approval.id, approval.id);
|
|
let mut duplicate = approval.clone();
|
|
duplicate.id = ApprovalRequestId::new("approval_02");
|
|
duplicate.request_payload = json!({"amount": 100});
|
|
let deduplicated = registry
|
|
.create_approval_request(CreateApprovalRequest {
|
|
approval: &duplicate,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(deduplicated.approval.id, approval.id);
|
|
|
|
let pending = registry
|
|
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(pending.len(), 1);
|
|
assert_eq!(pending[0].approval, approval);
|
|
|
|
let workspace_approvals = registry
|
|
.list_approval_requests(ListApprovalRequestsQuery {
|
|
workspace_id: &workspace_id,
|
|
status: None,
|
|
limit: 10,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(workspace_approvals.len(), 1);
|
|
assert_eq!(workspace_approvals[0].approval.id, approval.id);
|
|
|
|
let fetched = registry
|
|
.get_approval_request(&workspace_id, &approval.id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(fetched.approval, approval);
|
|
|
|
let decided = registry
|
|
.decide_approval_request(DecideApprovalRequest {
|
|
workspace_id: &workspace_id,
|
|
agent_id: &agent.id,
|
|
approval_id: &approval.id,
|
|
status: ApprovalRequestStatus::Approved,
|
|
decided_at: timestamp("2026-03-25T12:02:00Z"),
|
|
decided_by_key_id: &approval_key.id,
|
|
response_payload: Some(json!({"approve": "yes"})),
|
|
decision_note: Some("confirmed in test"),
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(decided.approval.status, ApprovalRequestStatus::Approved);
|
|
assert_eq!(
|
|
decided.approval.decided_by_key_id,
|
|
Some(approval_key.id.clone())
|
|
);
|
|
assert_eq!(
|
|
decided.approval.response_payload,
|
|
Some(json!({"approve": "yes"}))
|
|
);
|
|
|
|
let approval_inside_recovery_grace = registry
|
|
.claim_next_recoverable_approval_request(
|
|
timestamp("2026-03-25T12:02:01Z"),
|
|
timestamp("2026-03-25T12:01:59Z"),
|
|
timestamp("2026-03-25T11:55:00Z"),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert!(approval_inside_recovery_grace.is_none());
|
|
|
|
let claimed = registry
|
|
.claim_approval_request(
|
|
&workspace_id,
|
|
&agent.id,
|
|
&approval.id,
|
|
timestamp("2026-03-25T12:02:01Z"),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(claimed.approval.status, ApprovalRequestStatus::Executing);
|
|
let fresh_claim = registry
|
|
.claim_next_recoverable_approval_request(
|
|
timestamp("2026-03-25T12:02:10Z"),
|
|
timestamp("2026-03-25T12:02:09Z"),
|
|
timestamp("2026-03-25T12:01:59Z"),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert!(fresh_claim.is_none());
|
|
let recovered = registry
|
|
.claim_next_recoverable_approval_request(
|
|
timestamp("2026-03-25T12:03:00Z"),
|
|
timestamp("2026-03-25T12:02:59Z"),
|
|
timestamp("2026-03-25T12:02:30Z"),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(recovered.approval.id, approval.id);
|
|
assert_eq!(recovered.approval.status, ApprovalRequestStatus::Executing);
|
|
|
|
let completed = registry
|
|
.finish_approval_request(FinishApprovalRequest {
|
|
workspace_id: &workspace_id,
|
|
agent_id: &agent.id,
|
|
approval_id: &approval.id,
|
|
status: ApprovalRequestStatus::Completed,
|
|
response_payload: Some(json!({"id": "lead_123"})),
|
|
decision_note: None,
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(completed.approval.status, ApprovalRequestStatus::Completed);
|
|
assert_eq!(
|
|
completed.approval.response_payload,
|
|
Some(json!({"id": "lead_123"}))
|
|
);
|
|
|
|
let completed_by_status = registry
|
|
.list_approval_requests(ListApprovalRequestsQuery {
|
|
workspace_id: &workspace_id,
|
|
status: Some(ApprovalRequestStatus::Completed),
|
|
limit: 10,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(completed_by_status.len(), 1);
|
|
assert_eq!(
|
|
completed_by_status[0].approval.status,
|
|
ApprovalRequestStatus::Completed
|
|
);
|
|
|
|
let pending_after_decision = registry
|
|
.list_pending_approval_requests_for_agent(&workspace_id, &agent.id)
|
|
.await
|
|
.unwrap();
|
|
assert!(pending_after_decision.is_empty());
|
|
|
|
let repeated_decision = registry
|
|
.decide_approval_request(DecideApprovalRequest {
|
|
workspace_id: &workspace_id,
|
|
agent_id: &agent.id,
|
|
approval_id: &approval.id,
|
|
status: ApprovalRequestStatus::Denied,
|
|
decided_at: timestamp("2026-03-25T12:03:00Z"),
|
|
decided_by_key_id: &approval_key.id,
|
|
response_payload: Some(json!({"approve": "no"})),
|
|
decision_note: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert!(repeated_decision.is_none());
|
|
|
|
let mut expired = approval.clone();
|
|
expired.id = ApprovalRequestId::new("approval_expired_pending");
|
|
expired.request_payload = json!({"amount": 200});
|
|
expired.created_at = timestamp("2026-03-25T12:04:00Z");
|
|
expired.expires_at = timestamp("2026-03-25T12:04:01Z");
|
|
registry
|
|
.create_approval_request(CreateApprovalRequest { approval: &expired })
|
|
.await
|
|
.unwrap();
|
|
let mut replacement = expired.clone();
|
|
replacement.id = ApprovalRequestId::new("approval_replacement_pending");
|
|
replacement.created_at = timestamp("2026-03-25T12:05:00Z");
|
|
replacement.expires_at = timestamp("2026-03-25T12:10:00Z");
|
|
let replaced = registry
|
|
.create_approval_request(CreateApprovalRequest {
|
|
approval: &replacement,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(replaced.approval.id, replacement.id);
|
|
|
|
database.cleanup().await;
|
|
}
|