Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c30b0fd
test(common): add client_base helper for syncer tests
dubadub Apr 16, 2026
6cd90bf
test(remote): pin commit happy path and NeedChunks wire shape
dubadub Apr 16, 2026
ff2f03c
test(remote): pin commit 401/5xx error mapping
dubadub Apr 16, 2026
1520b8f
test(remote): pin list happy/empty/unauthorized
dubadub Apr 16, 2026
e17315a
test(remote): pin poll ok/unauthorized/timeout-as-ok
dubadub Apr 16, 2026
e452ccd
test(remote): pin upload + upload_batch multipart shape
dubadub Apr 16, 2026
0f3abd1
test(remote): pin download ok + unauthorized
dubadub Apr 16, 2026
3991edb
test(remote): pin download_batch multipart streaming + missing conten…
dubadub Apr 16, 2026
05e70be
test(syncer): pin check_upload_once happy path
dubadub Apr 16, 2026
142cfa2
test(syncer): pin check_upload_once NeedChunks -> upload_batch
dubadub Apr 16, 2026
28e8c0d
test(syncer): pin tombstone upload short-circuits hashify
dubadub Apr 16, 2026
87feb60
test(syncer): pin check_download_once applies remote list to empty state
dubadub Apr 16, 2026
c143142
test(syncer): pin check_download_once tombstone path
dubadub Apr 16, 2026
bb66d36
test(syncer): pin check_download_once empty-list noop
dubadub Apr 16, 2026
b5b2d19
test(syncer): pin Unauthorized propagation from remote.list
dubadub Apr 16, 2026
70b6ce6
test(client): absorb Plan 2 review follow-ups (tombstone, jid=0, empt…
dubadub Apr 16, 2026
4360f9e
chore(client): fix pre-existing clippy warnings in test code
dubadub Apr 16, 2026
c3d1268
test(client): address Plan 3 review items
github-actions[bot] May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ pub fn run_download_once(

let storage_dir = &PathBuf::from(storage_dir);
let chunk_cache = InMemoryCache::new(INMEMORY_CACHE_MAX_REC, INMEMORY_CACHE_MAX_MEM);
let chunker = &mut Chunker::new(chunk_cache, storage_dir.clone());
let chunker = Arc::new(Mutex::new(chunker));
let chunker = Arc::new(Mutex::new(Chunker::new(chunk_cache, storage_dir.clone())));
let remote = &remote::Remote::new(api_endpoint, remote_token);

let pool = connection::get_connection_pool(db_file_path)?;
Expand Down Expand Up @@ -138,8 +137,7 @@ pub fn run_upload_once(
) -> Result<(), errors::SyncError> {
let storage_dir = &PathBuf::from(storage_dir);
let chunk_cache = InMemoryCache::new(INMEMORY_CACHE_MAX_REC, INMEMORY_CACHE_MAX_MEM);
let chunker = &mut Chunker::new(chunk_cache, storage_dir.clone());
let chunker = Arc::new(Mutex::new(chunker));
let chunker = Arc::new(Mutex::new(Chunker::new(chunk_cache, storage_dir.clone())));
let remote = &remote::Remote::new(api_endpoint, remote_token);

let pool = connection::get_connection_pool(db_file_path)?;
Expand Down Expand Up @@ -188,7 +186,7 @@ pub async fn run_async(

let storage_dir = &PathBuf::from(storage_dir);
let chunk_cache = InMemoryCache::new(INMEMORY_CACHE_MAX_REC, INMEMORY_CACHE_MAX_MEM);
let chunker = &mut Chunker::new(chunk_cache, storage_dir.clone());
let chunker = Chunker::new(chunk_cache, storage_dir.clone());
let remote = &remote::Remote::new(api_endpoint, remote_token);

let pool = connection::get_connection_pool(db_file_path)?;
Expand Down
4 changes: 2 additions & 2 deletions client/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ mod tests {

assert_eq!(form.jid, Some(42));
assert_eq!(form.path, "recipes/test.cook");
assert_eq!(form.deleted, false);
assert!(!form.deleted);
assert_eq!(form.size, 512);
assert_eq!(form.namespace_id, 5);
}
Expand All @@ -238,7 +238,7 @@ mod tests {
};

assert_eq!(form.path, "recipes/deleted.cook");
assert_eq!(form.deleted, true);
assert!(form.deleted);
assert_eq!(form.size, 0);
}

Expand Down
4 changes: 2 additions & 2 deletions client/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::schema::*;

type Result<T, E = diesel::result::Error> = std::result::Result<T, E>;

pub fn create(conn: &mut Connection, forms: &Vec<CreateForm>) -> Result<usize> {
pub fn create(conn: &mut Connection, forms: &[CreateForm]) -> Result<usize> {
trace!("inserting {:?}", forms);

insert_into(file_records::table).values(forms).execute(conn)
Expand All @@ -25,7 +25,7 @@ pub fn update_jid(conn: &mut Connection, record: &FileRecord, jid: i32) -> Resul
.execute(conn)
}

pub fn delete(conn: &mut Connection, forms: &Vec<DeleteForm>) -> Result<usize> {
pub fn delete(conn: &mut Connection, forms: &[DeleteForm]) -> Result<usize> {
trace!("marking as deleted {:?}", forms);

insert_into(file_records::table).values(forms).execute(conn)
Expand Down
10 changes: 5 additions & 5 deletions client/src/syncer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub async fn run(
pool: &ConnectionPool,
storage_path: &Path,
namespace_id: i32,
chunker: &mut Chunker,
chunker: Chunker,
remote: &Remote,
local_registry_updated_rx: Receiver<models::IndexerUpdateEvent>,
read_only: bool,
Expand Down Expand Up @@ -78,7 +78,7 @@ async fn download_loop(
token: CancellationToken,
listener: Option<Arc<dyn SyncStatusListener>>,
pool: &ConnectionPool,
chunker: Arc<Mutex<&mut Chunker>>,
chunker: Arc<Mutex<Chunker>>,
remote: &Remote,
storage_path: &Path,
namespace_id: i32,
Expand Down Expand Up @@ -134,7 +134,7 @@ pub async fn upload_loop(
token: CancellationToken,
listener: Option<Arc<dyn SyncStatusListener>>,
pool: &ConnectionPool,
chunker: Arc<Mutex<&mut Chunker>>,
chunker: Arc<Mutex<Chunker>>,
remote: &Remote,
namespace_id: i32,
mut local_registry_updated_rx: Receiver<models::IndexerUpdateEvent>,
Expand Down Expand Up @@ -182,7 +182,7 @@ pub async fn upload_loop(

pub async fn check_upload_once(
pool: &ConnectionPool,
chunker: Arc<Mutex<&mut Chunker>>,
chunker: Arc<Mutex<Chunker>>,
remote: &Remote,
namespace_id: i32,
) -> Result<bool> {
Expand Down Expand Up @@ -246,7 +246,7 @@ pub async fn check_upload_once(

pub async fn check_download_once(
pool: &ConnectionPool,
chunker: Arc<Mutex<&mut Chunker>>,
chunker: Arc<Mutex<Chunker>>,
remote: &Remote,
storage_path: &Path,
namespace_id: i32,
Expand Down
20 changes: 20 additions & 0 deletions client/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,23 @@ fn base64_url_nopad(bytes: &[u8]) -> String {
use base64::Engine;
URL_SAFE_NO_PAD.encode(bytes)
}

use cooklang_sync_client::chunker::{Chunker, InMemoryCache};

/// Bundle of the three things every syncer integration test needs:
/// a fresh SQLite pool, a tempdir root, and a `Chunker` rooted at that dir.
///
/// The tempdir is returned separately (not moved into the `Chunker`) so the
/// test can write fixture files into it before invoking the syncer.
pub struct ClientBase {
pub pool: cooklang_sync_client::connection::ConnectionPool,
pub dir: TempDir,
pub chunker: Chunker,
}

pub fn client_base() -> ClientBase {
let (pool, dir) = fresh_client_pool();
let cache = InMemoryCache::new(100, 10_000_000);
let chunker = Chunker::new(cache, dir.path().to_path_buf());
ClientBase { pool, dir, chunker }
}
3 changes: 1 addition & 2 deletions client/tests/connection_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ fn get_connection_pool_returns_error_for_unwritable_path() {
// directory, causing SQLite to fail to open the file.
let bogus = "/dev/null/does_not_exist/db.sqlite3";
let err = get_connection_pool(bogus)
.err()
.expect("pool creation should fail on unwritable parent");
.expect_err("pool creation should fail on unwritable parent");
let msg = format!("{err}");
assert!(
msg.to_lowercase().contains("connection"),
Expand Down
13 changes: 13 additions & 0 deletions client/tests/indexer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,16 @@ fn check_index_once_does_not_tombstone_just_downloaded_file() {
assert_eq!(live[0].path, rel);
assert!(!live[0].deleted, "downloaded file must not be soft-deleted by the indexer");
}

#[test]
fn check_index_once_on_empty_dir_is_noop() {
let (pool, _dir) = common::fresh_client_pool();
let storage = TempDir::new().expect("tempdir");

let changed = check_index_once(&pool, storage.path(), 1).expect("check_index_once");
assert!(!changed, "empty dir must return Ok(false)");

let conn = &mut get_connection(&pool).expect("checkout");
let rows = registry::non_deleted(conn, 1).expect("non_deleted");
assert!(rows.is_empty(), "empty dir must not produce any registry rows");
}
35 changes: 35 additions & 0 deletions client/tests/registry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,38 @@ fn latest_jid_returns_highest_jid_in_namespace_and_ignores_null_jid_rows() {
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");

// Create then delete (tombstone is appended; jid still None on both rows).
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");

// Latest row per path is the tombstone, which still has jid=None, so
// updated_locally must surface it - this is what lets the upload path
// retry tombstones that never reached the server.
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");

// Insert a single row with jid=Some(0).
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");
}
Loading
Loading