khive-storage 0.13.0

Storage capability contracts and backend-neutral request context.
Documentation
use super::*;
use std::fmt;

#[derive(Debug)]
struct FakeSource(String);

impl fmt::Display for FakeSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl StdError for FakeSource {}

fn driver_err(operation: &'static str, message: &str) -> StorageError {
    StorageError::driver(
        StorageCapability::Text,
        operation,
        FakeSource(message.into()),
    )
}

#[test]
fn writer_task_request_state_display_is_stable() {
    assert_eq!(
        WriterTaskRequestState::NotStarted.to_string(),
        "not_started"
    );
    assert_eq!(
        WriterTaskRequestState::TransactionRolledBack.to_string(),
        "transaction_rolled_back"
    );
    assert_eq!(
        WriterTaskRequestState::SideEffectsUnknown.to_string(),
        "side_effects_unknown"
    );
}

#[test]
fn writer_task_busy_is_retryable_without_claiming_queue_rejection() {
    let error = StorageError::WriterTaskBusy { timeout_ms: 175 };
    assert!(error.is_retryable());
    assert_eq!(
            error.to_string(),
            "writer task could not begin within 175ms because SQLite remained busy; request was not executed"
        );
    assert_eq!(error.capability(), None);
}

#[test]
fn capacity_retryability_distinguishes_contention_from_refusal() {
    for (phase, label, retryable) in [
        (None, "floor", false),
        (Some(CapacityUnavailablePhase::Identity), "identity", false),
        (Some(CapacityUnavailablePhase::Lock), "lock", false),
        (
            Some(CapacityUnavailablePhase::LockContended),
            "lock_contended",
            true,
        ),
        (Some(CapacityUnavailablePhase::Probe), "probe", false),
    ] {
        let error = match phase {
            Some(phase) => {
                assert_eq!(phase.as_str(), label);
                assert_eq!(phase.to_string(), label);
                StorageError::CapacityUnavailable {
                    capability: StorageCapability::Sql,
                    phase,
                    message: "capacity admission unavailable".into(),
                }
            }
            None => StorageError::CapacityFloor {
                capability: StorageCapability::Sql,
                volume: "volume".into(),
                available_bytes: 99,
                floor_bytes: 100,
                required_headroom_bytes: 0,
            },
        };
        assert_eq!(error.is_retryable(), retryable, "{label}");
        let wrapped = StorageError::WriterTaskRequestFailed {
            request_state: WriterTaskRequestState::TransactionRolledBack,
            source: Box::new(error),
        };
        assert_eq!(wrapped.is_retryable(), retryable, "wrapped {label}");
    }
}

#[test]
fn writer_task_request_failure_preserves_source_policy_and_rollback_state() {
    let error = StorageError::WriterTaskRequestFailed {
        request_state: WriterTaskRequestState::TransactionRolledBack,
        source: Box::new(StorageError::Pool {
            operation: "writer_task_commit".into(),
            message: "commit refused".into(),
        }),
    };

    assert_eq!(error.capability(), None);
    assert!(
        error.is_retryable(),
        "rollback finality must not discard the source error's retry policy"
    );
    assert_eq!(
            error.to_string(),
            "writer task request failed (request_state=transaction_rolled_back): pool failure during writer_task_commit: commit refused"
        );
    assert_eq!(
        StdError::source(&error).map(ToString::to_string),
        Some("pool failure during writer_task_commit: commit refused".to_string()),
        "the original typed storage error must remain the public source"
    );
}

#[test]
fn writer_task_request_failure_does_not_invent_retryability() {
    let error = StorageError::WriterTaskRequestFailed {
        request_state: WriterTaskRequestState::TransactionRolledBack,
        source: Box::new(StorageError::InvalidInput {
            capability: StorageCapability::Notes,
            operation: "append_note".into(),
            message: "deterministic refusal".into(),
        }),
    };

    assert!(!error.is_retryable());
    assert_eq!(error.capability(), Some(StorageCapability::Notes));
}

#[test]
fn writer_task_terminated_is_uncapability_scoped_and_not_retryable() {
    for request_state in [
        WriterTaskRequestState::NotStarted,
        WriterTaskRequestState::TransactionRolledBack,
        WriterTaskRequestState::SideEffectsUnknown,
    ] {
        let error = StorageError::writer_task_terminated(request_state);
        assert_eq!(error.capability(), None);
        assert!(!error.is_retryable());
        assert_eq!(
            error.to_string(),
            format!("writer task terminated (request_state={request_state})")
        );
    }
}

