use super::*;
fn base_note() -> Note {
Note::new("ns:test", "memory", "hello world")
}
#[test]
fn note_key_is_optional_and_unkeyed_wire_shape_is_unchanged() {
let note = base_note();
assert_eq!(note.key, None);
let legacy = serde_json::to_value(¬e).unwrap();
assert!(legacy.get("key").is_none());
assert_eq!(serde_json::from_value::<Note>(legacy).unwrap(), note);
let mut keyed = note;
keyed.key = Some("operation-1".to_string());
let encoded = serde_json::to_value(&keyed).unwrap();
assert_eq!(encoded["key"], "operation-1");
assert_eq!(serde_json::from_value::<Note>(encoded).unwrap(), keyed);
}
#[test]
fn with_salience_clamps_to_range() {
let n = base_note().with_salience(1.5);
assert_eq!(n.salience, Some(1.0));
let n = base_note().with_salience(-0.1);
assert_eq!(n.salience, Some(0.0));
let n = base_note().with_salience(0.7);
assert_eq!(n.salience, Some(0.7));
}
#[test]
fn with_salience_ignores_nan() {
let n = base_note().with_salience(f64::NAN);
assert_eq!(n.salience, None, "NaN must not set salience");
}
#[test]
fn with_salience_ignores_inf() {
let n = base_note().with_salience(f64::INFINITY);
assert_eq!(n.salience, None, "+Inf must not set salience");
let n = base_note().with_salience(f64::NEG_INFINITY);
assert_eq!(n.salience, None, "-Inf must not set salience");
}
#[test]
fn with_decay_floors_at_zero() {
let n = base_note().with_decay(-1.0);
assert_eq!(n.decay_factor, Some(0.0));
let n = base_note().with_decay(0.5);
assert_eq!(n.decay_factor, Some(0.5));
}
#[test]
fn with_decay_ignores_nan() {
let n = base_note().with_decay(f64::NAN);
assert_eq!(n.decay_factor, None, "NaN must not set decay_factor");
}
#[test]
fn with_decay_ignores_inf() {
let n = base_note().with_decay(f64::INFINITY);
assert_eq!(n.decay_factor, None, "+Inf must not set decay_factor");
}
#[test]
fn try_with_salience_accepts_valid_range() {
let n = base_note().try_with_salience(0.0).unwrap();
assert_eq!(n.salience, Some(0.0));
let n = base_note().try_with_salience(1.0).unwrap();
assert_eq!(n.salience, Some(1.0));
let n = base_note().try_with_salience(0.85).unwrap();
assert_eq!(n.salience, Some(0.85));
}
#[test]
fn try_with_salience_rejects_nan() {
let err = base_note().try_with_salience(f64::NAN).unwrap_err();
assert!(err.contains("finite"), "error must mention finite: {err}");
}
#[test]
fn try_with_salience_rejects_out_of_range() {
let err = base_note().try_with_salience(1.1).unwrap_err();
assert!(err.contains("1.0"), "error must mention bound: {err}");
let err = base_note().try_with_salience(-0.01).unwrap_err();
assert!(err.contains("0.0"), "error must mention bound: {err}");
}
#[test]
fn try_with_decay_accepts_valid_values() {
let n = base_note().try_with_decay(0.0).unwrap();
assert_eq!(n.decay_factor, Some(0.0));
let n = base_note().try_with_decay(2.5).unwrap();
assert_eq!(n.decay_factor, Some(2.5));
}
#[test]
fn try_with_decay_rejects_nan() {
let err = base_note().try_with_decay(f64::NAN).unwrap_err();
assert!(err.contains("finite"), "error must mention finite: {err}");
}
#[test]
fn try_with_decay_rejects_negative() {
let err = base_note().try_with_decay(-0.1).unwrap_err();
assert!(err.contains("0.0"), "error must mention bound: {err}");
}
#[derive(Default)]
struct DefaultOnlyNoteStore {
query_calls: std::sync::atomic::AtomicUsize,
}
impl DefaultOnlyNoteStore {
fn query_call_count(&self) -> usize {
self.query_calls.load(std::sync::atomic::Ordering::SeqCst)
}
}
fn unsupported<T>(operation: &'static str) -> StorageResult<T> {
Err(crate::StorageError::Unsupported {
capability: crate::StorageCapability::Notes,
operation: operation.into(),
message: "test double does not implement this operation".into(),
})
}
#[async_trait]
impl NoteStore for DefaultOnlyNoteStore {
async fn upsert_note(&self, _note: Note) -> StorageResult<()> {
unsupported("upsert_note")
}
async fn upsert_notes(&self, _notes: Vec<Note>) -> StorageResult<BatchWriteSummary> {
unsupported("upsert_notes")
}
async fn get_note(&self, _id: Uuid) -> StorageResult<Option<Note>> {
unsupported("get_note")
}
async fn get_note_including_deleted(&self, _id: Uuid) -> StorageResult<Option<Note>> {
unsupported("get_note_including_deleted")
}
async fn delete_note(&self, _id: Uuid, _mode: DeleteMode) -> StorageResult<bool> {
unsupported("delete_note")
}
async fn update_note_properties(
&self,
_id: Uuid,
_properties: Option<Value>,
_updated_at: i64,
) -> StorageResult<bool> {
unsupported("update_note_properties")
}
async fn set_note_property(
&self,
_id: Uuid,
_key: &str,
_value: Value,
_updated_at: i64,
) -> StorageResult<bool> {
unsupported("set_note_property")
}
async fn try_patch_note_property(
&self,
_id: Uuid,
_namespace: &str,
_filter: &NoteFilter,
_json_path: &str,
_value: Value,
_updated_at: i64,
) -> StorageResult<bool> {
unsupported("try_patch_note_property")
}
async fn query_notes(
&self,
_namespace: &str,
_kind: Option<&str>,
_page: PageRequest,
) -> StorageResult<Page<Note>> {
self.query_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Page {
items: Vec::new(),
total: Some(0),
})
}
async fn query_notes_filtered(
&self,
_namespace: &str,
_filter: &NoteFilter,
_page: PageRequest,
) -> StorageResult<Page<Note>> {
self.query_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Page {
items: Vec::new(),
total: Some(0),
})
}
async fn query_notes_filtered_bounded(
&self,
_namespace: &str,
_filter: &NoteFilter,
_max_rows: u32,
) -> StorageResult<Vec<Note>> {
unsupported("query_notes_filtered_bounded")
}
async fn count_notes(&self, _namespace: &str, _kind: Option<&str>) -> StorageResult<u64> {
unsupported("count_notes")
}
async fn try_insert_note(&self, _note: Note) -> StorageResult<bool> {
unsupported("try_insert_note")
}
}
#[tokio::test]
async fn default_snapshot_count_is_fail_closed_without_query_fallback() {
let store = DefaultOnlyNoteStore::default();
let result = store
.count_notes_filtered_in_snapshot(
"ns:test",
&[NoteFilter::default(), NoteFilter::default()],
)
.await;
assert!(
matches!(
result,
Err(crate::StorageError::Unsupported { ref operation, .. })
if operation == "count_notes_filtered_in_snapshot"
),
"inherited snapshot count must fail closed, got {result:?}"
);
assert_eq!(
store.query_call_count(),
0,
"inherited snapshot count must not fall back to independent queries"
);
}
#[tokio::test]
async fn count_free_page_default_is_fail_closed_without_exact_query_fallback() {
let store = DefaultOnlyNoteStore::default();
let result = store
.query_notes_filtered_count_free("ns:test", &NoteFilter::default(), PageRequest::default())
.await;
assert!(
matches!(
result,
Err(crate::StorageError::Unsupported { ref operation, .. })
if operation == "query_notes_filtered_count_free"
),
"inherited count-free query must fail closed, got {result:?}"
);
assert_eq!(
store.query_call_count(),
0,
"count-free default must not invoke the exact-count page"
);
}
#[tokio::test]
async fn unfiltered_count_free_page_default_is_fail_closed_without_exact_query_fallback() {
let store = DefaultOnlyNoteStore::default();
let result = store
.query_notes_count_free("ns:test", Some("message"), PageRequest::default())
.await;
assert!(
matches!(
result,
Err(crate::StorageError::Unsupported { ref operation, .. })
if operation == "query_notes_count_free"
),
"inherited count-free query must fail closed, got {result:?}"
);
assert_eq!(
store.query_call_count(),
0,
"count-free default must not invoke the exact-count page"
);
}
#[tokio::test]
async fn bounded_snapshot_count_default_is_fail_closed_without_query_fallback() {
let store = DefaultOnlyNoteStore::default();
let result = store
.count_notes_filtered_bounded_in_snapshot(
"ns:test",
&[NoteFilter::default(), NoteFilter::default()],
1_000,
)
.await;
assert!(
matches!(
result,
Err(crate::StorageError::Unsupported { ref operation, .. })
if operation == "count_notes_filtered_bounded_in_snapshot"
),
"inherited bounded snapshot count must fail closed, got {result:?}"
);
assert_eq!(
store.query_call_count(),
0,
"bounded snapshot count must not compose independent page queries"
);
}