1use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::Serialize;
8
9#[allow(dead_code)]
27pub mod error_codes {
28 pub const NOT_FOUND: &str = "NOT_FOUND";
30 pub const VALIDATION_FAILED: &str = "VALIDATION_FAILED";
31 pub const DATABASE_ERROR: &str = "DATABASE_ERROR";
32 pub const CONFLICT: &str = "CONFLICT";
33 pub const REFLECTION_DEPTH_EXCEEDED: &str = "REFLECTION_DEPTH_EXCEEDED";
34 pub const SYNTHESIS_DEPTH_EXCEEDED: &str = "SYNTHESIS_DEPTH_EXCEEDED";
35 pub const REFLECTION_CYCLE_DETECTED: &str = "REFLECTION_CYCLE_DETECTED";
36 pub const GOVERNANCE_REFUSED: &str = "GOVERNANCE_REFUSED";
37 pub const QUOTA_EXCEEDED: &str = "QUOTA_EXCEEDED";
45 pub const ATTESTATION_FAILED: &str = "ATTESTATION_FAILED";
53
54 pub const PENDING_ACTION_NOT_FOUND: &str = "PENDING_ACTION_NOT_FOUND";
56 pub const AMBIGUOUS_ID_PREFIX: &str = "AMBIGUOUS_ID_PREFIX";
57 pub const INVALID_ARGUMENT: &str = "INVALID_ARGUMENT";
58 pub const PENDING_ACTION_STATE_INVALID: &str = "PENDING_ACTION_STATE_INVALID";
59 pub const LINK_PERMISSION_DENIED: &str = "LINK_PERMISSION_DENIED";
60 pub const LINK_REFLECTION_CYCLE: &str = "LINK_REFLECTION_CYCLE";
61 pub const APPROVER_LAUNDERING: &str = "APPROVER_LAUNDERING";
62 pub const UNIQUE_CONFLICT: &str = "UNIQUE_CONFLICT";
63 pub const ARCHIVE_RESTORE_COLLISION: &str = "ARCHIVE_RESTORE_COLLISION";
64 pub const ARCHIVE_SUPERSEDE_FAILED: &str = "ARCHIVE_SUPERSEDE_FAILED";
65 pub const SQLCIPHER_MISSING_PASSPHRASE: &str = "SQLCIPHER_MISSING_PASSPHRASE";
66
67 pub const STORE_BACKEND_UNAVAILABLE: &str = "BACKEND_UNAVAILABLE";
69 pub const STORE_UNSUPPORTED_CAPABILITY: &str = "UNSUPPORTED_CAPABILITY";
70 pub const STORE_OPERATION_FAILED: &str = "STORE_OPERATION_FAILED";
71 pub const STORE_DATABASE_ERROR: &str = "DATABASE_ERROR";
72 pub const STORE_NOT_FOUND: &str = "NOT_FOUND";
73 pub const STORE_VALIDATION_FAILED: &str = "VALIDATION_FAILED";
74 pub const STORE_GOVERNANCE_REFUSED: &str = "GOVERNANCE_REFUSED";
75 pub const STORE_VERSION_CONFLICT: &str = "VERSION_CONFLICT";
76}
77
78#[allow(dead_code)]
96pub mod msg {
97 pub const INTERNAL_SERVER_ERROR: &str = "internal server error";
99
100 pub const MEMORY_NOT_FOUND: &str = "memory not found";
102 pub const NOT_FOUND_IN_ARCHIVE: &str = "not found in archive";
103 pub const SKILL_NOT_FOUND: &str = "skill not found";
104 pub const SOURCE_MEMORY_NOT_FOUND: &str = "source memory not found";
105 pub const PENDING_ACTION_NOT_FOUND_OR_DECIDED: &str =
106 "pending action not found or already decided";
107
108 pub const GOVERNANCE_REQUIRES_APPROVAL: &str = "governance requires approval";
110 pub const GOVERNANCE_CHECK_FAILED: &str = "governance check failed";
111 pub const CONSENSUS_NOT_REACHED: &str = "consensus threshold not yet reached";
112 pub const DECISION_WRITE_FAILED: &str = "decision write failed";
113
114 pub const CALLER_NOT_SOURCE_MEMORY_OWNER: &str = "caller does not own this source memory";
116 pub const CALLER_NOT_NAMESPACE_STANDARD_OWNER: &str =
117 "caller does not own this namespace standard";
118 pub const AGENT_ID_BODY_MISMATCH: &str =
119 "agent_id body parameter does not match authenticated caller";
120 pub const AGENT_ID_QUERY_MISMATCH: &str =
121 "agent_id query parameter does not match authenticated caller";
122 pub const INVALID_OR_MISSING_SIGNATURE: &str = "invalid or missing X-AI-Memory-Signature";
123
124 pub const FORGET_FILTER_REQUIRED: &str =
126 "at least one of namespace, pattern, or tier is required";
127 pub const MAX_DEPTH_MIN: &str = "max_depth must be >= 1";
128 pub const VERIFY_LINK_ARGS_REQUIRED: &str = "verify_link requires either source_id or link_id";
129 pub const ENTITY_ID_EMPTY: &str = "entity_id cannot be empty";
130 pub const MEMORY_ID_EMPTY: &str = "memory_id cannot be empty";
131
132 pub const ID_REQUIRED: &str = "id is required";
134 pub const CONTENT_REQUIRED: &str = "content is required";
135 pub const TITLE_REQUIRED: &str = "title is required";
136 pub const MEMORY_ID_REQUIRED: &str = "memory_id is required";
137 pub const NAMESPACE_REQUIRED: &str = "namespace is required";
138 pub const SOURCE_ID_REQUIRED: &str = "source_id is required";
139 pub const TARGET_ID_REQUIRED: &str = "target_id is required";
140 pub const QUERY_REQUIRED: &str = "query is required";
141 pub const CONTEXT_REQUIRED: &str = "context is required";
142
143 pub const READ_SCHEMA_VERSION: &str = "read schema_version";
145
146 #[must_use]
149 pub fn invalid(field: &str, e: impl std::fmt::Display) -> String {
150 format!("invalid {field}: {e}")
151 }
152
153 #[must_use]
155 pub fn error_line(e: impl std::fmt::Display) -> String {
156 format!("error: {e}")
157 }
158
159 #[must_use]
161 pub fn memory_not_found(id: impl std::fmt::Display) -> String {
162 format!("{MEMORY_NOT_FOUND}: {id}")
163 }
164
165 #[must_use]
167 pub fn skill_not_found(skill_id: impl std::fmt::Display) -> String {
168 format!("{SKILL_NOT_FOUND}: {skill_id}")
169 }
170
171 #[must_use]
173 pub fn pending_action_not_found(pending_id: impl std::fmt::Display) -> String {
174 format!("pending action not found: {pending_id}")
175 }
176
177 #[must_use]
179 pub fn not_found(id: impl std::fmt::Display) -> String {
180 format!("not found: {id}")
181 }
182
183 #[must_use]
185 pub fn approve_rejected(reason: impl std::fmt::Display) -> String {
186 format!("approve rejected: {reason}")
187 }
188
189 #[must_use]
191 pub fn older_than_days_negative(days: impl std::fmt::Display) -> String {
192 format!("older_than_days must be non-negative (got {days})")
193 }
194
195 #[must_use]
197 pub fn signature_verify_failed(e: impl std::fmt::Display) -> String {
198 format!("signature verify failed: {e}")
199 }
200
201 #[must_use]
203 pub fn zstd_decompress_body(e: impl std::fmt::Display) -> String {
204 format!("zstd decompress body: {e}")
205 }
206
207 #[must_use]
209 pub fn network(e: impl std::fmt::Display) -> String {
210 format!("network: {e}")
211 }
212
213 #[must_use]
215 pub fn unsubscribe(e: impl std::fmt::Display) -> String {
216 format!("unsubscribe: {e}")
217 }
218
219 #[must_use]
221 pub fn opening(path: impl std::fmt::Display) -> String {
222 format!("opening {path}")
223 }
224
225 #[must_use]
227 pub fn reading(path: impl std::fmt::Display) -> String {
228 format!("reading {path}")
229 }
230
231 #[must_use]
233 pub fn writing(path: impl std::fmt::Display) -> String {
234 format!("writing {path}")
235 }
236}
237
238#[cfg(test)]
239mod arch_9_slug_tests {
240 use super::error_codes::*;
241
242 #[test]
243 fn arch_9_canonical_slugs_have_stable_string_values() {
244 assert_eq!(NOT_FOUND, "NOT_FOUND");
253 assert_eq!(VALIDATION_FAILED, "VALIDATION_FAILED");
254 assert_eq!(DATABASE_ERROR, "DATABASE_ERROR");
255 assert_eq!(CONFLICT, "CONFLICT");
256 assert_eq!(GOVERNANCE_REFUSED, "GOVERNANCE_REFUSED");
257 assert_eq!(REFLECTION_DEPTH_EXCEEDED, "REFLECTION_DEPTH_EXCEEDED");
258 assert_eq!(SYNTHESIS_DEPTH_EXCEEDED, "SYNTHESIS_DEPTH_EXCEEDED");
259 assert_eq!(REFLECTION_CYCLE_DETECTED, "REFLECTION_CYCLE_DETECTED");
260 assert_eq!(AMBIGUOUS_ID_PREFIX, "AMBIGUOUS_ID_PREFIX");
261 assert_eq!(INVALID_ARGUMENT, "INVALID_ARGUMENT");
262 assert_eq!(LINK_PERMISSION_DENIED, "LINK_PERMISSION_DENIED");
263 assert_eq!(LINK_REFLECTION_CYCLE, "LINK_REFLECTION_CYCLE");
264 assert_eq!(UNIQUE_CONFLICT, "UNIQUE_CONFLICT");
265 assert_eq!(STORE_BACKEND_UNAVAILABLE, "BACKEND_UNAVAILABLE");
269 assert_eq!(STORE_UNSUPPORTED_CAPABILITY, "UNSUPPORTED_CAPABILITY");
270 assert_eq!(STORE_OPERATION_FAILED, "STORE_OPERATION_FAILED");
271 assert_eq!(STORE_VERSION_CONFLICT, "VERSION_CONFLICT");
272 }
273
274 #[cfg(feature = "sal")]
285 #[test]
286 fn arch_9_store_error_slug_round_trip() {
287 use crate::store::{BoxBackendError, StoreError};
288 let variants = [
289 StoreError::NotFound { id: "x".into() },
290 StoreError::Conflict { id: "x".into() },
291 StoreError::PermissionDenied {
292 action: "a".into(),
293 target: "t".into(),
294 reason: "r".into(),
295 },
296 StoreError::BackendUnavailable {
297 backend: "b".into(),
298 detail: "d".into(),
299 },
300 StoreError::InvalidInput { detail: "d".into() },
301 StoreError::LinkRefused { detail: "d".into() },
302 StoreError::UnsupportedCapability {
303 capability: "c".into(),
304 },
305 StoreError::IntegrityFailed { detail: "d".into() },
306 StoreError::Backend(BoxBackendError::new("boom")),
307 ];
308 let expected = [
309 NOT_FOUND,
310 CONFLICT,
311 GOVERNANCE_REFUSED,
312 STORE_BACKEND_UNAVAILABLE,
313 VALIDATION_FAILED,
314 CONFLICT,
315 STORE_UNSUPPORTED_CAPABILITY,
316 STORE_OPERATION_FAILED,
317 DATABASE_ERROR,
318 ];
319 for (got, want) in variants.iter().zip(expected.iter()) {
320 assert_eq!(got.code(), *want, "ARCH-9 StoreError code drift");
321 }
322 }
323
324 #[test]
325 fn arch_9_storage_error_slug_round_trip() {
326 use crate::storage::{LinkEnd, StorageError};
329 let variants = [
330 StorageError::MemoryNotFound {
331 id: "x".into(),
332 role: None,
333 },
334 StorageError::MemoryNotFound {
335 id: "x".into(),
336 role: Some(LinkEnd::Source),
337 },
338 StorageError::PendingActionNotFound {
339 pending_id: "p".into(),
340 },
341 StorageError::AmbiguousIdPrefix {
342 prefix: "x".into(),
343 candidates: vec!["a".into(), "b".into()],
344 },
345 StorageError::InvalidArgument { reason: "r".into() },
346 StorageError::PendingActionStateInvalid {
347 pending_id: "p".into(),
348 status: "s".into(),
349 },
350 StorageError::LinkPermissionDenied { reason: "r".into() },
351 StorageError::LinkReflectionCycle {
352 source_id: "s".into(),
353 target_id: "t".into(),
354 },
355 StorageError::ApproverLaundering {
356 pending_id: "p".into(),
357 claimed: "a".into(),
358 requester: "b".into(),
359 },
360 StorageError::UniqueConflict { reason: "r".into() },
361 StorageError::ArchiveRestoreCollision { id: "x".into() },
362 StorageError::ArchiveSupersedeFailed {
363 archived_id: "x".into(),
364 },
365 StorageError::SqlcipherMissingPassphrase,
366 ];
367 let expected = [
368 NOT_FOUND,
369 NOT_FOUND,
370 PENDING_ACTION_NOT_FOUND,
371 AMBIGUOUS_ID_PREFIX,
372 INVALID_ARGUMENT,
373 PENDING_ACTION_STATE_INVALID,
374 LINK_PERMISSION_DENIED,
375 LINK_REFLECTION_CYCLE,
376 APPROVER_LAUNDERING,
377 UNIQUE_CONFLICT,
378 ARCHIVE_RESTORE_COLLISION,
379 ARCHIVE_SUPERSEDE_FAILED,
380 SQLCIPHER_MISSING_PASSPHRASE,
381 ];
382 for (got, want) in variants.iter().zip(expected.iter()) {
383 assert_eq!(got.code(), *want, "ARCH-9 StorageError code drift");
384 }
385 }
386}
387
388#[allow(dead_code)]
389#[derive(Debug, Serialize)]
390pub struct ApiError {
391 pub code: &'static str,
392 pub message: String,
393}
394
395#[allow(dead_code)]
396#[derive(Debug)]
397pub enum MemoryError {
398 NotFound(String),
399 ValidationFailed(String),
400 DatabaseError(String),
401 Conflict(String),
402 ReflectionDepthExceeded {
412 attempted: u32,
413 cap: u32,
414 namespace: String,
415 },
416 SynthesisDepthExceeded {
425 attempted: u32,
426 cap: u32,
427 namespace: String,
428 },
429 ReflectionCycleDetected {
438 source: String,
439 target: String,
440 cycle_path: Vec<String>,
441 },
442 RefusedByGovernance(String),
462 RefusedByGovernanceGate(crate::governance::GovernanceRefusal),
476}
477
478impl MemoryError {
479 pub fn code(&self) -> &'static str {
480 match self {
485 Self::NotFound(_) => error_codes::NOT_FOUND,
486 Self::ValidationFailed(_) => error_codes::VALIDATION_FAILED,
487 Self::DatabaseError(_) => error_codes::DATABASE_ERROR,
488 Self::Conflict(_) => error_codes::CONFLICT,
489 Self::ReflectionDepthExceeded { .. } => error_codes::REFLECTION_DEPTH_EXCEEDED,
490 Self::SynthesisDepthExceeded { .. } => error_codes::SYNTHESIS_DEPTH_EXCEEDED,
491 Self::ReflectionCycleDetected { .. } => error_codes::REFLECTION_CYCLE_DETECTED,
492 Self::RefusedByGovernance(_) | Self::RefusedByGovernanceGate(_) => {
493 error_codes::GOVERNANCE_REFUSED
494 }
495 }
496 }
497
498 pub fn status(&self) -> StatusCode {
499 match self {
500 Self::NotFound(_) => StatusCode::NOT_FOUND,
501 Self::ValidationFailed(_) => StatusCode::BAD_REQUEST,
502 Self::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
503 Self::Conflict(_)
507 | Self::ReflectionDepthExceeded { .. }
508 | Self::SynthesisDepthExceeded { .. }
509 | Self::ReflectionCycleDetected { .. } => StatusCode::CONFLICT,
510 Self::RefusedByGovernance(_) | Self::RefusedByGovernanceGate(_) => {
517 StatusCode::FORBIDDEN
518 }
519 }
520 }
521
522 pub fn message(&self) -> String {
523 match self {
524 Self::NotFound(m)
525 | Self::ValidationFailed(m)
526 | Self::DatabaseError(m)
527 | Self::Conflict(m) => m.clone(),
528 Self::ReflectionDepthExceeded {
529 attempted,
530 cap,
531 namespace,
532 } => format!(
533 "reflection depth {attempted} would exceed namespace \
534 max_reflection_depth {cap} (namespace='{namespace}')"
535 ),
536 Self::SynthesisDepthExceeded {
537 attempted,
538 cap,
539 namespace,
540 } => format!(
541 "synthesis depth {attempted} would exceed compiled \
542 max_synthesis_depth {cap} (namespace='{namespace}')"
543 ),
544 Self::ReflectionCycleDetected {
545 source,
546 target,
547 cycle_path,
548 } => format!(
549 "adding reflects_on edge {source} → {target} would create a cycle: {}",
550 cycle_path.join(" → ")
551 ),
552 Self::RefusedByGovernance(reason) => {
553 format!("write refused by substrate governance: {reason}")
554 }
555 Self::RefusedByGovernanceGate(refusal) => refusal.to_string(),
562 }
563 }
564}
565
566impl IntoResponse for MemoryError {
567 fn into_response(self) -> Response {
568 let body = ApiError {
569 code: self.code(),
570 message: self.message(),
571 };
572 (self.status(), Json(body)).into_response()
573 }
574}
575
576impl std::fmt::Display for MemoryError {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 write!(f, "[{}] {}", self.code(), self.message())
579 }
580}
581
582impl From<anyhow::Error> for MemoryError {
583 fn from(e: anyhow::Error) -> Self {
584 if let Some(refusal) = e.downcast_ref::<crate::storage::GovernanceRefusal>() {
593 return Self::RefusedByGovernance(refusal.reason.clone());
594 }
595 if let Some(refusal) = e.downcast_ref::<crate::governance::GovernanceRefusal>() {
604 return Self::RefusedByGovernanceGate(refusal.clone());
605 }
606 if let Some(se) = e.downcast_ref::<crate::storage::StorageError>() {
613 use crate::storage::StorageError as SE;
614 return match se {
615 SE::MemoryNotFound { .. } | SE::PendingActionNotFound { .. } => {
616 Self::NotFound(se.to_string())
617 }
618 SE::AmbiguousIdPrefix { .. } | SE::InvalidArgument { .. } => {
619 Self::ValidationFailed(se.to_string())
620 }
621 SE::PendingActionStateInvalid { .. }
622 | SE::UniqueConflict { .. }
623 | SE::ArchiveRestoreCollision { .. }
624 | SE::LinkReflectionCycle { .. } => Self::Conflict(se.to_string()),
625 SE::LinkPermissionDenied { .. } | SE::ApproverLaundering { .. } => {
626 Self::RefusedByGovernance(se.to_string())
627 }
628 SE::ArchiveSupersedeFailed { .. } | SE::SqlcipherMissingPassphrase => {
629 Self::DatabaseError(se.to_string())
630 }
631 };
632 }
633 Self::DatabaseError(e.to_string())
634 }
635}
636
637impl From<rusqlite::Error> for MemoryError {
638 fn from(e: rusqlite::Error) -> Self {
639 Self::DatabaseError(e.to_string())
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn error_codes() {
649 assert_eq!(MemoryError::NotFound("x".into()).code(), "NOT_FOUND");
650 assert_eq!(
651 MemoryError::ValidationFailed("x".into()).code(),
652 "VALIDATION_FAILED"
653 );
654 assert_eq!(
655 MemoryError::DatabaseError("x".into()).code(),
656 "DATABASE_ERROR"
657 );
658 assert_eq!(MemoryError::Conflict("x".into()).code(), "CONFLICT");
659 }
660
661 #[test]
662 fn error_status_codes() {
663 assert_eq!(
664 MemoryError::NotFound("x".into()).status(),
665 StatusCode::NOT_FOUND
666 );
667 assert_eq!(
668 MemoryError::ValidationFailed("x".into()).status(),
669 StatusCode::BAD_REQUEST
670 );
671 assert_eq!(
672 MemoryError::DatabaseError("x".into()).status(),
673 StatusCode::INTERNAL_SERVER_ERROR
674 );
675 assert_eq!(
676 MemoryError::Conflict("x".into()).status(),
677 StatusCode::CONFLICT
678 );
679 }
680
681 #[test]
682 fn error_messages() {
683 assert_eq!(
684 MemoryError::NotFound("not here".into()).message(),
685 "not here"
686 );
687 assert_eq!(
688 MemoryError::ValidationFailed("bad input".into()).message(),
689 "bad input"
690 );
691 }
692
693 #[test]
694 fn error_display() {
695 let err = MemoryError::NotFound("memory xyz".into());
696 let display = format!("{err}");
697 assert!(display.contains("NOT_FOUND"));
698 assert!(display.contains("memory xyz"));
699 }
700
701 #[test]
702 fn from_anyhow() {
703 let err: MemoryError = anyhow::anyhow!("db broke").into();
704 assert_eq!(err.code(), "DATABASE_ERROR");
705 assert!(err.message().contains("db broke"));
706 }
707
708 #[test]
716 fn from_anyhow_downcasts_governance_gate_refusal() {
717 use crate::governance::GovernanceRefusal;
718 use crate::models::{GovernanceLevel, GovernedAction};
719
720 let refusal = GovernanceRefusal::new(
721 GovernedAction::Store,
722 GovernanceLevel::Owner,
723 "ai:bob",
724 "caller 'ai:bob' is not the owner ('ai:alice')",
725 )
726 .with_namespace("team/prod")
727 .with_owner("ai:alice");
728 let anyhow_err = anyhow::Error::new(refusal.clone());
729
730 let mem_err: MemoryError = anyhow_err.into();
731 assert_eq!(mem_err.code(), "GOVERNANCE_REFUSED");
732 assert_eq!(mem_err.status(), StatusCode::FORBIDDEN);
733 assert_eq!(
734 mem_err.message(),
735 "store denied by governance: caller 'ai:bob' is not the owner ('ai:alice')",
736 );
737
738 match &mem_err {
739 MemoryError::RefusedByGovernanceGate(r) => {
740 assert_eq!(r.action, GovernedAction::Store);
741 assert_eq!(r.denied_level, GovernanceLevel::Owner);
742 assert_eq!(r.namespace.as_deref(), Some("team/prod"));
743 assert_eq!(r.owner.as_deref(), Some("ai:alice"));
744 assert_eq!(r.agent_id, "ai:bob");
745 assert_eq!(r, &refusal);
746 }
747 other => {
748 panic!("typed envelope must downcast to RefusedByGovernanceGate; got {other:?}")
749 }
750 }
751 }
752
753 #[test]
760 fn from_anyhow_preserves_pre_write_hook_refusal_variant() {
761 let hook_refusal = crate::storage::GovernanceRefusal {
762 reason: "rule R1 denies write".to_string(),
763 };
764 let anyhow_err = anyhow::Error::new(hook_refusal);
765
766 let mem_err: MemoryError = anyhow_err.into();
767 assert_eq!(mem_err.code(), "GOVERNANCE_REFUSED");
768 assert_eq!(mem_err.status(), StatusCode::FORBIDDEN);
769 assert_eq!(
773 mem_err.message(),
774 "write refused by substrate governance: rule R1 denies write",
775 );
776 assert!(
777 matches!(mem_err, MemoryError::RefusedByGovernance(_)),
778 "pre-write-hook refusal must map to RefusedByGovernance, not the new gate variant",
779 );
780 }
781
782 #[test]
783 fn api_error_serializes() {
784 let api_err = ApiError {
785 code: "TEST",
786 message: "test msg".into(),
787 };
788 let json = serde_json::to_value(&api_err).unwrap();
789 assert_eq!(json["code"], "TEST");
790 assert_eq!(json["message"], "test msg");
791 }
792
793 #[test]
798 fn error_display_validation() {
799 let err = MemoryError::ValidationFailed("bad input".into());
800 let s = format!("{err}");
801 assert!(s.contains("VALIDATION_FAILED"));
802 assert!(s.contains("bad input"));
803 }
804
805 #[test]
806 fn error_display_database() {
807 let err = MemoryError::DatabaseError("conn refused".into());
808 let s = format!("{err}");
809 assert!(s.contains("DATABASE_ERROR"));
810 assert!(s.contains("conn refused"));
811 }
812
813 #[test]
814 fn error_display_conflict() {
815 let err = MemoryError::Conflict("dup".into());
816 let s = format!("{err}");
817 assert!(s.contains("CONFLICT"));
818 assert!(s.contains("dup"));
819 }
820
821 #[test]
822 fn error_message_database_and_conflict() {
823 assert_eq!(MemoryError::DatabaseError("oops".into()).message(), "oops");
824 assert_eq!(MemoryError::Conflict("c".into()).message(), "c");
825 }
826
827 #[test]
828 fn from_rusqlite_error_maps_to_database() {
829 let rusqlite_err = rusqlite::Error::InvalidQuery;
830 let err: MemoryError = rusqlite_err.into();
831 assert_eq!(err.code(), "DATABASE_ERROR");
832 }
833
834 #[test]
835 fn into_response_carries_status_and_body() {
836 use axum::response::IntoResponse;
837 let err = MemoryError::NotFound("missing".into());
838 let resp = err.into_response();
839 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
840 }
841
842 #[test]
843 fn into_response_validation_status() {
844 use axum::response::IntoResponse;
845 let err = MemoryError::ValidationFailed("v".into());
846 let resp = err.into_response();
847 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
848 }
849
850 #[test]
851 fn into_response_database_status() {
852 use axum::response::IntoResponse;
853 let err = MemoryError::DatabaseError("d".into());
854 let resp = err.into_response();
855 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
856 }
857
858 #[test]
859 fn into_response_conflict_status() {
860 use axum::response::IntoResponse;
861 let err = MemoryError::Conflict("c".into());
862 let resp = err.into_response();
863 assert_eq!(resp.status(), StatusCode::CONFLICT);
864 }
865
866 #[test]
872 fn reflection_depth_exceeded_code() {
873 let err = MemoryError::ReflectionDepthExceeded {
874 attempted: 4,
875 cap: 3,
876 namespace: "ns/x".into(),
877 };
878 assert_eq!(err.code(), "REFLECTION_DEPTH_EXCEEDED");
879 }
880
881 #[test]
882 fn reflection_depth_exceeded_status_is_conflict() {
883 let err = MemoryError::ReflectionDepthExceeded {
884 attempted: 5,
885 cap: 3,
886 namespace: "ns/y".into(),
887 };
888 assert_eq!(err.status(), StatusCode::CONFLICT);
889 }
890
891 #[test]
892 fn reflection_depth_exceeded_message_contains_triple() {
893 let err = MemoryError::ReflectionDepthExceeded {
894 attempted: 7,
895 cap: 3,
896 namespace: "ai-memory/research".into(),
897 };
898 let msg = err.message();
899 assert!(msg.contains("7"));
900 assert!(msg.contains("3"));
901 assert!(msg.contains("ai-memory/research"));
902 assert!(msg.contains("max_reflection_depth"));
903 }
904
905 #[test]
906 fn reflection_depth_exceeded_display() {
907 let err = MemoryError::ReflectionDepthExceeded {
908 attempted: 4,
909 cap: 3,
910 namespace: "ns".into(),
911 };
912 let s = format!("{err}");
913 assert!(s.contains("REFLECTION_DEPTH_EXCEEDED"));
914 assert!(s.contains("max_reflection_depth"));
915 }
916
917 #[test]
918 fn reflection_depth_exceeded_into_response_is_conflict() {
919 use axum::response::IntoResponse;
920 let err = MemoryError::ReflectionDepthExceeded {
921 attempted: 4,
922 cap: 3,
923 namespace: "ns".into(),
924 };
925 let resp = err.into_response();
926 assert_eq!(resp.status(), StatusCode::CONFLICT);
927 }
928
929 #[test]
936 fn synthesis_depth_exceeded_code() {
937 let err = MemoryError::SynthesisDepthExceeded {
938 attempted: 4,
939 cap: 3,
940 namespace: "ns/x".into(),
941 };
942 assert_eq!(err.code(), "SYNTHESIS_DEPTH_EXCEEDED");
943 }
944
945 #[test]
946 fn synthesis_depth_exceeded_status_is_conflict() {
947 let err = MemoryError::SynthesisDepthExceeded {
948 attempted: 5,
949 cap: 3,
950 namespace: "ns/y".into(),
951 };
952 assert_eq!(err.status(), StatusCode::CONFLICT);
953 }
954
955 #[test]
956 fn synthesis_depth_exceeded_message_contains_triple() {
957 let err = MemoryError::SynthesisDepthExceeded {
958 attempted: 7,
959 cap: 3,
960 namespace: "ai-memory/research".into(),
961 };
962 let msg = err.message();
963 assert!(msg.contains("7"));
964 assert!(msg.contains("3"));
965 assert!(msg.contains("ai-memory/research"));
966 assert!(msg.contains("max_synthesis_depth"));
967 }
968
969 #[test]
970 fn synthesis_depth_exceeded_display() {
971 let err = MemoryError::SynthesisDepthExceeded {
972 attempted: 4,
973 cap: 3,
974 namespace: "ns".into(),
975 };
976 let s = format!("{err}");
977 assert!(s.contains("SYNTHESIS_DEPTH_EXCEEDED"));
978 assert!(s.contains("max_synthesis_depth"));
979 }
980
981 #[test]
987 fn reflection_cycle_detected_code() {
988 let err = MemoryError::ReflectionCycleDetected {
989 source: "uuid-A".into(),
990 target: "uuid-B".into(),
991 cycle_path: vec!["uuid-B".into(), "uuid-C".into(), "uuid-A".into()],
992 };
993 assert_eq!(err.code(), "REFLECTION_CYCLE_DETECTED");
994 }
995
996 #[test]
997 fn reflection_cycle_detected_status_is_conflict() {
998 let err = MemoryError::ReflectionCycleDetected {
999 source: "src".into(),
1000 target: "dst".into(),
1001 cycle_path: vec!["dst".into(), "src".into()],
1002 };
1003 assert_eq!(err.status(), StatusCode::CONFLICT);
1004 }
1005
1006 #[test]
1007 fn reflection_cycle_detected_message_contains_path() {
1008 let err = MemoryError::ReflectionCycleDetected {
1009 source: "uuid-A".into(),
1010 target: "uuid-B".into(),
1011 cycle_path: vec!["uuid-B".into(), "uuid-C".into(), "uuid-A".into()],
1012 };
1013 let msg = err.message();
1014 assert!(
1015 msg.contains("uuid-A"),
1016 "expected source UUID in message, got: {msg}"
1017 );
1018 assert!(
1019 msg.contains("uuid-B"),
1020 "expected target UUID in message, got: {msg}"
1021 );
1022 assert!(
1023 msg.contains("uuid-C"),
1024 "expected cycle path intermediate in message, got: {msg}"
1025 );
1026 assert!(
1027 msg.contains("cycle"),
1028 "expected cycle context in message, got: {msg}"
1029 );
1030 }
1031
1032 #[test]
1033 fn reflection_cycle_detected_display_includes_code() {
1034 let err = MemoryError::ReflectionCycleDetected {
1035 source: "s".into(),
1036 target: "t".into(),
1037 cycle_path: vec!["t".into(), "s".into()],
1038 };
1039 let s = format!("{err}");
1040 assert!(
1041 s.contains("REFLECTION_CYCLE_DETECTED"),
1042 "Display should include code prefix; got: {s}"
1043 );
1044 assert!(
1045 s.contains("cycle"),
1046 "Display should describe the cycle; got: {s}"
1047 );
1048 }
1049
1050 #[test]
1051 fn reflection_cycle_detected_into_response_is_conflict() {
1052 use axum::response::IntoResponse;
1053 let err = MemoryError::ReflectionCycleDetected {
1054 source: "s".into(),
1055 target: "t".into(),
1056 cycle_path: vec!["t".into(), "s".into()],
1057 };
1058 let resp = err.into_response();
1059 assert_eq!(resp.status(), StatusCode::CONFLICT);
1060 }
1061
1062 #[test]
1068 fn refused_by_governance_code() {
1069 let err = MemoryError::RefusedByGovernance("blocked".into());
1070 assert_eq!(err.code(), "GOVERNANCE_REFUSED");
1071 }
1072
1073 #[test]
1074 fn refused_by_governance_status_is_forbidden() {
1075 let err = MemoryError::RefusedByGovernance("blocked".into());
1076 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1077 }
1078
1079 #[test]
1080 fn refused_by_governance_message_contains_reason() {
1081 let err = MemoryError::RefusedByGovernance("secrets namespace is read-only".into());
1082 let msg = err.message();
1083 assert!(
1084 msg.contains("secrets namespace is read-only"),
1085 "expected reason in message, got: {msg}"
1086 );
1087 assert!(
1088 msg.contains("substrate governance"),
1089 "expected refusal context in message, got: {msg}"
1090 );
1091 }
1092
1093 #[test]
1094 fn refused_by_governance_display_includes_code_and_reason() {
1095 let err = MemoryError::RefusedByGovernance("rule R042 fired".into());
1096 let s = format!("{err}");
1097 assert!(s.contains("GOVERNANCE_REFUSED"));
1098 assert!(s.contains("rule R042 fired"));
1099 }
1100
1101 #[test]
1102 fn refused_by_governance_into_response_is_forbidden() {
1103 use axum::response::IntoResponse;
1104 let err = MemoryError::RefusedByGovernance("nope".into());
1105 let resp = err.into_response();
1106 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1107 }
1108
1109 #[test]
1110 fn from_anyhow_promotes_governance_refusal() {
1111 let refusal = crate::storage::GovernanceRefusal {
1116 reason: "test reason".to_string(),
1117 };
1118 let any_err: anyhow::Error = anyhow::Error::new(refusal);
1119 let mapped: MemoryError = any_err.into();
1120 match mapped {
1121 MemoryError::RefusedByGovernance(r) => assert_eq!(r, "test reason"),
1122 other => panic!("expected RefusedByGovernance, got {other:?}"),
1123 }
1124 }
1125
1126 #[test]
1127 fn from_anyhow_unrelated_falls_through_to_database_error() {
1128 let any_err = anyhow::anyhow!("plain old db failure");
1131 let mapped: MemoryError = any_err.into();
1132 assert_eq!(mapped.code(), "DATABASE_ERROR");
1133 }
1134
1135 fn map_storage(se: crate::storage::StorageError) -> MemoryError {
1143 let any_err: anyhow::Error = anyhow::Error::new(se);
1144 MemoryError::from(any_err)
1145 }
1146
1147 #[test]
1148 fn from_anyhow_storage_memory_not_found_maps_to_notfound() {
1149 let mapped = map_storage(crate::storage::StorageError::MemoryNotFound {
1150 id: "m1".into(),
1151 role: None,
1152 });
1153 assert_eq!(mapped.code(), "NOT_FOUND");
1154 assert_eq!(mapped.status(), StatusCode::NOT_FOUND);
1155 assert!(mapped.message().contains("memory not found"));
1156 }
1157
1158 #[test]
1159 fn from_anyhow_storage_pending_action_not_found_maps_to_notfound() {
1160 let mapped = map_storage(crate::storage::StorageError::PendingActionNotFound {
1161 pending_id: "pa-1".into(),
1162 });
1163 assert_eq!(mapped.status(), StatusCode::NOT_FOUND);
1164 assert!(mapped.message().contains("pending action not found"));
1165 }
1166
1167 #[test]
1168 fn from_anyhow_storage_ambiguous_id_prefix_maps_to_validation() {
1169 let mapped = map_storage(crate::storage::StorageError::AmbiguousIdPrefix {
1170 prefix: "ab".into(),
1171 candidates: vec!["abc1".into(), "abc2".into()],
1172 });
1173 assert_eq!(mapped.code(), "VALIDATION_FAILED");
1174 assert_eq!(mapped.status(), StatusCode::BAD_REQUEST);
1175 assert!(mapped.message().contains("ambiguous ID prefix"));
1178 }
1179
1180 #[test]
1181 fn from_anyhow_storage_invalid_argument_maps_to_validation() {
1182 let mapped = map_storage(crate::storage::StorageError::InvalidArgument {
1183 reason: "max_depth must be >= 1".into(),
1184 });
1185 assert_eq!(mapped.status(), StatusCode::BAD_REQUEST);
1186 assert_eq!(mapped.message(), "max_depth must be >= 1");
1187 }
1188
1189 #[test]
1190 fn from_anyhow_storage_pending_action_state_invalid_maps_to_conflict() {
1191 let mapped = map_storage(crate::storage::StorageError::PendingActionStateInvalid {
1192 pending_id: "pa-9".into(),
1193 status: "rejected".into(),
1194 });
1195 assert_eq!(mapped.code(), "CONFLICT");
1196 assert_eq!(mapped.status(), StatusCode::CONFLICT);
1197 }
1198
1199 #[test]
1200 fn from_anyhow_storage_unique_conflict_maps_to_conflict() {
1201 let mapped = map_storage(crate::storage::StorageError::UniqueConflict {
1202 reason: "title 'X' already exists".into(),
1203 });
1204 assert_eq!(mapped.status(), StatusCode::CONFLICT);
1205 }
1206
1207 #[test]
1208 fn from_anyhow_storage_archive_restore_collision_maps_to_conflict() {
1209 let mapped =
1210 map_storage(crate::storage::StorageError::ArchiveRestoreCollision { id: "m1".into() });
1211 assert_eq!(mapped.status(), StatusCode::CONFLICT);
1212 assert!(mapped.message().contains("already exists in active table"));
1213 }
1214
1215 #[test]
1216 fn from_anyhow_storage_link_reflection_cycle_maps_to_conflict() {
1217 let mapped = map_storage(crate::storage::StorageError::LinkReflectionCycle {
1218 source_id: "a".into(),
1219 target_id: "b".into(),
1220 });
1221 assert_eq!(mapped.status(), StatusCode::CONFLICT);
1224 assert!(
1225 mapped
1226 .message()
1227 .starts_with(crate::storage::LINK_CYCLE_ERR_PREFIX),
1228 "wire body must preserve the canonical cycle prefix"
1229 );
1230 }
1231
1232 #[test]
1233 fn from_anyhow_storage_link_permission_denied_maps_to_governance() {
1234 let mapped = map_storage(crate::storage::StorageError::LinkPermissionDenied {
1235 reason: "rule R7".into(),
1236 });
1237 assert_eq!(mapped.code(), "GOVERNANCE_REFUSED");
1238 assert_eq!(mapped.status(), StatusCode::FORBIDDEN);
1239 assert!(
1247 mapped
1248 .message()
1249 .contains(crate::storage::LINK_PERMISSION_DENIED_ERR_PREFIX),
1250 "wire body must preserve the canonical denial prefix as a substring"
1251 );
1252 }
1253
1254 #[test]
1255 fn from_anyhow_storage_approver_laundering_maps_to_governance() {
1256 let mapped = map_storage(crate::storage::StorageError::ApproverLaundering {
1257 pending_id: "pa-1".into(),
1258 claimed: "x".into(),
1259 requester: "y".into(),
1260 });
1261 assert_eq!(mapped.status(), StatusCode::FORBIDDEN);
1262 assert!(mapped.message().contains("approver-on-behalf laundering"));
1263 }
1264
1265 #[test]
1266 fn from_anyhow_storage_archive_supersede_failed_maps_to_database_error() {
1267 let mapped = map_storage(crate::storage::StorageError::ArchiveSupersedeFailed {
1268 archived_id: "arch-1".into(),
1269 });
1270 assert_eq!(mapped.code(), "DATABASE_ERROR");
1271 assert_eq!(mapped.status(), StatusCode::INTERNAL_SERVER_ERROR);
1272 }
1273
1274 #[test]
1275 fn from_anyhow_storage_sqlcipher_missing_passphrase_maps_to_database_error() {
1276 let mapped = map_storage(crate::storage::StorageError::SqlcipherMissingPassphrase);
1277 assert_eq!(mapped.code(), "DATABASE_ERROR");
1278 assert!(mapped.message().contains("AI_MEMORY_DB_PASSPHRASE"));
1279 }
1280
1281 #[test]
1292 fn msg_invalid_and_error_line_shapes() {
1293 assert_eq!(msg::invalid("limit", "boom"), "invalid limit: boom");
1294 assert_eq!(msg::error_line("boom"), "error: boom");
1295 }
1296
1297 #[test]
1298 fn msg_not_found_family_shapes() {
1299 assert_eq!(
1300 msg::memory_not_found("m-1"),
1301 format!("{}: m-1", msg::MEMORY_NOT_FOUND)
1302 );
1303 assert_eq!(
1304 msg::skill_not_found("sk-1"),
1305 format!("{}: sk-1", msg::SKILL_NOT_FOUND)
1306 );
1307 assert_eq!(
1308 msg::pending_action_not_found("pa-1"),
1309 "pending action not found: pa-1"
1310 );
1311 assert_eq!(msg::not_found("x-9"), "not found: x-9");
1312 }
1313
1314 #[test]
1315 fn msg_governance_and_quota_shapes() {
1316 assert_eq!(
1317 msg::approve_rejected("consensus pending"),
1318 "approve rejected: consensus pending"
1319 );
1320 assert_eq!(
1321 msg::older_than_days_negative(-3),
1322 "older_than_days must be non-negative (got -3)"
1323 );
1324 }
1325
1326 #[test]
1327 fn msg_transport_error_shapes() {
1328 assert_eq!(
1329 msg::signature_verify_failed("bad sig"),
1330 "signature verify failed: bad sig"
1331 );
1332 assert_eq!(
1333 msg::zstd_decompress_body("truncated"),
1334 "zstd decompress body: truncated"
1335 );
1336 assert_eq!(msg::network("timeout"), "network: timeout");
1337 assert_eq!(msg::unsubscribe("missing id"), "unsubscribe: missing id");
1338 }
1339
1340 #[test]
1341 fn msg_fs_context_label_shapes() {
1342 assert_eq!(msg::opening("/a/b.toml"), "opening /a/b.toml");
1343 assert_eq!(msg::reading("/a/b.toml"), "reading /a/b.toml");
1344 assert_eq!(msg::writing("/a/b.toml"), "writing /a/b.toml");
1345 }
1346
1347 #[test]
1348 fn from_anyhow_storage_governance_refusal_still_wins_when_chained() {
1349 let refusal = crate::storage::GovernanceRefusal {
1354 reason: "policy".into(),
1355 };
1356 let mapped: MemoryError = anyhow::Error::new(refusal).into();
1357 assert_eq!(mapped.code(), "GOVERNANCE_REFUSED");
1358 }
1359}