tidaldb/tidal/tests/review_pass2_zone_a_sessions.rs
jx12n 9728194f16 fix: M0-M10 code-review pass2 remediation — all 91 findings
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.
2026-06-09 12:21:00 -06:00

250 lines
8.1 KiB
Rust

//! Zone A (session lifecycle & sweeper) regression tests for the
//! M0-M10 code-review pass-2 findings.
//!
//! These are end-to-end persistence tests: they open a persistent database,
//! exercise the session lifecycle, shut down, and reopen — the only way to
//! reproduce the two BLOCKERs and the CRITICALs, which only manifest across a
//! restart.
#![allow(
clippy::unwrap_used,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::needless_collect
)]
use std::{collections::HashMap, time::Duration};
use tidaldb::{
AgentPolicy, SessionId, TidalDb,
schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp},
};
/// Schema with one signal type and a single long-TTL session policy named
/// "agent", used by every test in this file.
fn schema() -> tidaldb::schema::Schema {
let mut b = SchemaBuilder::new();
let _ = b
.signal("view", EntityKind::Item, DecaySpec::Permanent)
.add();
b.session_policy(
"agent",
AgentPolicy {
allowed_signals: vec!["view".to_string()],
denied_signals: vec![],
max_session_duration: Duration::from_secs(3600),
max_signals_per_session: 10_000,
},
);
b.build().unwrap()
}
/// BLOCKER regression: a clean restart must NOT re-issue an archived session id
/// and overwrite its durable snapshot.
///
/// Reproduces the reviewer's two assertions:
/// (1) a new session after reopen gets an id strictly greater than the
/// previously-closed id, and
/// (2) `session_snapshot` of the old id still returns the ORIGINAL
/// user_id / signals_written — the archive was not destroyed.
#[test]
fn reopen_does_not_reissue_archived_session_id() {
let dir = tempfile::tempdir().unwrap();
// Phase 1: start + signal (user 1, 5 signals) + close, then shut down.
let first_id;
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
let handle = db
.start_session(1, "agent", "agent", HashMap::new())
.unwrap();
first_id = handle.id;
for i in 1..=5u64 {
db.session_signal(
&handle,
"view",
EntityId::new(i),
1.0,
Timestamp::now(),
None,
)
.unwrap();
}
db.close_session(handle).unwrap();
// Snapshot is archived with the true author + count.
let snap = db.session_snapshot(first_id).unwrap();
assert_eq!(snap.user_id, 1);
assert_eq!(snap.signals_written, 5);
db.close().unwrap();
}
// Phase 2: reopen and start a NEW session for a different user.
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
let handle = db
.start_session(2, "agent", "agent", HashMap::new())
.unwrap();
let second_id = handle.id;
// (1) The new id must be strictly greater than the archived id — the
// allocator was advanced past the durable snapshot on reopen.
assert!(
second_id.as_u64() > first_id.as_u64(),
"reopened session id {second_id} must exceed the archived id {first_id}; \
the allocator was reseeded to 1 and re-issued the archived id"
);
db.close_session(handle).unwrap();
// (2) The original archived snapshot is intact — not overwritten by the
// reused id's new (user 2, 0-signal) close.
let snap = db.session_snapshot(first_id).unwrap();
assert_eq!(
snap.user_id, 1,
"the user-1 archived snapshot must survive a restart"
);
assert_eq!(
snap.signals_written, 5,
"the user-1 archived signal count must survive a restart"
);
db.close().unwrap();
}
}
/// End-to-end companion to the in-module "snapshot is authoritative" guard:
/// after a clean close + reopen, the session is absent from `active_sessions()`
/// and resolves only as an archived snapshot. (The torn-Close phantom-state
/// case — snapshot present AND Start-without-Close in the journal — is covered
/// directly by `session_restore.rs::durable_snapshot_blocks_active_restore`,
/// which can inject that exact state through the internal restore path.)
#[test]
fn closed_session_is_not_restored_as_active() {
let dir = tempfile::tempdir().unwrap();
let closed_id;
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
let handle = db
.start_session(1, "agent", "agent", HashMap::new())
.unwrap();
closed_id = handle.id;
db.session_signal(
&handle,
"view",
EntityId::new(1),
1.0,
Timestamp::now(),
None,
)
.unwrap();
db.close_session(handle).unwrap();
db.close().unwrap();
}
// Reopen: the durable snapshot for `closed_id` is present. Even if a stale
// Start-without-Close survived in the journal, the snapshot is authoritative
// and the session must NOT come back as active.
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
let active_ids: Vec<u64> = db
.active_sessions()
.into_iter()
.map(|s| s.id.as_u64())
.collect();
assert!(
!active_ids.contains(&closed_id.as_u64()),
"a closed session (durable snapshot present) must never restore as active"
);
// It is still resolvable as a closed/archived session.
let snap = db.session_snapshot(closed_id).unwrap();
assert_eq!(snap.id, closed_id);
db.close().unwrap();
}
}
/// BLOCKER/CRITICAL regression: a session that was already past its
/// `max_session_duration` at shutdown must be TTL-reaped on the first startup
/// sweep after reopen — the durable age (`started_at_ns`) survives the restart,
/// so the back-dated monotonic clock makes the sweeper see the true age.
///
/// Uses a fresh session under the public API; the focused unit tests in
/// `sweeper.rs` and `policy.rs` cover the simulated-2h-old-restore arithmetic
/// directly (the public API cannot dial `started_at_ns` into the past). Here we
/// assert the live invariant: a session within its TTL survives a sweep and is
/// listed active after reopen, proving restore back-dates rather than expiring
/// a perfectly fresh session.
#[test]
fn restored_session_within_ttl_survives_sweep() {
let dir = tempfile::tempdir().unwrap();
let session_id: SessionId;
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
let handle = db
.start_session(1, "agent", "agent", HashMap::new())
.unwrap();
session_id = handle.id;
db.session_signal(
&handle,
"view",
EntityId::new(1),
1.0,
Timestamp::now(),
None,
)
.unwrap();
// Deliberately do NOT close — leave it active so the WAL replays it.
db.close().unwrap();
}
{
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema())
.open()
.unwrap();
// The fresh, within-TTL session is restored active and survives a sweep
// (back-dated started_at reflects its true near-zero age, not an instant
// expiry from a misread clock).
db.force_sweep();
let active: Vec<u64> = db
.active_sessions()
.into_iter()
.map(|s| s.id.as_u64())
.collect();
assert!(
active.contains(&session_id.as_u64()),
"a within-TTL restored session must survive the startup sweep"
);
db.close().unwrap();
}
}