khive-runtime 0.13.0

Composable Service API: entity/note CRUD, graph traversal, hybrid search, curation.
Documentation
use super::*;

fn failed() -> Vec<RefusalEventRecording> {
    vec![RefusalEventRecording::Failed {
        item_index: 2,
        subject: Uuid::from_u128(11),
        error_class: RefusalRecordingErrorClass::EventAppendFailed,
    }]
}

#[test]
fn refusal_events_preserve_the_source_type_display_and_policy_class() {
    let original = RuntimeError::SecretDetected(crate::secret_gate::SecretMatch {
        detector: "fixture",
        trigger: None,
        masked: "never-in-display".into(),
        location: Some("atoms[2].content".into()),
    });
    let message = original.to_string();
    let wrapped = original.with_refusal_events(failed());
    assert_eq!(wrapped.to_string(), message);
    assert!(matches!(
        wrapped.refusal_source(),
        RuntimeError::SecretDetected(_)
    ));
    assert!(std::error::Error::source(&wrapped)
        .unwrap()
        .downcast_ref::<RuntimeError>()
        .is_some_and(|error| matches!(error, RuntimeError::SecretDetected(_))));
    assert_eq!(
        wrapped.channel_ingest_failure_class(),
        ChannelIngestFailureClass::Permanent {
            reason: "SecretDetected"
        }
    );
    assert!(wrapped.retryable_failure_context().is_none());

    let lookalike = RuntimeError::InvalidInput(message).with_refusal_events(failed());
    assert_eq!(
        lookalike.channel_ingest_failure_class(),
        ChannelIngestFailureClass::Unknown {
            reason: "InvalidInput"
        }
    );
    let policy = RuntimeError::Khive(
        khive_types::KhiveError::conflict("immutable")
            .with_details(khive_types::Details::new([("reason", "stream_member")])),
    )
    .with_refusal_events(failed());
    assert!(policy.is_stream_policy_refusal());
}

#[test]
fn refusal_events_preserve_retry_and_writer_finality_contexts() {
    let checkout = RuntimeError::from(khive_db::SqliteError::WriterPoolCheckoutTimeout {
        timeout: Duration::from_millis(17),
    })
    .with_refusal_events(failed());
    assert_eq!(
        checkout
            .writer_pool_checkout_timeout_context()
            .unwrap()
            .timeout,
        Duration::from_millis(17)
    );
    assert_eq!(
        checkout.admission_failure_context().unwrap().stage,
        WRITER_POOL_CHECKOUT_TIMEOUT_STAGE
    );
    assert_eq!(
        checkout.retryable_failure_context().unwrap().stage,
        WRITER_POOL_CHECKOUT_TIMEOUT_STAGE
    );

    let queued =
        RuntimeError::Storage(khive_storage::StorageError::WriteQueueFull { timeout_ms: 23 })
            .with_refusal_events(failed());
    assert_eq!(
        queued.retryable_failure_context().unwrap().stage,
        WRITER_QUEUE_SATURATED_STAGE
    );
    assert_eq!(
        queued.channel_ingest_failure_class(),
        ChannelIngestFailureClass::Retryable { reason: "Storage" }
    );

    let busy =
        RuntimeError::Storage(khive_storage::StorageError::WriterTaskBusy { timeout_ms: 31 })
            .with_refusal_events(failed());
    assert_eq!(
        busy.retryable_failure_context().unwrap().stage,
        WRITER_TASK_BEGIN_BUSY_STAGE
    );

    let stopped = RuntimeError::Storage(khive_storage::StorageError::writer_task_terminated(
        khive_storage::WriterTaskRequestState::SideEffectsUnknown,
    ))
    .with_refusal_events(failed());
    let context = stopped.writer_task_failure_context().unwrap();
    assert_eq!(
        context.request_state,
        khive_storage::WriterTaskRequestState::SideEffectsUnknown
    );
    assert!(context.task_terminated);
    assert!(!context.retryable);
    assert!(stopped.retryable_failure_context().is_none());
}

#[test]
fn refusal_events_preserve_the_original_error_and_its_underlying_source_chain() {
    let original = RuntimeError::Storage(khive_storage::StorageError::driver(
        khive_storage::StorageCapability::Notes,
        "refusal-context-fixture",
        std::io::Error::new(std::io::ErrorKind::PermissionDenied, "fixture refusal"),
    ));
    let message = original.to_string();
    let wrapped = original.with_refusal_events(failed());
    assert_eq!(wrapped.to_string(), message);

    let runtime = std::error::Error::source(&wrapped)
        .unwrap()
        .downcast_ref::<RuntimeError>()
        .expect("the first source must retain the runtime classification");
    assert!(matches!(runtime, RuntimeError::Storage(_)));
    let storage = std::error::Error::source(runtime)
        .unwrap()
        .downcast_ref::<khive_storage::StorageError>()
        .expect("the original runtime error must retain its storage source");
    let driver = std::error::Error::source(storage)
        .unwrap()
        .downcast_ref::<std::io::Error>()
        .expect("the storage source chain must remain traversable");
    assert_eq!(driver.kind(), std::io::ErrorKind::PermissionDenied);
}

#[test]
fn refusal_events_with_no_eligible_targets_do_not_wrap_or_grow_metadata() {
    let error = RuntimeError::InvalidInput("unchanged".into()).with_refusal_events(vec![]);
    assert!(matches!(error, RuntimeError::InvalidInput(ref message) if message == "unchanged"));
    let error = error
        .with_refusal_events(failed())
        .with_refusal_events(vec![RefusalEventRecording::Recorded {
            item_index: 3,
            subject: Uuid::from_u128(12),
            event_id: Uuid::from_u128(13),
        }]);
    let source = std::error::Error::source(&error)
        .unwrap()
        .downcast_ref::<RuntimeError>()
        .expect("appending recordings must not add another error wrapper");
    assert!(matches!(source, RuntimeError::InvalidInput(_)));
    assert!(std::error::Error::source(source).is_none());
    let RuntimeError::RefusedWithEvents { context } = error else {
        panic!("recordings must accompany the original typed source");
    };
    let RefusalEventContext { source, recordings } = context;
    assert!(matches!(*source, RuntimeError::InvalidInput(_)));
    assert_eq!(recordings.len(), 2);
    assert_eq!(recordings[0], failed()[0]);
}

#[test]
fn refusal_recording_classes_serialize_only_the_closed_safe_spellings() {
    for (class, name) in [
        (
            RefusalRecordingErrorClass::EventStoreUnavailable,
            "event_store_unavailable",
        ),
        (
            RefusalRecordingErrorClass::EventAppendFailed,
            "event_append_failed",
        ),
    ] {
        assert_eq!(class.as_str(), name);
        assert_eq!(
            serde_json::to_value(class).unwrap(),
            serde_json::json!(name)
        );
    }
}