From 0d8fa8fd4ce53880ab2a9e1db7da1704c8eb773b Mon Sep 17 00:00:00 2001 From: Quanyi Ma Date: Mon, 6 Jul 2026 17:20:06 +0800 Subject: [PATCH 1/3] test(cloud): isolate restore object tests from cwd Signed-off-by: Quanyi Ma --- src/command/cloud.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/command/cloud.rs b/src/command/cloud.rs index 9004ca8cb..f93f8a126 100644 --- a/src/command/cloud.rs +++ b/src/command/cloud.rs @@ -2504,6 +2504,22 @@ mod tests { } } + async fn enter_isolated_libra_repo() -> ( + tempfile::TempDir, + tempfile::TempDir, + ScopedEnvVar, + ScopedEnvVar, + ChangeDirGuard, + ) { + let repo = tempdir().unwrap(); + let home = tempdir().unwrap(); + let home_env = ScopedEnvVar::set("HOME", home.path()); + let test_home_env = ScopedEnvVar::set("LIBRA_TEST_HOME", home.path()); + setup_with_new_libra_in(repo.path()).await; + let cwd = ChangeDirGuard::new(repo.path()); + (repo, home, home_env, test_home_env, cwd) + } + struct ClearedEnvVarGuard { key: String, previous: Option, @@ -2882,7 +2898,9 @@ mod tests { } #[tokio::test] + #[serial] async fn cloud_restore_indexed_objects_downloads_skips_and_verifies_hash() { + let _repo = enter_isolated_libra_repo().await; let remote = RemoteStorage::new(Arc::new(InMemory::new())); let local_dir = tempdir().unwrap(); let local = LocalStorage::new(local_dir.path().to_path_buf()); @@ -2917,7 +2935,9 @@ mod tests { } #[tokio::test] + #[serial] async fn cloud_restore_indexed_objects_reports_hash_mismatch() { + let _repo = enter_isolated_libra_repo().await; let remote = RemoteStorage::new(Arc::new(InMemory::new())); let local_dir = tempdir().unwrap(); let local = LocalStorage::new(local_dir.path().to_path_buf()); From e6352ea5f61a0eedbac0125903620da7441a3503 Mon Sep 17 00:00:00 2001 From: Quanyi Ma Date: Mon, 6 Jul 2026 17:20:06 +0800 Subject: [PATCH 2/3] fix(reset): preserve separator for dash-prefixed paths Signed-off-by: Quanyi Ma --- src/cli.rs | 1 + tests/command/reset_test.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 6ccf3982a..5c6c2034c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1066,6 +1066,7 @@ fn rewrite_reset_pathspec_separator_args(args: Vec) -> Vec { if !has_target_before_separator && args.get(separator_index + 1).is_some() { out.push("HEAD".to_string()); } + out.push("--".to_string()); out.extend(args.iter().skip(separator_index + 1).cloned()); out } diff --git a/tests/command/reset_test.rs b/tests/command/reset_test.rs index 96893997a..9cf6550b9 100644 --- a/tests/command/reset_test.rs +++ b/tests/command/reset_test.rs @@ -372,6 +372,41 @@ fn reset_double_dash_pathspec_unstages_file_named_like_revision() { assert_eq!(String::from_utf8_lossy(&status.stdout), " M HEAD\n"); } +#[test] +fn reset_double_dash_preserves_dash_prefixed_pathspecs() { + // Given: tracked files have names that would otherwise parse as flags. + let repo = create_committed_repo_via_cli(); + for name in ["-h", "--help"] { + fs::write(repo.path().join(name), format!("{name}\n")).unwrap(); + let add = run_libra_command(&["add", "--", name], repo.path()); + assert_cli_success(&add, "add dash-prefixed file"); + } + let commit = run_libra_command( + &["commit", "-m", "add dash-prefixed files", "--no-verify"], + repo.path(), + ); + assert_cli_success(&commit, "commit dash-prefixed files"); + + for name in ["-h", "--help"] { + fs::write(repo.path().join(name), format!("{name}\nupdated\n")).unwrap(); + let add = run_libra_command(&["add", "--", name], repo.path()); + assert_cli_success(&add, "stage dash-prefixed file update"); + + // When: `--` marks the dash-prefixed token as a pathspec. + let output = run_libra_command(&["reset", "--", name], repo.path()); + + // Then: reset treats the token as a filename instead of a reset/help flag. + assert_cli_success(&output, "reset dash-prefixed pathspec"); + let status = run_libra_command(&["status", "--short"], repo.path()); + assert_cli_success(&status, "status --short"); + let stdout = String::from_utf8_lossy(&status.stdout); + assert!( + stdout.contains(&format!(" M {name}\n")), + "expected {name} to be unstaged, got: {stdout}", + ); + } +} + #[test] fn reset_bare_revision_path_ambiguity_errors_like_git() { // Given: a token names both a branch and a tracked path. From 5182f5674b7cff3f33b666509b4f2f3c0482d79e Mon Sep 17 00:00:00 2001 From: Quanyi Ma Date: Mon, 6 Jul 2026 19:22:39 +0800 Subject: [PATCH 3/3] test(file): avoid sqlite3 binary in obliterate recovery Signed-off-by: Quanyi Ma --- tests/command/file_obliterate_test.rs | 56 ++++++++++++++++++++------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/tests/command/file_obliterate_test.rs b/tests/command/file_obliterate_test.rs index 1b61b20ea..1987ea017 100644 --- a/tests/command/file_obliterate_test.rs +++ b/tests/command/file_obliterate_test.rs @@ -6,7 +6,13 @@ //! //! Layer: L1 (deterministic; tempdir + isolated HOME, no network). -use std::fs; +use std::{ + fs, + path::{Path, PathBuf}, + time::Duration, +}; + +use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, Statement}; use super::{ assert_cli_success, create_committed_repo_via_cli, parse_cli_error_stderr, run_libra_command, @@ -36,10 +42,42 @@ fn repo_with_secret() -> (tempfile::TempDir, String) { (repo, oid) } -fn loose_path(repo: &std::path::Path, oid: &str) -> std::path::PathBuf { +fn loose_path(repo: &Path, oid: &str) -> PathBuf { repo.join(".libra/objects").join(&oid[..2]).join(&oid[2..]) } +async fn connect_raw_repo_db(repo: &Path) -> DatabaseConnection { + let db_path = repo.join(".libra").join("libra.db"); + let mut opts = ConnectOptions::new(format!("sqlite://{}", db_path.display())); + opts.sqlx_logging(false) + .connect_timeout(Duration::from_secs(5)); + Database::connect(opts) + .await + .expect("connect raw repository database") +} + +fn mark_obliteration_as_interrupted(repo: &Path) { + let runtime = tokio::runtime::Runtime::new().expect("create runtime for crash-recovery setup"); + runtime.block_on(async { + let conn = connect_raw_repo_db(repo).await; + let result = conn + .execute(Statement::from_string( + conn.get_database_backend(), + "UPDATE object_obliteration SET state='obliterating', payload_deleted_at=NULL", + )) + .await + .expect("reset obliteration state to 'obliterating'"); + assert_eq!( + result.rows_affected(), + 1, + "reset exactly one obliteration row to 'obliterating'" + ); + conn.close() + .await + .expect("close crash-recovery setup database"); + }); +} + #[test] fn obliterate_dry_run_previews_and_deletes_nothing() { let (repo, oid) = repo_with_secret(); @@ -148,17 +186,9 @@ fn obliterate_recover_finishes_interrupted() { "payload restored on disk for the test" ); - // Force the mid-state via sqlite3 — REQUIRED (fail, don't skip, if absent). - let db = p.join(".libra/libra.db"); - let status = std::process::Command::new("sqlite3") - .arg(&db) - .arg("UPDATE object_obliteration SET state='obliterating', payload_deleted_at=NULL;") - .status() - .expect("sqlite3 is required for the crash-recovery test"); - assert!( - status.success(), - "reset the obliteration state to 'obliterating'" - ); + // Force the mid-state in the repository database without relying on host + // sqlite3 binaries, which are not guaranteed on self-hosted runners. + mark_obliteration_as_interrupted(p); // Recovery must re-delete the payload and finalize. let recover = run_libra_command(&["file", "obliterate", "--recover"], p);