наблюдаемость: завершить базовый контур Community
CI / Rust Checks (push) Failing after 4m28s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped

Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
2026-07-31 01:01:14 +03:00
parent 99bd05c145
commit 0e8f1ca03a
160 changed files with 13506 additions and 1499 deletions
@@ -1,6 +1,8 @@
mod integration {
mod agents_usage;
mod common;
mod migrations;
mod observability;
mod operations_artifacts;
mod workspace_access;
}
@@ -228,32 +228,36 @@ async fn manages_operation_usage_and_agent_ref_reads() {
})
.await
.unwrap();
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
.unwrap();
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
.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
);
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)
@@ -0,0 +1,44 @@
use crank_registry::PostgresRegistry;
use sqlx::Row;
#[tokio::test]
async fn core_migration_is_versioned_and_safe_under_concurrent_startup() {
let database_url = crank_test_support::postgres_schema_url("test_core_migration").await;
let (first, second) = tokio::join!(
PostgresRegistry::connect(&database_url),
PostgresRegistry::connect(&database_url),
);
let first = first.expect("first service startup must apply the migration");
second.expect("second service startup must observe the applied migration");
let rows = sqlx::query(
"select version, description, checksum from __crank_core_migrations order by version",
)
.fetch_all(first.pool())
.await
.expect("migration ledger must be readable");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<i32, _>("version"), 1);
assert_eq!(
rows[0].get::<String, _>("description"),
"community baseline"
);
assert_eq!(
rows[0].get::<String, _>("checksum"),
"crank-community-baseline-v1"
);
let approval_columns = sqlx::query(
"select column_name
from information_schema.columns
where table_schema = current_schema()
and table_name = 'approval_requests'
and column_name in ('execution_started_at', 'execution_attempts', 'request_fingerprint')",
)
.fetch_all(first.pool())
.await
.expect("approval schema must be readable");
assert_eq!(approval_columns.len(), 3);
}
@@ -0,0 +1,31 @@
use crank_registry::{
CreateInvocationLogRequest, InvocationHistoryLossCategory, InvocationHistoryWriteOutcome,
};
use super::common::{TestDatabase, test_invocation_log};
#[tokio::test]
async fn invocation_history_write_returns_typed_loss_without_error_details() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let log = test_invocation_log(
"log_missing_owner",
&crank_core::OperationId::new("op_missing"),
None,
crank_core::InvocationStatus::Ok,
10,
"2026-03-25T12:20:00Z",
);
let outcome = registry
.create_invocation_log(CreateInvocationLogRequest { log: &log })
.await;
assert_eq!(
outcome,
InvocationHistoryWriteOutcome::Lost(crank_registry::InvocationHistoryLoss {
category: InvocationHistoryLossCategory::InvalidRecord,
})
);
database.cleanup().await;
}
@@ -38,6 +38,38 @@ fn timestamp(value: &str) -> OffsetDateTime {
OffsetDateTime::parse(value, &Rfc3339).unwrap()
}
#[tokio::test]
async fn bootstrap_user_does_not_overwrite_an_existing_password() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let email = "bootstrap-owner@example.com";
let user_id = registry
.upsert_bootstrap_user(email, "Bootstrap Owner", "initial-bootstrap-hash")
.await
.unwrap();
registry
.update_user_password(&user_id, "user-selected-hash")
.await
.unwrap();
let repeated_user_id = registry
.upsert_bootstrap_user(email, "Changed Bootstrap Name", "changed-bootstrap-hash")
.await
.unwrap();
let stored = registry
.get_auth_user_by_email(email)
.await
.unwrap()
.unwrap();
assert_eq!(repeated_user_id, user_id);
assert_eq!(stored.password_hash, "user-selected-hash");
assert_eq!(stored.user.display_name, "Bootstrap Owner");
database.cleanup().await;
}
#[tokio::test]
async fn stores_and_finishes_yaml_import_jobs() {
let database = TestDatabase::new().await;
@@ -352,6 +384,24 @@ async fn creates_and_loads_user_sessions_with_typed_expiration() {
assert_eq!(session.user.id, user_id);
assert!(session.user.created_at.unix_timestamp() > 0);
registry.touch_user_session(&session_id).await.unwrap();
let first_seen = sqlx::query_scalar::<_, OffsetDateTime>(
"select last_seen_at from user_sessions where id = $1",
)
.bind(session_id.as_str())
.fetch_one(registry.pool())
.await
.unwrap();
registry.touch_user_session(&session_id).await.unwrap();
let second_seen = sqlx::query_scalar::<_, OffsetDateTime>(
"select last_seen_at from user_sessions where id = $1",
)
.bind(session_id.as_str())
.fetch_one(registry.pool())
.await
.unwrap();
assert_eq!(second_seen, first_seen);
database.cleanup().await;
}
@@ -454,6 +504,40 @@ async fn manages_platform_api_key_read_paths() {
Some(timestamp("2026-03-25T12:05:00Z"))
);
registry
.touch_platform_api_key(
&workspace.id,
&PlatformApiKeyId::new("key_01"),
&timestamp("2026-03-25T12:05:30Z"),
)
.await
.unwrap();
let throttled = registry
.list_platform_api_keys(&workspace.id)
.await
.unwrap();
assert_eq!(
throttled[0].api_key.last_used_at,
Some(timestamp("2026-03-25T12:05:00Z"))
);
registry
.touch_platform_api_key(
&workspace.id,
&PlatformApiKeyId::new("key_01"),
&timestamp("2026-03-25T12:06:01Z"),
)
.await
.unwrap();
let refreshed = registry
.list_platform_api_keys(&workspace.id)
.await
.unwrap();
assert_eq!(
refreshed[0].api_key.last_used_at,
Some(timestamp("2026-03-25T12:06:01Z"))
);
database.cleanup().await;
}
@@ -587,7 +671,6 @@ async fn manages_approval_request_lifecycle() {
.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();
@@ -608,23 +691,10 @@ async fn manages_approval_request_lifecycle() {
.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,
@@ -637,7 +707,6 @@ async fn manages_approval_request_lifecycle() {
.await
.unwrap()
.unwrap();
assert_eq!(completed.approval.status, ApprovalRequestStatus::Completed);
assert_eq!(
completed.approval.response_payload,
@@ -653,9 +722,81 @@ async fn manages_approval_request_lifecycle() {
.await
.unwrap();
assert_eq!(completed_by_status.len(), 1);
let mut interrupted_approval = approval.clone();
interrupted_approval.id = ApprovalRequestId::new("approval_interrupted_01");
interrupted_approval.request_payload = json!({"amount": 150});
interrupted_approval.created_at = timestamp("2026-03-25T12:02:10Z");
registry
.create_approval_request(CreateApprovalRequest {
approval: &interrupted_approval,
})
.await
.unwrap();
registry
.decide_approval_request(DecideApprovalRequest {
workspace_id: &workspace_id,
agent_id: &agent.id,
approval_id: &interrupted_approval.id,
status: ApprovalRequestStatus::Approved,
decided_at: timestamp("2026-03-25T12:02:20Z"),
decided_by_key_id: &approval_key.id,
response_payload: Some(json!({"approve": "yes"})),
decision_note: None,
})
.await
.unwrap()
.unwrap();
registry
.claim_approval_request(
&workspace_id,
&agent.id,
&interrupted_approval.id,
timestamp("2026-03-25T12:02:21Z"),
)
.await
.unwrap()
.unwrap();
let recovered = registry
.claim_next_recoverable_approval_request(
timestamp("2026-03-25T12:03:00Z"),
timestamp("2026-03-25T12:02:59Z"),
)
.await
.unwrap();
assert!(recovered.is_none());
let interrupted = registry
.fail_next_interrupted_approval_request(timestamp("2026-03-25T12:02:30Z"))
.await
.unwrap()
.unwrap();
assert_eq!(interrupted.approval.id, interrupted_approval.id);
assert_eq!(interrupted.approval.status, ApprovalRequestStatus::Failed);
assert_eq!(
completed_by_status[0].approval.status,
ApprovalRequestStatus::Completed
interrupted.approval.response_payload,
Some(json!({
"error": {
"code": "approval_execution_outcome_unknown",
"message": "execution was interrupted; the operation was not retried automatically"
}
}))
);
let failed_by_status = registry
.list_approval_requests(ListApprovalRequestsQuery {
workspace_id: &workspace_id,
status: Some(ApprovalRequestStatus::Failed),
limit: 10,
})
.await
.unwrap();
assert_eq!(failed_by_status.len(), 1);
assert_eq!(
failed_by_status[0].approval.status,
ApprovalRequestStatus::Failed
);
let pending_after_decision = registry