851 lines
28 KiB
Rust
851 lines
28 KiB
Rust
#![allow(dead_code, unused_imports)]
|
|
|
|
use super::common::*;
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use crank_core::{
|
|
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig, AuthConfig,
|
|
AuthKind, AuthProfile, ConfigExport, ExecutionConfig, ExportMode, GeneratedDraft,
|
|
GeneratedDraftStatus, HttpMethod, InvocationLog, MembershipRole, OperationId,
|
|
OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId, 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, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
|
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
|
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
|
|
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
|
|
SaveAgentCatalogConfigRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
|
SaveSampleMetadataRequest, UsageBucket, UsageOutcomeGroup, UsageQuery, 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 manages_agent_read_paths() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let bindings = vec![AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation.version,
|
|
tool_name: "create_lead".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: Some("Creates CRM lead".to_owned()),
|
|
enabled: true,
|
|
}];
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: &bindings,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let listed = registry.list_agents(&test_workspace_id()).await.unwrap();
|
|
let summary = registry
|
|
.get_agent_summary(&test_workspace_id(), &agent.id)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let loaded_version = registry
|
|
.get_agent_version(&test_workspace_id(), &agent.id, version.version)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(listed, vec![summary.clone()]);
|
|
assert_eq!(summary.id, agent.id);
|
|
assert_eq!(summary.slug, agent.slug);
|
|
assert_eq!(summary.display_name, agent.display_name);
|
|
assert_eq!(summary.status, agent.status);
|
|
assert_eq!(loaded_version.agent_id, agent.id);
|
|
assert_eq!(loaded_version.version, version.version);
|
|
assert_eq!(loaded_version.status, version.status);
|
|
assert_eq!(loaded_version.snapshot, version);
|
|
assert_eq!(loaded_version.bindings, bindings);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_published_agent_tool_reads() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_pub_01", 1, OperationStatus::Draft);
|
|
let operation_v2 = test_operation("op_agent_pub_01", 2, OperationStatus::Draft);
|
|
let agent = test_agent("agent_pub_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let bindings = vec![AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation_v2.version,
|
|
tool_name: "create_lead".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: Some("Creates CRM lead".to_owned()),
|
|
enabled: true,
|
|
}];
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_version(CreateVersionRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
snapshot: &operation_v2,
|
|
change_note: Some("publishable"),
|
|
created_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation_v2.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: &bindings,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let tools = registry
|
|
.get_published_agent_tools_by_slug("default", &agent.slug)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(tools.len(), 1);
|
|
assert_eq!(tools[0].workspace_id, test_workspace_id());
|
|
assert_eq!(tools[0].workspace_slug, "default");
|
|
assert_eq!(tools[0].agent_id, agent.id);
|
|
assert_eq!(tools[0].agent_slug, agent.slug);
|
|
assert_eq!(tools[0].tool_name, bindings[0].tool_name);
|
|
assert_eq!(tools[0].tool_title, bindings[0].tool_title);
|
|
assert_eq!(tools[0].tool_description, "Creates CRM lead");
|
|
assert_eq!(tools[0].operation.id, operation_v2.id);
|
|
assert_eq!(tools[0].operation.version, operation_v2.version);
|
|
assert_eq!(tools[0].operation.protocol, operation_v2.protocol);
|
|
assert!(tools[0].operation.is_published());
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn published_agent_snapshot_remains_immutable_after_draft_edit() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_immutable_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_immutable_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let published_binding = AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation.version,
|
|
tool_name: "create_lead_immutable".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: Some("Published immutable binding".to_owned()),
|
|
enabled: true,
|
|
};
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: std::slice::from_ref(&published_binding),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let before = registry
|
|
.get_published_agent_tools_by_slug("default", &agent.slug)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(before.len(), 1);
|
|
assert_eq!(before[0].tool_name, published_binding.tool_name);
|
|
|
|
registry
|
|
.save_agent_catalog_config(SaveAgentCatalogConfigRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
agent_version: version.version,
|
|
bindings: &[],
|
|
tool_selection_policy: &Default::default(),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.expect_err("editing after publish must not mutate the published Agent Version");
|
|
|
|
let after = registry
|
|
.get_published_agent_tools_by_slug("default", &agent.slug)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(after, before);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stale_agent_revision_rejects_mutation() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_stale_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_stale_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let binding = AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation.version,
|
|
tool_name: "create_lead_stale".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: None,
|
|
enabled: true,
|
|
};
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: std::slice::from_ref(&binding),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let stale_save = registry
|
|
.save_agent_catalog_config(SaveAgentCatalogConfigRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
agent_version: version.version,
|
|
bindings: &[],
|
|
tool_selection_policy: &Default::default(),
|
|
expected_state: None,
|
|
})
|
|
.await;
|
|
|
|
assert!(
|
|
stale_save.is_err(),
|
|
"stale write against already-published Agent Version must be rejected"
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn published_agent_catalog_revision_is_durable_and_monotonic() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_revision_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_revision_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let binding = AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation.version,
|
|
tool_name: "create_lead_revision".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: None,
|
|
enabled: true,
|
|
};
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: std::slice::from_ref(&binding),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let first = registry
|
|
.get_published_agent_catalog_by_slug("default", &agent.slug)
|
|
.await
|
|
.unwrap()
|
|
.catalog_revision;
|
|
|
|
registry
|
|
.unpublish_agent(
|
|
&test_workspace_id(),
|
|
&agent.id,
|
|
×tamp("2026-03-25T12:12:00Z"),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:13:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let second = registry
|
|
.get_published_agent_catalog_by_slug("default", &agent.slug)
|
|
.await
|
|
.unwrap()
|
|
.catalog_revision;
|
|
|
|
assert_ne!(
|
|
first, second,
|
|
"unpublish/re-publish of the same Agent Version must invalidate stale catalog search results"
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn database_rejects_direct_published_agent_snapshot_mutation() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_agent_db_guard_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_db_guard_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let binding = AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation.version,
|
|
tool_name: "create_lead_db_guard".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: None,
|
|
enabled: true,
|
|
};
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: std::slice::from_ref(&binding),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let policy_update = sqlx::query(
|
|
"update agent_versions
|
|
set tool_selection_policy_json = '{\"changed\":true}'::jsonb
|
|
where agent_id = $1 and version = 1",
|
|
)
|
|
.bind(agent.id.as_str())
|
|
.execute(registry.pool())
|
|
.await;
|
|
assert!(
|
|
policy_update.is_err(),
|
|
"database trigger must reject direct Published Agent Version mutation"
|
|
);
|
|
|
|
let binding_delete = sqlx::query(
|
|
"delete from agent_operation_bindings
|
|
where agent_id = $1 and agent_version = 1",
|
|
)
|
|
.bind(agent.id.as_str())
|
|
.execute(registry.pool())
|
|
.await;
|
|
assert!(
|
|
binding_delete.is_err(),
|
|
"database trigger must reject direct Published Agent binding deletion"
|
|
);
|
|
|
|
let pointer_rewind = sqlx::query(
|
|
"update published_agents
|
|
set catalog_revision = catalog_revision
|
|
where agent_id = $1",
|
|
)
|
|
.bind(agent.id.as_str())
|
|
.execute(registry.pool())
|
|
.await;
|
|
assert!(
|
|
pointer_rewind.is_err(),
|
|
"database trigger must reject non-increasing catalog revision"
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn manages_operation_usage_and_agent_ref_reads() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_usage_01", 1, OperationStatus::Draft);
|
|
let operation_v2 = test_operation("op_usage_01", 2, OperationStatus::Draft);
|
|
let agent = test_agent("agent_usage_01", AgentStatus::Draft);
|
|
let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
|
|
let bindings = vec![AgentOperationBinding {
|
|
operation_id: operation.id.clone(),
|
|
operation_version: operation_v2.version,
|
|
tool_name: "create_lead".to_owned(),
|
|
tool_title: "Create lead".to_owned(),
|
|
tool_description_override: None,
|
|
enabled: true,
|
|
}];
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_version(CreateVersionRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
snapshot: &operation_v2,
|
|
change_note: Some("publishable"),
|
|
created_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_operation(PublishRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
operation_id: &operation.id,
|
|
version: operation_v2.version,
|
|
published_at: ×tamp("2026-03-25T12:10:00Z"),
|
|
published_by: Some("alice"),
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &version,
|
|
bindings: &bindings,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.publish_agent(PublishAgentRequest {
|
|
workspace_id: &test_workspace_id(),
|
|
agent_id: &agent.id,
|
|
version: version.version,
|
|
published_at: ×tamp("2026-03-25T12:11:00Z"),
|
|
published_by: Some("alice"),
|
|
expected_state: None,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
registry
|
|
.create_invocation_log(CreateInvocationLogRequest {
|
|
log: &test_invocation_log(
|
|
"log_usage_ok",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Ok,
|
|
120,
|
|
"2026-03-25T12:20:00Z",
|
|
),
|
|
})
|
|
.await,
|
|
crank_registry::InvocationHistoryWriteOutcome::Recorded
|
|
);
|
|
let stored = registry
|
|
.get_invocation_log(
|
|
&test_workspace_id(),
|
|
&crank_core::InvocationLogId::new("log_usage_ok"),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.expect("typed invocation history");
|
|
assert_eq!(stored.log.operation_version, Some(1));
|
|
assert_eq!(
|
|
stored.log.execution_stage,
|
|
Some(crank_core::ExecutionStage::Runtime)
|
|
);
|
|
assert_eq!(
|
|
stored.log.retryability,
|
|
Some(crank_core::Retryability::Never)
|
|
);
|
|
assert_eq!(
|
|
stored.log.outcome_certainty,
|
|
Some(crank_core::OutcomeCertainty::Certain)
|
|
);
|
|
assert_eq!(
|
|
registry
|
|
.create_invocation_log(CreateInvocationLogRequest {
|
|
log: &test_invocation_log(
|
|
"log_usage_err",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Error,
|
|
240,
|
|
"2026-03-25T12:21:00Z",
|
|
),
|
|
})
|
|
.await,
|
|
crank_registry::InvocationHistoryWriteOutcome::Recorded
|
|
);
|
|
|
|
let has_bindings = registry
|
|
.has_published_agent_bindings_for_operation(&test_workspace_id(), &operation.id)
|
|
.await
|
|
.unwrap();
|
|
let agent_refs = registry
|
|
.list_operation_agent_refs(&test_workspace_id())
|
|
.await
|
|
.unwrap();
|
|
let usage = registry
|
|
.list_operation_usage_summaries(&test_workspace_id(), "2026-03-25T12:00:00Z")
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(has_bindings);
|
|
assert_eq!(agent_refs.len(), 1);
|
|
assert_eq!(agent_refs[0].operation_id, operation.id);
|
|
assert_eq!(agent_refs[0].agent_id, agent.id);
|
|
assert_eq!(agent_refs[0].agent_slug, agent.slug);
|
|
assert_eq!(agent_refs[0].display_name, agent.display_name);
|
|
assert_eq!(usage.len(), 1);
|
|
assert_eq!(usage[0].operation_id, operation.id);
|
|
assert_eq!(usage[0].calls_today, 2);
|
|
assert_eq!(usage[0].error_rate_pct, 50.0);
|
|
assert_eq!(usage[0].avg_latency_ms, 180);
|
|
|
|
database.cleanup().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn invocation_history_filters_cursor_and_usage_outcomes_are_bounded() {
|
|
let database = TestDatabase::new().await;
|
|
let registry = database.registry().await;
|
|
let operation = test_operation("op_history_01", 1, OperationStatus::Draft);
|
|
let agent = test_agent("agent_history_01", AgentStatus::Draft);
|
|
|
|
registry
|
|
.create_operation(&test_workspace_id(), &operation, None)
|
|
.await
|
|
.unwrap();
|
|
registry
|
|
.create_agent(CreateAgentRequest {
|
|
agent: &agent,
|
|
version: &test_agent_version(&agent.id, 1, AgentStatus::Draft),
|
|
bindings: &[],
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut success = test_invocation_log(
|
|
"log_history_success",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Ok,
|
|
50,
|
|
"2026-03-25T12:00:00Z",
|
|
);
|
|
success.execution_error_code = None;
|
|
success.message = "=formula must remain data".to_owned();
|
|
let mut upstream = test_invocation_log(
|
|
"log_history_upstream",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Error,
|
|
150,
|
|
"2026-03-25T12:01:00Z",
|
|
);
|
|
upstream.execution_error_code = Some(crank_core::ExecutionErrorCode::UpstreamTimeout);
|
|
let mut client = test_invocation_log(
|
|
"log_history_client",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Error,
|
|
250,
|
|
"2026-03-25T12:02:00Z",
|
|
);
|
|
client.execution_error_code = Some(crank_core::ExecutionErrorCode::InputSchemaInvalid);
|
|
let mut schema = test_invocation_log(
|
|
"log_history_schema",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Error,
|
|
350,
|
|
"2026-03-25T12:03:00Z",
|
|
);
|
|
schema.execution_error_code = Some(crank_core::ExecutionErrorCode::OutputSchemaInvalid);
|
|
let mut crank = test_invocation_log(
|
|
"log_history_crank",
|
|
&operation.id,
|
|
Some(agent.id.clone()),
|
|
crank_core::InvocationStatus::Error,
|
|
450,
|
|
"2026-03-25T12:04:00Z",
|
|
);
|
|
crank.execution_error_code = Some(crank_core::ExecutionErrorCode::RuntimeInternal);
|
|
|
|
for log in [&success, &upstream, &client, &schema, &crank] {
|
|
assert_eq!(
|
|
registry
|
|
.create_invocation_log(CreateInvocationLogRequest { log })
|
|
.await,
|
|
crank_registry::InvocationHistoryWriteOutcome::Recorded
|
|
);
|
|
}
|
|
|
|
let first_page = registry
|
|
.list_invocation_logs(crank_registry::ListInvocationLogsQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
level: None,
|
|
status: Some(crank_core::InvocationStatus::Error),
|
|
outcome_group: None,
|
|
search_text: None,
|
|
source: None,
|
|
operation_id: None,
|
|
agent_id: None,
|
|
created_after: Some("2026-03-25T12:00:00Z"),
|
|
created_before: Some("2026-03-25T12:04:00Z"),
|
|
cursor_created_at: None,
|
|
cursor_id: None,
|
|
limit: 2,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(first_page.len(), 2);
|
|
assert_eq!(first_page[0].log.id.as_str(), "log_history_schema");
|
|
assert_eq!(first_page[1].log.id.as_str(), "log_history_client");
|
|
|
|
let second_page = registry
|
|
.list_invocation_logs(crank_registry::ListInvocationLogsQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
level: None,
|
|
status: Some(crank_core::InvocationStatus::Error),
|
|
outcome_group: None,
|
|
search_text: None,
|
|
source: None,
|
|
operation_id: None,
|
|
agent_id: None,
|
|
created_after: Some("2026-03-25T12:00:00Z"),
|
|
created_before: Some("2026-03-25T12:04:00Z"),
|
|
cursor_created_at: Some("2026-03-25T12:02:00Z"),
|
|
cursor_id: Some(&crank_core::InvocationLogId::new("log_history_client")),
|
|
limit: 2,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(second_page.len(), 1);
|
|
assert_eq!(second_page[0].log.id.as_str(), "log_history_upstream");
|
|
|
|
let upstream_only = registry
|
|
.list_invocation_logs(crank_registry::ListInvocationLogsQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
level: None,
|
|
status: None,
|
|
outcome_group: Some(UsageOutcomeGroup::Upstream),
|
|
search_text: None,
|
|
source: None,
|
|
operation_id: None,
|
|
agent_id: None,
|
|
created_after: Some("2026-03-25T12:00:00Z"),
|
|
created_before: Some("2026-03-25T12:05:00Z"),
|
|
cursor_created_at: None,
|
|
cursor_id: None,
|
|
limit: 10,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(upstream_only.len(), 1);
|
|
assert_eq!(upstream_only[0].log.id.as_str(), "log_history_upstream");
|
|
|
|
let usage = registry
|
|
.list_usage_outcomes(UsageQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
period: crank_core::UsagePeriod::Last24Hours,
|
|
source: None,
|
|
created_after: "2026-03-25T12:00:00Z",
|
|
created_before: "2026-03-25T12:05:00Z",
|
|
bucket: UsageBucket::Hour,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let by_group = usage
|
|
.iter()
|
|
.map(|item| (item.group, item.calls_total))
|
|
.collect::<BTreeMap<_, _>>();
|
|
assert_eq!(by_group.get(&UsageOutcomeGroup::Success), Some(&1));
|
|
assert_eq!(by_group.get(&UsageOutcomeGroup::Upstream), Some(&1));
|
|
assert_eq!(by_group.get(&UsageOutcomeGroup::Client), Some(&1));
|
|
assert_eq!(by_group.get(&UsageOutcomeGroup::Schema), Some(&1));
|
|
assert_eq!(by_group.get(&UsageOutcomeGroup::Crank), Some(&1));
|
|
|
|
let half_open = registry
|
|
.summarize_usage(UsageQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
period: crank_core::UsagePeriod::Last24Hours,
|
|
source: None,
|
|
created_after: "2026-03-25T12:00:00Z",
|
|
created_before: "2026-03-25T12:04:00Z",
|
|
bucket: UsageBucket::Hour,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(half_open.rollup.calls_total, 4);
|
|
|
|
let retention = registry
|
|
.delete_invocation_logs_before(timestamp("2026-03-25T12:02:00Z"))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(retention.deleted_records, 2);
|
|
assert_eq!(
|
|
retention.status,
|
|
crank_registry::InvocationRetentionStatus::Completed
|
|
);
|
|
assert_eq!(retention.policy.preserved_usage_window_days, 90);
|
|
let retained = registry
|
|
.list_invocation_logs(crank_registry::ListInvocationLogsQuery {
|
|
workspace_id: &test_workspace_id(),
|
|
level: None,
|
|
status: None,
|
|
outcome_group: None,
|
|
search_text: None,
|
|
source: None,
|
|
operation_id: None,
|
|
agent_id: None,
|
|
created_after: Some("2026-03-25T12:00:00Z"),
|
|
created_before: Some("2026-03-25T12:05:00Z"),
|
|
cursor_created_at: None,
|
|
cursor_id: None,
|
|
limit: 10,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(retained.len(), 3);
|
|
assert_eq!(
|
|
retained.last().unwrap().log.id.as_str(),
|
|
"log_history_client"
|
|
);
|
|
|
|
database.cleanup().await;
|
|
}
|