220 lines
7.8 KiB
Rust
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(®istry).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(®istry).await;
|
|
let call_at = truncate_to_micros(OffsetDateTime::now_utc() + Duration::seconds(1));
|
|
record_onboarding_test_and_call(
|
|
®istry,
|
|
&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(®istry).await;
|
|
let (later_agent_id, later_key_id) =
|
|
publish_additional_agent_path(®istry, &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;
|
|
}
|