Resolves every finding in docs/reviews/M0-M10-code-review-2026-06-08-pass2.md across the engine, network, server, and CLI crates: session restore, replication/CRDT, WAL format and recovery, storage indexes, query/ranking executors, cohort/community governance, and scatter-gather routing. Adds regression tests: - review_pass2_creator_search_filter - review_pass2_d_replication - review_pass2_query_for_session - review_pass2_storage_indexes_bitmap_cache - review_pass2_zone_a_sessions Verified: cargo clippy -D warnings and full test suite green across all crates.
91 lines
2.9 KiB
Rust
91 lines
2.9 KiB
Rust
#![allow(clippy::unwrap_used, clippy::doc_markdown)]
|
||
//! M0–M10 review pass 2, zone C regression: FOR SESSION missing-session handling
|
||
//! must NOT diverge between RETRIEVE and SEARCH.
|
||
//!
|
||
//! A RETRIEVE/SEARCH with `FOR SESSION <id>` pointing at an expired/evicted
|
||
//! session is a WELL-FORMED query. Per CODING_GUIDELINES §6 ("graceful
|
||
//! degradation, never failure") the engine must execute it WITHOUT the session
|
||
//! boost rather than hard-erroring. Before the fix RETRIEVE returned
|
||
//! `Err(SessionNotFound)` while the structurally-identical SEARCH degraded — a
|
||
//! confusing surface-specific outage the moment a session was swept. This test
|
||
//! proves both surfaces now degrade identically.
|
||
|
||
use std::{collections::HashMap, time::Duration};
|
||
|
||
use tidaldb::{
|
||
SessionId, TidalDb,
|
||
query::{retrieve::Retrieve, search::Search},
|
||
schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
|
||
};
|
||
|
||
fn test_schema() -> tidaldb::schema::Schema {
|
||
let mut builder = SchemaBuilder::new();
|
||
let _ = builder
|
||
.signal(
|
||
"view",
|
||
EntityKind::Item,
|
||
DecaySpec::Exponential {
|
||
half_life: Duration::from_secs(7 * 24 * 3600),
|
||
},
|
||
)
|
||
.windows(&[Window::AllTime])
|
||
.add();
|
||
builder.build().unwrap()
|
||
}
|
||
|
||
fn test_db() -> TidalDb {
|
||
TidalDb::builder()
|
||
.ephemeral()
|
||
.with_schema(test_schema())
|
||
.open()
|
||
.unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn retrieve_and_search_degrade_identically_on_missing_session() {
|
||
let db = test_db();
|
||
|
||
// Seed one item with a `view` signal so both queries have something to rank.
|
||
let now = Timestamp::now();
|
||
db.signal("view", EntityId::new(1), 1.0, now).unwrap();
|
||
let mut meta = HashMap::new();
|
||
meta.insert("title".to_string(), "jazz piano".to_string());
|
||
db.write_item_with_metadata(EntityId::new(1), &meta)
|
||
.unwrap();
|
||
|
||
// A session id that was never started (the "swept session" case).
|
||
let missing = SessionId::from_raw(9_999_999);
|
||
|
||
// RETRIEVE FOR SESSION <missing> must NOT error — it degrades to no-boost.
|
||
let retrieve = Retrieve::builder()
|
||
.profile("new")
|
||
.for_session(missing)
|
||
.limit(10)
|
||
.build()
|
||
.unwrap();
|
||
let r = db.retrieve(&retrieve);
|
||
assert!(
|
||
r.is_ok(),
|
||
"RETRIEVE with a missing session must degrade gracefully, got {:?}",
|
||
r.err()
|
||
);
|
||
|
||
// SEARCH FOR SESSION <missing> must likewise degrade (this already worked;
|
||
// asserted here to lock the two surfaces together).
|
||
let search = Search::builder()
|
||
.query("jazz")
|
||
.using_profile("search")
|
||
.for_session(missing)
|
||
.limit(10)
|
||
.build()
|
||
.unwrap();
|
||
let s = db.search(&search);
|
||
assert!(
|
||
s.is_ok(),
|
||
"SEARCH with a missing session must degrade gracefully, got {:?}",
|
||
s.err()
|
||
);
|
||
|
||
db.close().unwrap();
|
||
}
|