#[test]
fn blob_integrity_errors_are_blob_scoped_and_not_retryable() {
    let requested = crate::blob::ContentRef::from_hex("a".repeat(64)).unwrap();
    let actual = crate::blob::ContentRef::from_hex("b".repeat(64)).unwrap();
    let errors = [
        StorageError::BlobTooLarge {
            content_ref: requested.clone(),
            max_bytes: 8,
            observed_at_least: 9,
        },
        StorageError::BlobSizeMismatch {
            content_ref: requested.clone(),
            metadata_bytes: 7,
            actual_bytes: 8,
        },
        StorageError::BlobDigestMismatch {
            expected: requested,
            actual,
        },
    ];

    for error in errors {
        assert_eq!(error.capability(), Some(StorageCapability::Blob));
        assert!(!error.is_retryable());
    }
}

#[test]
fn fts5_syntax_error_at_fts_search_is_classified_as_syntax_error() {
    let e = driver_err("fts_search", "fts5: syntax error near \"@\"");
    assert!(e.is_fts5_syntax_error());
}

#[test]
fn fts5_parser_stack_overflow_is_classified_as_syntax_error() {
    let e = driver_err("fts_search", "fts5: parser stack overflow");
    assert!(e.is_fts5_syntax_error());
}

#[test]
fn fts5_unsupported_column_query_is_classified_as_syntax_error() {
    let e = driver_err(
        "fts_search",
        "fts5: column queries are not supported (detail=none)",
    );
    assert!(e.is_fts5_syntax_error());
}

#[test]
fn timeout_is_not_classified_as_syntax_error() {
    let e = StorageError::Timeout {
        operation: "fts_search".into(),
    };
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn pool_failure_is_not_classified_as_syntax_error() {
    let e = StorageError::Pool {
        operation: "fts_search".into(),
        message: "pool exhausted".into(),
    };
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn driver_error_at_non_search_operation_is_not_classified_as_syntax_error() {
    let e = driver_err("open_fts_reader", "fts5: syntax error near \"@\"");
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn driver_error_with_unrelated_message_is_not_classified_as_syntax_error() {
    let e = driver_err("fts_search", "disk I/O error");
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn fts5_phrase_detail_query_is_classified_as_syntax_error() {
    let e = driver_err(
        "fts_search",
        "fts5: phrase queries are not supported (detail!=full)",
    );
    assert!(e.is_fts5_syntax_error());
}

#[test]
fn fts5_near_detail_query_is_classified_as_syntax_error() {
    let e = driver_err(
        "fts_search",
        "fts5: NEAR queries are not supported (detail!=full)",
    );
    assert!(e.is_fts5_syntax_error());
}

#[test]
fn unprefixed_detail_message_is_not_classified_as_syntax_error() {
    let e = driver_err(
        "fts_search",
        "phrase queries are not supported (detail!=full)",
    );
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn fts5_shadow_table_corruption_is_not_classified_as_syntax_error() {
    let e = driver_err(
        "fts_search",
        "fts5: error creating shadow table notes_content: no such table",
    );
    assert!(!e.is_fts5_syntax_error());
}

#[test]
fn non_text_capability_is_not_classified_as_syntax_error() {
    let e = StorageError::Driver {
        capability: StorageCapability::Vectors,
        operation: "fts_search".into(),
        source: Box::new(FakeSource("fts5: syntax error near \"@\"".into())),
    };
    assert!(!e.is_fts5_syntax_error());
}

fn driver_err_sql(operation: &'static str, message: &str) -> StorageError {
    StorageError::driver(
        StorageCapability::Sql,
        operation,
        FakeSource(message.into()),
    )
}

#[test]
fn unique_constraint_failure_at_execute_sql_capability_is_classified() {
    let e = driver_err_sql(
        "execute",
        "UNIQUE constraint failed: brain_serve_ledger.namespace, \
             brain_serve_ledger.target_id, brain_serve_ledger.query_class, \
             brain_serve_ledger.served_at",
    );
    assert!(e.is_unique_constraint_violation());
}

#[test]
fn unique_constraint_failure_at_pool_writer_execute_is_classified() {
    let e = driver_err_sql("pool_writer.execute", "UNIQUE constraint failed: t.id");
    assert!(e.is_unique_constraint_violation());
}

#[test]
fn unique_constraint_message_at_non_execute_operation_is_not_classified() {
    let e = driver_err_sql("query_row", "UNIQUE constraint failed: t.id");
    assert!(!e.is_unique_constraint_violation());
}

#[test]
fn non_unique_driver_error_at_execute_is_not_classified() {
    let e = driver_err_sql("execute", "disk I/O error");
    assert!(!e.is_unique_constraint_violation());
}

#[test]
fn non_sql_capability_is_not_classified_as_unique_violation() {
    let e = driver_err("execute", "UNIQUE constraint failed: t.id");
    assert!(!e.is_unique_constraint_violation());
}

#[test]
fn timeout_is_not_classified_as_unique_violation() {
    let e = StorageError::Timeout {
        operation: "execute".into(),
    };
    assert!(!e.is_unique_constraint_violation());
}