tidaldb/tidal/tests/m6p3_edge_cases.rs
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

188 lines
5.8 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::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();
}