Resolves the 142 findings from tidal/docs/reviews/CODE_REVIEW_m0-m10.md across the engine, server, net, and CLI surfaces: - WAL/session-journal durability, checkpoint format, and crash-recovery hardening - Replication shipper/receiver, tenant isolation, and migration paths - Cluster scatter-gather, router, standalone server + health/offload endpoints - tidalctl refactored into command modules with JSON output and WAL-state tooling - Cohort, governance, signal-ledger, and vector-registry correctness fixes - Expanded UAT/integration/durability test coverage across all milestones
192 lines
5.9 KiB
Rust
192 lines
5.9 KiB
Rust
//! Milestone 6 Phase 3 Integration Tests: Missing-Metadata + Context Error Cases.
|
|
//!
|
|
//! Exercises M6P3 edge cases end-to-end through `TidalDb`:
|
|
//!
|
|
//! 1. Missing title sorted last under alphabetical sort.
|
|
//! 2. Missing duration sorted last under duration sort.
|
|
//! 3. `DateSaved` sort errors without `FOR USER` context.
|
|
//!
|
|
//! Sort modes live in `m6p3_sorts.rs`; engagement + geographic filters live in
|
|
//! `m6p3_filters.rs`.
|
|
|
|
#![allow(
|
|
clippy::too_many_lines,
|
|
clippy::unwrap_used,
|
|
clippy::cast_precision_loss
|
|
)]
|
|
|
|
use std::{collections::HashMap, time::Duration};
|
|
|
|
use tidaldb::{
|
|
TidalDb,
|
|
query::retrieve::Retrieve,
|
|
schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
|
|
};
|
|
|
|
// ── Schema ──────────────────────────────────────────────────────────────────
|
|
|
|
fn m6p3_schema_without_viewer_count() -> tidaldb::schema::Schema {
|
|
let mut builder = SchemaBuilder::new();
|
|
for &(name, half_life_days) in &[
|
|
("view", 7),
|
|
("like", 14),
|
|
("share", 7),
|
|
("comment", 7),
|
|
("skip", 1),
|
|
("hide", 1),
|
|
] {
|
|
let _ = builder
|
|
.signal(
|
|
name,
|
|
EntityKind::Item,
|
|
DecaySpec::Exponential {
|
|
half_life: Duration::from_secs(half_life_days * 24 * 3600),
|
|
},
|
|
)
|
|
.windows(&[
|
|
Window::OneHour,
|
|
Window::TwentyFourHours,
|
|
Window::SevenDays,
|
|
Window::AllTime,
|
|
])
|
|
.velocity(true)
|
|
.add();
|
|
}
|
|
builder.build().expect("schema must be valid")
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
fn item_meta(title: &str, category: &str, creator_id: u32) -> HashMap<String, String> {
|
|
let mut m = HashMap::new();
|
|
m.insert("title".to_string(), title.to_string());
|
|
m.insert("category".to_string(), category.to_string());
|
|
m.insert("creator_id".to_string(), creator_id.to_string());
|
|
m
|
|
}
|
|
|
|
fn item_meta_with_duration(
|
|
title: &str,
|
|
category: &str,
|
|
creator_id: u32,
|
|
duration_secs: u32,
|
|
) -> HashMap<String, String> {
|
|
let mut m = item_meta(title, category, creator_id);
|
|
m.insert("duration".to_string(), duration_secs.to_string());
|
|
m
|
|
}
|
|
|
|
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn missing_title_sorted_last_alphabetical() {
|
|
let schema = m6p3_schema_without_viewer_count();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
// Item 1 has a title; item 2 has no title; item 3 has a title.
|
|
db.write_item_with_metadata(EntityId::new(1), &item_meta("Beta", "music", 100))
|
|
.unwrap();
|
|
let mut no_title = HashMap::new();
|
|
no_title.insert("category".to_string(), "music".to_string());
|
|
no_title.insert("creator_id".to_string(), "100".to_string());
|
|
db.write_item_with_metadata(EntityId::new(2), &no_title)
|
|
.unwrap();
|
|
db.write_item_with_metadata(EntityId::new(3), &item_meta("Alpha", "music", 100))
|
|
.unwrap();
|
|
|
|
let ts = Timestamp::now();
|
|
for id in 1..=3 {
|
|
db.signal("view", EntityId::new(id), 1.0, ts).unwrap();
|
|
}
|
|
|
|
let query = Retrieve::builder()
|
|
.profile("alphabetical_asc")
|
|
.limit(10)
|
|
.build()
|
|
.unwrap();
|
|
let results = db.retrieve(&query).unwrap();
|
|
|
|
assert!(results.items.len() >= 3);
|
|
// Alpha, Beta, then no-title (sorted last)
|
|
assert_eq!(results.items[0].entity_id, EntityId::new(3)); // Alpha
|
|
assert_eq!(results.items[1].entity_id, EntityId::new(1)); // Beta
|
|
assert_eq!(results.items[2].entity_id, EntityId::new(2)); // no title
|
|
|
|
db.close().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn missing_duration_sorted_last() {
|
|
let schema = m6p3_schema_without_viewer_count();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
db.write_item_with_metadata(
|
|
EntityId::new(1),
|
|
&item_meta_with_duration("Short", "music", 100, 60),
|
|
)
|
|
.unwrap();
|
|
// Item 2: no duration
|
|
db.write_item_with_metadata(EntityId::new(2), &item_meta("NoDuration", "music", 100))
|
|
.unwrap();
|
|
db.write_item_with_metadata(
|
|
EntityId::new(3),
|
|
&item_meta_with_duration("Long", "music", 100, 3600),
|
|
)
|
|
.unwrap();
|
|
|
|
let ts = Timestamp::now();
|
|
for id in 1..=3 {
|
|
db.signal("view", EntityId::new(id), 1.0, ts).unwrap();
|
|
}
|
|
|
|
let query = Retrieve::builder()
|
|
.profile("shortest")
|
|
.limit(10)
|
|
.build()
|
|
.unwrap();
|
|
let results = db.retrieve(&query).unwrap();
|
|
|
|
assert!(results.items.len() >= 3);
|
|
// No-duration item should be last.
|
|
assert_eq!(results.items[2].entity_id, EntityId::new(2));
|
|
|
|
db.close().unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn date_saved_requires_for_user() {
|
|
let schema = m6p3_schema_without_viewer_count();
|
|
let db = TidalDb::builder()
|
|
.ephemeral()
|
|
.with_schema(schema)
|
|
.open()
|
|
.unwrap();
|
|
|
|
db.write_item_with_metadata(EntityId::new(1), &item_meta("Item", "music", 100))
|
|
.unwrap();
|
|
db.signal("view", EntityId::new(1), 1.0, Timestamp::now())
|
|
.unwrap();
|
|
|
|
// Query with DateSaved but NO for_user -> should error.
|
|
let query = Retrieve::builder()
|
|
.profile("date_saved")
|
|
.limit(10)
|
|
.build()
|
|
.unwrap();
|
|
let result = db.retrieve(&query);
|
|
assert!(
|
|
result.is_err(),
|
|
"DateSaved sort without FOR USER should fail"
|
|
);
|
|
|
|
db.close().unwrap();
|
|
}
|