feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
@@ -23,8 +23,9 @@ use crank_registry::{
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
DescriptorMetadata, OperationSampleMetadata, PlatformApiKeyRecord, PostgresRegistry,
PublishAgentRequest, PublishRequest, RegistryError, RegistryOperation, SampleKind,
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
WorkspaceRecord, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
SaveAgentCatalogConfigRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, UsageBucket, UsageOutcomeGroup, UsageQuery, WorkspaceRecord,
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
};
fn test_workspace_id() -> WorkspaceId {
@@ -145,6 +146,7 @@ async fn manages_published_agent_tool_reads() {
version: version.version,
published_at: &timestamp("2026-03-25T12:11:00Z"),
published_by: Some("alice"),
expected_state: None,
})
.await
.unwrap();
@@ -170,6 +172,333 @@ async fn manages_published_agent_tool_reads() {
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: &timestamp("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: &timestamp("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: &timestamp("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: &timestamp("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: &timestamp("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: &timestamp("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,
&timestamp("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: &timestamp("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: &timestamp("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: &timestamp("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;
@@ -225,6 +554,7 @@ async fn manages_operation_usage_and_agent_ref_reads() {
version: version.version,
published_at: &timestamp("2026-03-25T12:11:00Z"),
published_by: Some("alice"),
expected_state: None,
})
.await
.unwrap();
@@ -243,6 +573,27 @@ async fn manages_operation_usage_and_agent_ref_reads() {
.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 {
@@ -286,3 +637,214 @@ async fn manages_operation_usage_and_agent_ref_reads() {
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;
}