Files
crank/crates/crank-registry/tests/integration/onboarding_lifecycle.rs
T

220 lines
7.8 KiB
Rust

use super::*;
#[tokio::test]
async fn server_owned_lifecycle_events_are_idempotent_and_completion_uses_the_eligible_cohort() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace_id = WorkspaceId::new("ws_default");
let eligible_at = truncate_to_micros(OffsetDateTime::now_utc() - Duration::minutes(3));
let eligible = registry
.ensure_onboarding_eligibility(&workspace_id, eligible_at)
.await
.unwrap();
assert_eq!(eligible.eligible_since, Some(eligible_at));
let repeated = registry
.ensure_onboarding_eligibility(&workspace_id, OffsetDateTime::now_utc())
.await
.unwrap();
assert_eq!(repeated.eligible_since, Some(eligible_at));
let (operation_id, agent_id, key_id) = published_path(&registry).await;
let mut test_log = test_invocation_log(
"log_onboarding_server_test",
&operation_id,
None,
InvocationStatus::Ok,
20,
"2026-03-25T12:14:00Z",
);
test_log.source = InvocationSource::AdminTestRun;
registry
.create_invocation_log(CreateInvocationLogRequest { log: &test_log })
.await;
registry
.touch_platform_api_key(&workspace_id, &key_id, &OffsetDateTime::now_utc())
.await
.unwrap();
let mut success = test_invocation_log(
"log_onboarding_server_call",
&operation_id,
Some(agent_id),
InvocationStatus::Ok,
25,
"2026-03-25T12:16:00Z",
);
success.platform_api_key_id = Some(key_id);
success.created_at = truncate_to_micros(OffsetDateTime::now_utc());
registry
.create_invocation_log(CreateInvocationLogRequest { log: &success })
.await;
let completed = registry
.ensure_onboarding_completion(&workspace_id, OffsetDateTime::now_utc())
.await
.unwrap();
assert!(completed.completed);
let later_draft = test_operation("op_onboarding_later_draft", 1, OperationStatus::Draft);
registry
.create_operation(&workspace_id, &later_draft, None)
.await
.unwrap();
let after_later_draft = registry
.get_onboarding_projection(&workspace_id)
.await
.unwrap();
assert_eq!(after_later_draft.operation_id, Some(operation_id));
assert!(after_later_draft.completed);
let events = registry
.list_product_events(crank_registry::ListProductEventsQuery {
workspace_id: &workspace_id,
kind: Some(crank_core::ProductEventKind::OnboardingCompleted),
created_after: OffsetDateTime::UNIX_EPOCH,
created_before: OffsetDateTime::now_utc() + Duration::minutes(1),
limit: 10,
})
.await
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].event.eligible_since, Some(eligible_at));
assert_eq!(events[0].event.occurred_at, success.created_at);
registry
.ensure_onboarding_completion(&workspace_id, OffsetDateTime::now_utc())
.await
.unwrap();
let events = registry
.list_product_events(crank_registry::ListProductEventsQuery {
workspace_id: &workspace_id,
kind: Some(crank_core::ProductEventKind::OnboardingCompleted),
created_after: OffsetDateTime::UNIX_EPOCH,
created_before: OffsetDateTime::now_utc() + Duration::minutes(1),
limit: 10,
})
.await
.unwrap();
assert_eq!(events.len(), 1);
database.cleanup().await;
}
#[tokio::test]
async fn first_call_before_first_eligibility_is_repaired_with_the_invocation_timestamp() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace_id = WorkspaceId::new("ws_default");
let (operation_id, agent_id, key_id) = published_path(&registry).await;
let call_at = truncate_to_micros(OffsetDateTime::now_utc() + Duration::seconds(1));
record_onboarding_test_and_call(
&registry,
&operation_id,
&agent_id,
&key_id,
"before_eligibility",
call_at,
)
.await;
let before_eligibility = registry
.get_onboarding_projection(&workspace_id)
.await
.unwrap();
assert!(before_eligibility.completed);
assert!(!before_eligibility.was_completed);
let eligible_at = call_at + Duration::seconds(1);
let repaired = registry
.ensure_onboarding_eligibility(&workspace_id, eligible_at)
.await
.unwrap();
assert!(repaired.completed);
assert!(repaired.was_completed);
let events = registry
.list_product_events(crank_registry::ListProductEventsQuery {
workspace_id: &workspace_id,
kind: Some(crank_core::ProductEventKind::OnboardingCompleted),
created_after: OffsetDateTime::UNIX_EPOCH,
created_before: eligible_at + Duration::minutes(1),
limit: 10,
})
.await
.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].event.occurred_at, call_at);
assert_eq!(events[0].event.eligible_since, Some(eligible_at));
database.cleanup().await;
}
#[tokio::test]
async fn projection_keeps_agent_and_key_on_the_same_terminal_path() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace_id = WorkspaceId::new("ws_default");
let (operation_id, _earlier_agent_id, _earlier_key_id) = published_path(&registry).await;
let (later_agent_id, later_key_id) =
publish_additional_agent_path(&registry, &operation_id).await;
let mut success = test_invocation_log(
"log_onboarding_later_agent_success",
&operation_id,
Some(later_agent_id.clone()),
InvocationStatus::Ok,
25,
"2026-03-25T12:18:00Z",
);
success.platform_api_key_id = Some(later_key_id.clone());
success.created_at = OffsetDateTime::now_utc();
registry
.create_invocation_log(CreateInvocationLogRequest { log: &success })
.await;
let projection = registry
.get_onboarding_projection(&workspace_id)
.await
.unwrap();
assert_eq!(projection.agent_id, Some(later_agent_id));
assert_eq!(projection.platform_api_key_id, Some(later_key_id));
assert_eq!(projection.first_call_log_id, Some(success.id));
database.cleanup().await;
}
#[tokio::test]
async fn presentation_milestone_is_idempotent_and_revision_guarded() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace_id = WorkspaceId::new("ws_default");
let initial = registry
.get_onboarding_projection(&workspace_id)
.await
.unwrap();
let event_id = ProductEventId::new("pe_onboarding_eligible");
let request = RecordOnboardingMilestoneRequest {
workspace_id: &workspace_id,
event_id: &event_id,
milestone: OnboardingPresentationMilestone::Eligible,
idempotency_key: "eligible:first-login",
expected_revision: initial.revision,
occurred_at: OffsetDateTime::now_utc(),
eligible_since: Some(OffsetDateTime::now_utc()),
};
let first = registry
.record_onboarding_milestone(request.clone())
.await
.unwrap();
assert!(first.accepted);
let replay = registry
.record_onboarding_milestone(RecordOnboardingMilestoneRequest {
expected_revision: first.projection.revision,
..request.clone()
})
.await
.unwrap();
assert!(!replay.accepted);
assert_eq!(replay.projection.revision, first.projection.revision);
let stale_replay = registry.record_onboarding_milestone(request).await.unwrap();
assert!(!stale_replay.accepted);
assert_eq!(stale_replay.projection.revision, first.projection.revision);
database.cleanup().await;
}