mod common;
use cooklang_sync_client::connection::get_connection;
use cooklang_sync_client::models::{CreateForm, DeleteForm, FileRecord};
use cooklang_sync_client::registry;
use cooklang_sync_client::schema::file_records;
use diesel::prelude::*;
use time::OffsetDateTime;
fn sample_create(path: &str, size: i64, ns: i32) -> CreateForm {
CreateForm {
jid: None,
path: path.to_string(),
deleted: false,
size,
modified_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap(),
namespace_id: ns,
}
}
fn sample_delete(from: &FileRecord) -> DeleteForm {
DeleteForm {
path: from.path.clone(),
jid: None,
size: from.size,
modified_at: from.modified_at,
deleted: true,
namespace_id: from.namespace_id,
}
}
#[test]
fn create_inserts_rows_and_returns_count() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
let forms = vec![
sample_create("a.cook", 10, 1),
sample_create("b.cook", 20, 1),
];
let n = registry::create(conn, &forms).expect("create should insert");
assert_eq!(n, 2);
let all: Vec<FileRecord> = file_records::table
.select(FileRecord::as_select())
.order(file_records::id.asc())
.load(conn)
.expect("load all");
assert_eq!(all.len(), 2);
assert_eq!(all[0].path, "a.cook");
assert_eq!(all[0].size, 10);
assert!(all[0].jid.is_none(), "new rows must have no jid yet");
assert!(all[0].id < all[1].id, "id must be monotonic");
}
#[test]
fn update_jid_sets_jid_and_preserves_other_columns() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
registry::create(conn, &vec![sample_create("a.cook", 42, 1)]).unwrap();
let row: FileRecord = file_records::table
.select(FileRecord::as_select())
.first(conn)
.expect("row");
assert!(row.jid.is_none());
let original_path = row.path.clone();
let original_size = row.size;
let original_mtime = row.modified_at;
let n = registry::update_jid(conn, &row, 7).expect("update_jid");
assert_eq!(n, 1);
let after: FileRecord = file_records::table
.select(FileRecord::as_select())
.first(conn)
.expect("reload");
assert_eq!(after.jid, Some(7));
assert_eq!(after.path, original_path);
assert_eq!(after.size, original_size);
assert_eq!(after.modified_at, original_mtime);
assert!(!after.deleted);
}
#[test]
fn delete_appends_tombstone_row_rather_than_updating() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
registry::create(conn, &vec![sample_create("a.cook", 10, 1)]).unwrap();
let live: FileRecord = file_records::table
.select(FileRecord::as_select())
.first(conn)
.unwrap();
assert!(!live.deleted);
let n = registry::delete(conn, &vec![sample_delete(&live)]).expect("delete");
assert_eq!(n, 1);
let rows: Vec<FileRecord> = file_records::table
.filter(file_records::path.eq("a.cook"))
.select(FileRecord::as_select())
.order(file_records::id.asc())
.load(conn)
.unwrap();
assert_eq!(rows.len(), 2, "delete is append-only; original row is preserved");
assert!(!rows[0].deleted);
assert!(rows[1].deleted);
assert!(rows[1].id > rows[0].id, "tombstone id must be newer");
}
#[test]
fn non_deleted_returns_latest_live_row_per_path_scoped_by_namespace() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
registry::create(
conn,
&vec![
sample_create("a.cook", 10, 1), sample_create("b.cook", 20, 1), ],
)
.unwrap();
let mut modified = sample_create("a.cook", 11, 1);
modified.modified_at = OffsetDateTime::from_unix_timestamp(1_700_000_500).unwrap();
registry::create(conn, &vec![modified]).unwrap();
registry::create(conn, &vec![sample_create("c.cook", 30, 1)]).unwrap(); let c: FileRecord = file_records::table
.filter(file_records::path.eq("c.cook"))
.select(FileRecord::as_select())
.first(conn)
.unwrap();
registry::delete(conn, &vec![sample_delete(&c)]).unwrap();
registry::create(conn, &vec![sample_create("a.cook", 999, 2)]).unwrap();
let live = registry::non_deleted(conn, 1).expect("non_deleted ns 1");
let paths: Vec<(&str, i64)> = live.iter().map(|r| (r.path.as_str(), r.size)).collect();
assert_eq!(paths, vec![("b.cook", 20), ("a.cook", 11)]);
let live_ns2 = registry::non_deleted(conn, 2).unwrap();
assert_eq!(live_ns2.len(), 1);
assert_eq!(live_ns2[0].path, "a.cook");
assert_eq!(live_ns2[0].size, 999);
}
#[test]
fn non_deleted_empty_db_returns_empty_vec() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
let live = registry::non_deleted(conn, 1).unwrap();
assert!(live.is_empty());
}
#[test]
fn updated_locally_returns_latest_null_jid_rows_per_path_scoped_by_namespace() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
registry::create(conn, &vec![sample_create("a.cook", 10, 1)]).unwrap();
let a1: FileRecord = file_records::table
.filter(file_records::path.eq("a.cook"))
.select(FileRecord::as_select())
.first(conn)
.unwrap();
registry::update_jid(conn, &a1, 5).unwrap();
let mut a2 = sample_create("a.cook", 11, 1);
a2.modified_at = OffsetDateTime::from_unix_timestamp(1_700_000_500).unwrap();
registry::create(conn, &vec![a2]).unwrap();
registry::create(conn, &vec![sample_create("b.cook", 20, 1)]).unwrap();
let b: FileRecord = file_records::table
.filter(file_records::path.eq("b.cook"))
.select(FileRecord::as_select())
.first(conn)
.unwrap();
registry::update_jid(conn, &b, 6).unwrap();
registry::create(conn, &vec![sample_create("x.cook", 30, 2)]).unwrap();
let pending = registry::updated_locally(conn, 1).unwrap();
let paths: Vec<(&str, i64)> = pending.iter().map(|r| (r.path.as_str(), r.size)).collect();
assert_eq!(paths, vec![("a.cook", 11)]);
let pending_ns2 = registry::updated_locally(conn, 2).unwrap();
assert_eq!(pending_ns2.len(), 1);
assert_eq!(pending_ns2[0].path, "x.cook");
}
#[test]
fn updated_locally_empty_db_returns_empty_vec() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).unwrap();
assert!(registry::updated_locally(conn, 1).unwrap().is_empty());
}
#[test]
fn latest_jid_returns_not_found_on_empty_db() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).unwrap();
let err = registry::latest_jid(conn, 1).expect_err("no jid rows = NotFound");
assert!(
matches!(err, diesel::result::Error::NotFound),
"empty DB should produce NotFound, got: {err:?}"
);
}
#[test]
fn latest_jid_returns_highest_jid_in_namespace_and_ignores_null_jid_rows() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).unwrap();
registry::create(
conn,
&vec![
sample_create("a.cook", 10, 1),
sample_create("b.cook", 20, 1),
sample_create("c.cook", 30, 1),
],
)
.unwrap();
let rows: Vec<FileRecord> = file_records::table
.filter(file_records::namespace_id.eq(1))
.select(FileRecord::as_select())
.order(file_records::id.asc())
.load(conn)
.unwrap();
registry::update_jid(conn, &rows[0], 3).unwrap();
registry::update_jid(conn, &rows[1], 7).unwrap();
registry::create(conn, &vec![sample_create("x.cook", 1, 2)]).unwrap();
let x: FileRecord = file_records::table
.filter(file_records::namespace_id.eq(2))
.select(FileRecord::as_select())
.first(conn)
.unwrap();
registry::update_jid(conn, &x, 100).unwrap();
assert_eq!(registry::latest_jid(conn, 1).unwrap(), 7);
assert_eq!(registry::latest_jid(conn, 2).unwrap(), 100);
}
#[test]
fn updated_locally_surfaces_unsynced_tombstone() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
registry::create(conn, &vec![sample_create("gone.cook", 10, 1)]).expect("create");
let existing: Vec<FileRecord> = registry::non_deleted(conn, 1).expect("non_deleted");
let sample = existing.first().expect("row present");
registry::delete(conn, &vec![sample_delete(sample)]).expect("delete");
let unsynced = registry::updated_locally(conn, 1).expect("updated_locally");
assert_eq!(unsynced.len(), 1, "tombstone with jid=None must appear in updated_locally");
assert_eq!(unsynced[0].path, "gone.cook");
assert!(unsynced[0].deleted, "latest row is the tombstone");
assert!(unsynced[0].jid.is_none());
}
#[test]
fn latest_jid_returns_zero_for_explicit_jid_zero() {
let (pool, _dir) = common::fresh_client_pool();
let conn = &mut get_connection(&pool).expect("checkout");
let mut form = sample_create("a.cook", 5, 1);
form.jid = Some(0);
registry::create(conn, &vec![form]).expect("create");
let latest = registry::latest_jid(conn, 1).expect("latest_jid");
assert_eq!(latest, 0, "Some(0) must unwrap to 0, not NotFound");
}