From 5bbf3f45aeffc6c847d4d063cd7a62a967026f15 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 11:22:16 +0900 Subject: [PATCH 01/35] test: require successful incomplete-download help --- .../tests/cli_help_incomplete_plans_exit.rs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src-tauri/tests/cli_help_incomplete_plans_exit.rs diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs new file mode 100644 index 000000000..f09718b87 --- /dev/null +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -0,0 +1,137 @@ +//! Black-box help and invalid-argument contracts for incomplete-download planning CLIs. + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const BINARIES: [(&str, &str); 2] = [ + ( + "disksage-incomplete-download-materialization", + "usage: disksage-incomplete-download-materialization", + ), + ( + "disksage-incomplete-download-recovery", + "usage: disksage-incomplete-download-recovery", + ), +]; + +fn build_feature_gated_binaries() -> (tempfile::TempDir, Vec) { + let target_dir = tempfile::tempdir().expect("isolated Cargo target directory must be created"); + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); + let mut command = Command::new(cargo); + command + .current_dir(env!("CARGO_MANIFEST_DIR")) + .args(["build", "--locked", "--features", "cloud-cli"]); + for (binary, _) in BINARIES { + command.args(["--bin", binary]); + } + let status = command + .arg("--target-dir") + .arg(target_dir.path()) + .status() + .expect("feature-gated incomplete-download CLIs must be buildable for process contracts"); + assert!( + status.success(), + "feature-gated incomplete-download CLI build must succeed before process assertions" + ); + + let binaries = BINARIES + .iter() + .map(|(binary, _)| { + let path = target_dir + .path() + .join("debug") + .join(format!("{binary}{}", std::env::consts::EXE_SUFFIX)); + assert!( + path.is_file(), + "{binary} must exist after the explicit cloud-cli build" + ); + path + }) + .collect(); + (target_dir, binaries) +} + +fn command(binary: &Path) -> Command { + let mut command = Command::new(binary); + command.env_remove("HOME").env_remove("USERPROFILE"); + command +} + +fn assert_help_success(binary: &Path, usage: &str, flag: &str) { + let output = command(binary) + .arg(flag) + .output() + .expect("incomplete-download CLI must launch for its help contract"); + + assert!( + output.status.success(), + "{flag} must be a successful terminal action, got status {:?} and stderr {:?}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + output.stderr.is_empty(), + "successful help must not be projected through stderr" + ); + let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); + assert!( + stdout.contains(usage), + "help output must contain the stable usage synopsis" + ); +} + +fn assert_invalid_argument_is_bounded(binary: &Path) { + let output = command(binary) + .arg("--opaque-option=not-shown") + .output() + .expect("incomplete-download CLI must launch for invalid argument validation"); + + assert!( + !output.status.success(), + "an unknown argument must remain a non-zero failure" + ); + assert!( + output.stdout.is_empty(), + "invalid invocation must not emit successful output on stdout" + ); + let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); + assert!(!stderr.is_empty(), "invalid invocation must remain visible"); + assert!( + !stderr.contains("not-shown"), + "invalid diagnostics must not echo arbitrary argument payloads" + ); +} + +fn assert_help_does_not_hide_invalid_argument(binary: &Path) { + let output = command(binary) + .args(["--help", "--opaque-option=not-shown"]) + .output() + .expect("incomplete-download CLI must launch for mixed help validation"); + + assert!( + !output.status.success(), + "help must not turn an otherwise invalid invocation into success" + ); + assert!( + output.stdout.is_empty(), + "mixed invalid invocation must not emit successful help on stdout" + ); + let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); + assert!(!stderr.is_empty(), "mixed invalid invocation must remain visible"); + assert!( + !stderr.contains("not-shown"), + "mixed invalid diagnostics must not echo arbitrary argument payloads" + ); +} + +#[test] +fn incomplete_download_planning_help_is_successful_and_invalid_arguments_are_bounded() { + let (_target_dir, binaries) = build_feature_gated_binaries(); + for ((_, usage), binary) in BINARIES.iter().zip(&binaries) { + assert_help_success(binary, usage, "--help"); + assert_help_success(binary, usage, "-h"); + assert_invalid_argument_is_bounded(binary); + assert_help_does_not_hide_invalid_argument(binary); + } +} From d66667a34a48239755117030c6e6fa4f06f5945c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 11:29:58 +0900 Subject: [PATCH 02/35] test: cover incomplete-download execution help --- .../tests/cli_help_materialize_exec_exit.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src-tauri/tests/cli_help_materialize_exec_exit.rs diff --git a/src-tauri/tests/cli_help_materialize_exec_exit.rs b/src-tauri/tests/cli_help_materialize_exec_exit.rs new file mode 100644 index 000000000..cec5c02a6 --- /dev/null +++ b/src-tauri/tests/cli_help_materialize_exec_exit.rs @@ -0,0 +1,80 @@ +//! Black-box help and invalid-argument contract for incomplete-download execution. + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const BINARY: &str = "disksage-incomplete-download-materialize"; +const USAGE: &str = "usage: disksage-incomplete-download-materialize"; + +fn build_binary() -> (tempfile::TempDir, PathBuf) { + let target_dir = tempfile::tempdir().expect("isolated Cargo target directory must be created"); + let cargo = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); + let status = Command::new(cargo) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .args([ + "build", + "--locked", + "--features", + "cloud-cli", + "--bin", + BINARY, + "--target-dir", + ]) + .arg(target_dir.path()) + .status() + .expect("feature-gated execution CLI must be buildable for process contracts"); + assert!(status.success(), "execution CLI build must succeed before process assertions"); + + let binary = target_dir + .path() + .join("debug") + .join(format!("{BINARY}{}", std::env::consts::EXE_SUFFIX)); + assert!(binary.is_file(), "execution CLI must exist after explicit cloud-cli build"); + (target_dir, binary) +} + +fn command(binary: &Path) -> Command { + let mut command = Command::new(binary); + command.env_remove("HOME").env_remove("USERPROFILE"); + command +} + +#[test] +fn materialization_execution_help_is_successful_and_invalid_arguments_are_bounded() { + let (_target_dir, binary) = build_binary(); + + for flag in ["--help", "-h"] { + let output = command(&binary) + .arg(flag) + .output() + .expect("execution CLI must launch for help validation"); + assert!( + output.status.success(), + "{flag} must be a successful terminal action, got status {:?} and stderr {:?}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty(), "successful help must keep stderr empty"); + let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); + assert!(stdout.contains(USAGE), "help must contain the stable usage synopsis"); + } + + for args in [ + vec!["--opaque-option=not-shown"], + vec!["--help", "--opaque-option=not-shown"], + ] { + let output = command(&binary) + .args(args) + .output() + .expect("execution CLI must launch for invalid argument validation"); + assert!(!output.status.success(), "invalid invocation must remain non-zero"); + assert!(output.stdout.is_empty(), "invalid invocation must keep stdout empty"); + let stderr = String::from_utf8(output.stderr).expect("diagnostics must be valid UTF-8"); + assert!(!stderr.is_empty(), "invalid invocation must remain visible"); + assert!( + !stderr.contains("not-shown"), + "diagnostics must not echo arbitrary argument payloads" + ); + } +} From ae78c36e255566b1a8b845ce04910f658c9e9d8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 11:31:57 +0900 Subject: [PATCH 03/35] test: consolidate incomplete-download CLI contracts --- src-tauri/tests/cli_help_incomplete_plans_exit.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index f09718b87..4f7d12859 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -1,10 +1,10 @@ -//! Black-box help and invalid-argument contracts for incomplete-download planning CLIs. +//! Black-box help and invalid-argument contracts for incomplete-download operational CLIs. use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command; -const BINARIES: [(&str, &str); 2] = [ +const BINARIES: [(&str, &str); 3] = [ ( "disksage-incomplete-download-materialization", "usage: disksage-incomplete-download-materialization", @@ -13,6 +13,10 @@ const BINARIES: [(&str, &str); 2] = [ "disksage-incomplete-download-recovery", "usage: disksage-incomplete-download-recovery", ), + ( + "disksage-incomplete-download-materialize", + "usage: disksage-incomplete-download-materialize", + ), ]; fn build_feature_gated_binaries() -> (tempfile::TempDir, Vec) { @@ -126,7 +130,7 @@ fn assert_help_does_not_hide_invalid_argument(binary: &Path) { } #[test] -fn incomplete_download_planning_help_is_successful_and_invalid_arguments_are_bounded() { +fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { let (_target_dir, binaries) = build_feature_gated_binaries(); for ((_, usage), binary) in BINARIES.iter().zip(&binaries) { assert_help_success(binary, usage, "--help"); From 25c46281dcbf2991528050f01114e39dd07ec0c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 11:32:15 +0900 Subject: [PATCH 04/35] test: avoid duplicate feature builds --- .../tests/cli_help_materialize_exec_exit.rs | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 src-tauri/tests/cli_help_materialize_exec_exit.rs diff --git a/src-tauri/tests/cli_help_materialize_exec_exit.rs b/src-tauri/tests/cli_help_materialize_exec_exit.rs deleted file mode 100644 index cec5c02a6..000000000 --- a/src-tauri/tests/cli_help_materialize_exec_exit.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Black-box help and invalid-argument contract for incomplete-download execution. - -use std::ffi::OsString; -use std::path::{Path, PathBuf}; -use std::process::Command; - -const BINARY: &str = "disksage-incomplete-download-materialize"; -const USAGE: &str = "usage: disksage-incomplete-download-materialize"; - -fn build_binary() -> (tempfile::TempDir, PathBuf) { - let target_dir = tempfile::tempdir().expect("isolated Cargo target directory must be created"); - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); - let status = Command::new(cargo) - .current_dir(env!("CARGO_MANIFEST_DIR")) - .args([ - "build", - "--locked", - "--features", - "cloud-cli", - "--bin", - BINARY, - "--target-dir", - ]) - .arg(target_dir.path()) - .status() - .expect("feature-gated execution CLI must be buildable for process contracts"); - assert!(status.success(), "execution CLI build must succeed before process assertions"); - - let binary = target_dir - .path() - .join("debug") - .join(format!("{BINARY}{}", std::env::consts::EXE_SUFFIX)); - assert!(binary.is_file(), "execution CLI must exist after explicit cloud-cli build"); - (target_dir, binary) -} - -fn command(binary: &Path) -> Command { - let mut command = Command::new(binary); - command.env_remove("HOME").env_remove("USERPROFILE"); - command -} - -#[test] -fn materialization_execution_help_is_successful_and_invalid_arguments_are_bounded() { - let (_target_dir, binary) = build_binary(); - - for flag in ["--help", "-h"] { - let output = command(&binary) - .arg(flag) - .output() - .expect("execution CLI must launch for help validation"); - assert!( - output.status.success(), - "{flag} must be a successful terminal action, got status {:?} and stderr {:?}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - ); - assert!(output.stderr.is_empty(), "successful help must keep stderr empty"); - let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); - assert!(stdout.contains(USAGE), "help must contain the stable usage synopsis"); - } - - for args in [ - vec!["--opaque-option=not-shown"], - vec!["--help", "--opaque-option=not-shown"], - ] { - let output = command(&binary) - .args(args) - .output() - .expect("execution CLI must launch for invalid argument validation"); - assert!(!output.status.success(), "invalid invocation must remain non-zero"); - assert!(output.stdout.is_empty(), "invalid invocation must keep stdout empty"); - let stderr = String::from_utf8(output.stderr).expect("diagnostics must be valid UTF-8"); - assert!(!stderr.is_empty(), "invalid invocation must remain visible"); - assert!( - !stderr.contains("not-shown"), - "diagnostics must not echo arbitrary argument payloads" - ); - } -} From a7a86fc3bb51c4df84c7fb369b53b05b862aa56f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 12:07:32 +0900 Subject: [PATCH 05/35] fix: make materialization help terminal and bounded --- ...age-incomplete-download-materialization.rs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index b9c5f7926..85209a4ad 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -26,6 +26,15 @@ fn absolute_without_parent(path: &Path) -> bool { .any(|component| matches!(component, Component::ParentDir)) } +fn usage() -> String { + format!( + "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH \ + [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ + [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ + [--private-output ABSOLUTE_NEW_FILE.json]" + ) +} + fn parse_args(raw: &[String]) -> Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; @@ -74,15 +83,8 @@ fn parse_args(raw: &[String]) -> Result { } private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); } - "--help" | "-h" => { - return Err(format!( - "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH \ - [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ - [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ - [--private-output ABSOLUTE_NEW_FILE.json]" - )); - } - flag => return Err(format!("알 수 없는 인자: {flag}")), + "--help" | "-h" => return Err(usage()), + _unknown => return Err("incomplete-download-materialization-unknown-argument".into()), } index += 1; } @@ -112,7 +114,12 @@ fn system_now_ms() -> u64 { #[cfg(not(coverage))] fn run() -> Result<(), String> { - let args = parse_args(&std::env::args().skip(1).collect::>())?; + let raw = std::env::args().skip(1).collect::>(); + if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + println!("{}", usage()); + return Ok(()); + } + let args = parse_args(&raw)?; let audit = collect_incomplete_download_audit( &args.root, system_now_ms(), From 4aac7f3387f339d2692df65f2e0e5508c55eb0cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 13:06:27 +0900 Subject: [PATCH 06/35] fix: make incomplete recovery help terminal and bounded --- .../disksage-incomplete-download-recovery.rs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index 9444b6cbc..256d6482d 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -24,6 +24,15 @@ fn absolute_without_parent(path: &Path) -> bool { .any(|component| matches!(component, Component::ParentDir)) } +fn usage() -> String { + format!( + "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH \ + [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ + [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ + [--private-output ABSOLUTE_NEW_FILE.json]" + ) +} + fn parse_args(raw: &[String]) -> Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; @@ -72,15 +81,8 @@ fn parse_args(raw: &[String]) -> Result { } private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); } - "--help" | "-h" => { - return Err(format!( - "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH \ - [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ - [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ - [--private-output ABSOLUTE_NEW_FILE.json]" - )); - } - flag => return Err(format!("알 수 없는 인자: {flag}")), + "--help" | "-h" => return Err(usage()), + _unknown => return Err("incomplete-download-recovery-unknown-argument".into()), } index += 1; } @@ -110,7 +112,12 @@ fn system_now_ms() -> u64 { #[cfg(not(coverage))] fn run() -> Result<(), String> { - let args = parse_args(&std::env::args().skip(1).collect::>())?; + let raw = std::env::args().skip(1).collect::>(); + if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + println!("{}", usage()); + return Ok(()); + } + let args = parse_args(&raw)?; let audit = collect_incomplete_download_audit( &args.root, system_now_ms(), From 0b794a0aa68f2a66978cb89e950bb04e24c8e2b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 14:06:03 +0900 Subject: [PATCH 07/35] fix: make materialize help terminal and bounded --- ...isksage-incomplete-download-materialize.rs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index d3669bbe4..676a09764 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -106,7 +106,7 @@ fn parse_args(raw: &[String]) -> Result { } "--receipt-dir" => { if receipt_dir.is_some() { - return Err("--receipt-dir은 한 번만 지정할 수 있음".into()); + return Err("--receipt-dir는 한 번만 지정할 수 있음".into()); } receipt_dir = Some(PathBuf::from(value(&mut index, "--receipt-dir")?)); } @@ -163,7 +163,7 @@ fn parse_args(raw: &[String]) -> Result { execute = true; } "--help" | "-h" => return Err(usage()), - flag => return Err(format!("알 수 없는 인자: {flag}")), + _unknown => return Err("incomplete-download-materialize-unknown-argument".into()), } index += 1; } @@ -298,7 +298,12 @@ fn verify_discovered_cloud_root( #[cfg(not(coverage))] fn run() -> Result<(), String> { - let args = parse_args(&std::env::args().skip(1).collect::>())?; + let raw = std::env::args().skip(1).collect::>(); + if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + println!("{}", usage()); + return Ok(()); + } + let args = parse_args(&raw)?; let plan: IncompleteDownloadDestinationPlan = read_bounded_json( &args.destination_plan, MAX_PRIVATE_PLAN_BYTES, @@ -313,17 +318,16 @@ fn run() -> Result<(), String> { verify_discovered_cloud_root(&home, &plan)?; let capacity_observed_at_ms = system_now_ms(); - let capacity = - if args.live_icloud_capacity { - if plan.provider != CloudProvider::Icloud { - return Err("live-icloud-capacity-requires-icloud-plan".into()); - } - collect_icloud_native_capacity(capacity_observed_at_ms)? - } else { - read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { - "materialization-execution-capacity-snapshot-missing".to_string() - })?)? - }; + let capacity = if args.live_icloud_capacity { + if plan.provider != CloudProvider::Icloud { + return Err("live-icloud-capacity-requires-icloud-plan".into()); + } + collect_icloud_native_capacity(capacity_observed_at_ms)? + } else { + read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { + "materialization-execution-capacity-snapshot-missing".to_string() + })?)? + }; let audit = collect_incomplete_download_audit( &args.source_root, From 8b80d2d20770f1d4edb2b379db10cec6393e010a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 14:07:00 +0900 Subject: [PATCH 08/35] fix: keep materialize help change narrow --- ...isksage-incomplete-download-materialize.rs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index 676a09764..3861c6322 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -106,7 +106,7 @@ fn parse_args(raw: &[String]) -> Result { } "--receipt-dir" => { if receipt_dir.is_some() { - return Err("--receipt-dir는 한 번만 지정할 수 있음".into()); + return Err("--receipt-dir은 한 번만 지정할 수 있음".into()); } receipt_dir = Some(PathBuf::from(value(&mut index, "--receipt-dir")?)); } @@ -318,16 +318,17 @@ fn run() -> Result<(), String> { verify_discovered_cloud_root(&home, &plan)?; let capacity_observed_at_ms = system_now_ms(); - let capacity = if args.live_icloud_capacity { - if plan.provider != CloudProvider::Icloud { - return Err("live-icloud-capacity-requires-icloud-plan".into()); - } - collect_icloud_native_capacity(capacity_observed_at_ms)? - } else { - read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { - "materialization-execution-capacity-snapshot-missing".to_string() - })?)? - }; + let capacity = + if args.live_icloud_capacity { + if plan.provider != CloudProvider::Icloud { + return Err("live-icloud-capacity-requires-icloud-plan".into()); + } + collect_icloud_native_capacity(capacity_observed_at_ms)? + } else { + read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { + "materialization-execution-capacity-snapshot-missing".to_string() + })?)? + }; let audit = collect_incomplete_download_audit( &args.source_root, From 0ccce55c054d7e58635b8d1d470baee37f3e9502 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 20:30:09 +0900 Subject: [PATCH 09/35] test: reject non-UTF8 incomplete-download CLI arguments --- .../tests/cli_help_incomplete_plans_exit.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index 4f7d12859..3e926175c 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -129,6 +129,33 @@ fn assert_help_does_not_hide_invalid_argument(binary: &Path) { ); } +#[cfg(unix)] +fn assert_non_utf8_argument_is_bounded(binary: &Path) { + use std::os::unix::ffi::OsStringExt; + + let opaque = OsString::from_vec(vec![b'-', b'-', b'o', b'p', b'a', b'q', b'u', b'e', 0xff]); + let output = command(binary) + .arg(opaque) + .output() + .expect("incomplete-download CLI must launch for non-UTF-8 argument validation"); + + assert_eq!( + output.status.code(), + Some(2), + "invalid non-UTF-8 input must use the ordinary bounded argument-error exit" + ); + assert!( + output.stdout.is_empty(), + "invalid non-UTF-8 input must not emit successful output" + ); + let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must remain valid UTF-8"); + assert!(!stderr.is_empty(), "invalid non-UTF-8 input must remain visible"); + assert!( + !stderr.contains("panicked") && !stderr.contains("thread 'main'"), + "invalid host arguments must not escape through a Rust panic" + ); +} + #[test] fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { let (_target_dir, binaries) = build_feature_gated_binaries(); @@ -137,5 +164,7 @@ fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { assert_help_success(binary, usage, "-h"); assert_invalid_argument_is_bounded(binary); assert_help_does_not_hide_invalid_argument(binary); + #[cfg(unix)] + assert_non_utf8_argument_is_bounded(binary); } } From e20f931453e21cba0566ae82d5dab17c5262b485 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 20:45:34 +0900 Subject: [PATCH 10/35] test: pin incomplete-download CLI error contracts --- .../tests/cli_help_incomplete_plans_exit.rs | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index 3e926175c..3e55503f6 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -4,18 +4,21 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command; -const BINARIES: [(&str, &str); 3] = [ +const BINARIES: [(&str, &str, &str); 3] = [ ( "disksage-incomplete-download-materialization", - "usage: disksage-incomplete-download-materialization", + "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]", + "incomplete-download-materialization-unknown-argument", ), ( "disksage-incomplete-download-recovery", - "usage: disksage-incomplete-download-recovery", + "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]", + "incomplete-download-recovery-unknown-argument", ), ( "disksage-incomplete-download-materialize", - "usage: disksage-incomplete-download-materialize", + "usage: disksage-incomplete-download-materialize --source-root ABSOLUTE_PATH --destination-plan ABSOLUTE_PRIVATE_PLAN.json --confirm-plan-fingerprint HEX64 --receipt-dir ABSOLUTE_PRIVATE_DIRECTORY --approved-by human:ID --rationale TEXT --execute (--live-icloud-capacity | --capacity-snapshot ABSOLUTE.json) [--max-entries 1..=200000] [--stale-after-days 1..=3650]", + "incomplete-download-materialize-unknown-argument", ), ]; @@ -26,7 +29,7 @@ fn build_feature_gated_binaries() -> (tempfile::TempDir, Vec) { command .current_dir(env!("CARGO_MANIFEST_DIR")) .args(["build", "--locked", "--features", "cloud-cli"]); - for (binary, _) in BINARIES { + for (binary, _, _) in BINARIES { command.args(["--bin", binary]); } let status = command @@ -41,7 +44,7 @@ fn build_feature_gated_binaries() -> (tempfile::TempDir, Vec) { let binaries = BINARIES .iter() - .map(|(binary, _)| { + .map(|(binary, _, _)| { let path = target_dir .path() .join("debug") @@ -79,13 +82,14 @@ fn assert_help_success(binary: &Path, usage: &str, flag: &str) { "successful help must not be projected through stderr" ); let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); - assert!( - stdout.contains(usage), - "help output must contain the stable usage synopsis" + assert_eq!( + stdout, + format!("{usage}\n"), + "help output must equal the stable usage synopsis" ); } -fn assert_invalid_argument_is_bounded(binary: &Path) { +fn assert_invalid_argument_is_bounded(binary: &Path, error_token: &str) { let output = command(binary) .arg("--opaque-option=not-shown") .output() @@ -100,14 +104,14 @@ fn assert_invalid_argument_is_bounded(binary: &Path) { "invalid invocation must not emit successful output on stdout" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); - assert!(!stderr.is_empty(), "invalid invocation must remain visible"); + assert!(stderr.contains(error_token), "invalid invocation must use its stable error token"); assert!( !stderr.contains("not-shown"), "invalid diagnostics must not echo arbitrary argument payloads" ); } -fn assert_help_does_not_hide_invalid_argument(binary: &Path) { +fn assert_help_does_not_hide_invalid_argument(binary: &Path, error_token: &str) { let output = command(binary) .args(["--help", "--opaque-option=not-shown"]) .output() @@ -122,7 +126,7 @@ fn assert_help_does_not_hide_invalid_argument(binary: &Path) { "mixed invalid invocation must not emit successful help on stdout" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); - assert!(!stderr.is_empty(), "mixed invalid invocation must remain visible"); + assert!(stderr.contains(error_token), "mixed invalid invocation must use its stable error token"); assert!( !stderr.contains("not-shown"), "mixed invalid diagnostics must not echo arbitrary argument payloads" @@ -130,7 +134,7 @@ fn assert_help_does_not_hide_invalid_argument(binary: &Path) { } #[cfg(unix)] -fn assert_non_utf8_argument_is_bounded(binary: &Path) { +fn assert_non_utf8_argument_is_bounded(binary: &Path, error_token: &str) { use std::os::unix::ffi::OsStringExt; let opaque = OsString::from_vec(vec![b'-', b'-', b'o', b'p', b'a', b'q', b'u', b'e', 0xff]); @@ -149,7 +153,7 @@ fn assert_non_utf8_argument_is_bounded(binary: &Path) { "invalid non-UTF-8 input must not emit successful output" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must remain valid UTF-8"); - assert!(!stderr.is_empty(), "invalid non-UTF-8 input must remain visible"); + assert!(stderr.contains(error_token), "invalid non-UTF-8 input must use its stable error token"); assert!( !stderr.contains("panicked") && !stderr.contains("thread 'main'"), "invalid host arguments must not escape through a Rust panic" @@ -159,12 +163,12 @@ fn assert_non_utf8_argument_is_bounded(binary: &Path) { #[test] fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { let (_target_dir, binaries) = build_feature_gated_binaries(); - for ((_, usage), binary) in BINARIES.iter().zip(&binaries) { + for ((_, usage, error_token), binary) in BINARIES.iter().zip(&binaries) { assert_help_success(binary, usage, "--help"); assert_help_success(binary, usage, "-h"); - assert_invalid_argument_is_bounded(binary); - assert_help_does_not_hide_invalid_argument(binary); + assert_invalid_argument_is_bounded(binary, error_token); + assert_help_does_not_hide_invalid_argument(binary, error_token); #[cfg(unix)] - assert_non_utf8_argument_is_bounded(binary); + assert_non_utf8_argument_is_bounded(binary, error_token); } } From ac4c2358c6e64aaa62069aee716d0fcbcfe39416 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 03:11:08 +0900 Subject: [PATCH 11/35] fix: bound materialization CLI non-UTF8 args --- .../bin/disksage-incomplete-download-materialization.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index 85209a4ad..a4a8af2a0 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -114,7 +114,14 @@ fn system_now_ms() -> u64 { #[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args().skip(1).collect::>(); + let raw = std::env::args_os() + .skip(1) + .map(|argument| { + argument.into_string().map_err(|_| { + "incomplete-download-materialization-unknown-argument".to_string() + }) + }) + .collect::, _>>()?; if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { println!("{}", usage()); return Ok(()); From b52328b8cfda40071b7f4ecdf84111e0b2bbf948 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 04:35:42 +0900 Subject: [PATCH 12/35] fix: bound incomplete recovery argv decoding --- .../src/bin/disksage-incomplete-download-recovery.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index 256d6482d..9ece08546 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -112,7 +112,14 @@ fn system_now_ms() -> u64 { #[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args().skip(1).collect::>(); + let raw = std::env::args_os() + .skip(1) + .map(|argument| { + argument + .into_string() + .map_err(|_| "incomplete-download-recovery-unknown-argument".to_string()) + }) + .collect::, _>>()?; if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { println!("{}", usage()); return Ok(()); From 558dc5d4828ee1f18158c345651838666e1bcef4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 04:37:05 +0900 Subject: [PATCH 13/35] fix: bound materialization argv decoding --- .../bin/disksage-incomplete-download-materialize.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index 3861c6322..a50b939e8 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -94,7 +94,7 @@ fn parse_args(raw: &[String]) -> Result { } "--destination-plan" => { if destination_plan.is_some() { - return Err("--destination-plan은 한 번만 지정할 수 있음".into()); + return Err("--destination-plan는 한 번만 지정할 수 있음".replace("는", "은")); } destination_plan = Some(PathBuf::from(value(&mut index, "--destination-plan")?)); } @@ -298,7 +298,14 @@ fn verify_discovered_cloud_root( #[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args().skip(1).collect::>(); + let raw = std::env::args_os() + .skip(1) + .map(|argument| { + argument + .into_string() + .map_err(|_| "incomplete-download-materialize-unknown-argument".to_string()) + }) + .collect::, _>>()?; if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { println!("{}", usage()); return Ok(()); From 75b8d36646292e71070af51aa141607482fd5496 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 04:38:04 +0900 Subject: [PATCH 14/35] fix: preserve destination-plan diagnostic --- src-tauri/src/bin/disksage-incomplete-download-materialize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index a50b939e8..ca1c87b27 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -94,7 +94,7 @@ fn parse_args(raw: &[String]) -> Result { } "--destination-plan" => { if destination_plan.is_some() { - return Err("--destination-plan는 한 번만 지정할 수 있음".replace("는", "은")); + return Err("--destination-plan은 한 번만 지정할 수 있음".into()); } destination_plan = Some(PathBuf::from(value(&mut index, "--destination-plan")?)); } From 2d231f90545ed3571ee26b9660e3cc8c8120cb43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 04:53:05 +0900 Subject: [PATCH 15/35] fix: keep mixed help fail-closed --- .../src/bin/disksage-incomplete-download-materialization.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index a4a8af2a0..642f17ad0 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -83,7 +83,6 @@ fn parse_args(raw: &[String]) -> Result { } private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); } - "--help" | "-h" => return Err(usage()), _unknown => return Err("incomplete-download-materialization-unknown-argument".into()), } index += 1; From 6eeb1a4de38e96de303315eea761904c32513572 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 04:53:54 +0900 Subject: [PATCH 16/35] fix: keep recovery mixed help fail-closed --- src-tauri/src/bin/disksage-incomplete-download-recovery.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index 9ece08546..3c3d447de 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -81,7 +81,6 @@ fn parse_args(raw: &[String]) -> Result { } private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); } - "--help" | "-h" => return Err(usage()), _unknown => return Err("incomplete-download-recovery-unknown-argument".into()), } index += 1; From c134b7915c0c795daca99239f11546f0d5f8b0fd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:07:08 +0900 Subject: [PATCH 17/35] fix: keep mixed help invocation fail-closed --- src-tauri/src/bin/disksage-incomplete-download-materialize.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index ca1c87b27..bae70ea2f 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -162,7 +162,6 @@ fn parse_args(raw: &[String]) -> Result { } execute = true; } - "--help" | "-h" => return Err(usage()), _unknown => return Err("incomplete-download-materialize-unknown-argument".into()), } index += 1; From a57837412541bb935ca634e6b9cf129eb81052b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:33:41 -0700 Subject: [PATCH 18/35] test: preserve native incomplete-download paths --- .../tests/cli_help_incomplete_plans_exit.rs | 107 +++++++++++++++++- 1 file changed, 104 insertions(+), 3 deletions(-) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index 3e55503f6..0fef6afb0 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -4,6 +4,12 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command; +const MATERIALIZATION_SOURCE: &str = + include_str!("../src/bin/disksage-incomplete-download-materialization.rs"); +const RECOVERY_SOURCE: &str = include_str!("../src/bin/disksage-incomplete-download-recovery.rs"); +const MATERIALIZE_SOURCE: &str = + include_str!("../src/bin/disksage-incomplete-download-materialize.rs"); + const BINARIES: [(&str, &str, &str); 3] = [ ( "disksage-incomplete-download-materialization", @@ -104,7 +110,10 @@ fn assert_invalid_argument_is_bounded(binary: &Path, error_token: &str) { "invalid invocation must not emit successful output on stdout" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); - assert!(stderr.contains(error_token), "invalid invocation must use its stable error token"); + assert!( + stderr.contains(error_token), + "invalid invocation must use its stable error token" + ); assert!( !stderr.contains("not-shown"), "invalid diagnostics must not echo arbitrary argument payloads" @@ -126,7 +135,10 @@ fn assert_help_does_not_hide_invalid_argument(binary: &Path, error_token: &str) "mixed invalid invocation must not emit successful help on stdout" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); - assert!(stderr.contains(error_token), "mixed invalid invocation must use its stable error token"); + assert!( + stderr.contains(error_token), + "mixed invalid invocation must use its stable error token" + ); assert!( !stderr.contains("not-shown"), "mixed invalid diagnostics must not echo arbitrary argument payloads" @@ -153,13 +165,100 @@ fn assert_non_utf8_argument_is_bounded(binary: &Path, error_token: &str) { "invalid non-UTF-8 input must not emit successful output" ); let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must remain valid UTF-8"); - assert!(stderr.contains(error_token), "invalid non-UTF-8 input must use its stable error token"); + assert!( + stderr.contains(error_token), + "invalid non-UTF-8 input must use its stable error token" + ); assert!( !stderr.contains("panicked") && !stderr.contains("thread 'main'"), "invalid host arguments must not escape through a Rust panic" ); } +#[cfg(unix)] +fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { + use std::os::unix::ffi::OsStringExt; + + let parent = tempfile::tempdir().expect("native-path parent fixture must be created"); + let native_root = parent.path().join(OsString::from_vec(vec![ + b'i', b'n', b'c', b'o', b'm', b'p', b'l', b'e', b't', b'e', b'-', 0xff, + ])); + std::fs::create_dir(&native_root).expect("native non-UTF-8 source root must be created"); + + for binary in &binaries[..2] { + let output = command(binary) + .arg("--root") + .arg(&native_root) + .output() + .expect("read-only incomplete-download CLI must launch with a native root"); + assert!( + output.status.success(), + "valid native filesystem roots must reach the read-only domain boundary; status={:?}, stderr={:?}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty(), "successful native-path planning must not emit argument diagnostics"); + let summary: serde_json::Value = serde_json::from_slice(&output.stdout) + .expect("successful native-path planning must emit machine-readable JSON"); + assert!(summary.is_object(), "planning output must remain a JSON object"); + } + + let missing_plan = parent.path().join(OsString::from_vec(vec![ + b'p', b'l', b'a', b'n', b'-', 0xff, b'.', b'j', b's', b'o', b'n', + ])); + let source_root = tempfile::tempdir().expect("materialization source fixture must be created"); + let receipt_dir = tempfile::tempdir().expect("receipt directory fixture must be created"); + let capacity_snapshot = parent.path().join("capacity.json"); + let output = command(&binaries[2]) + .arg("--source-root") + .arg(source_root.path()) + .arg("--destination-plan") + .arg(&missing_plan) + .arg("--confirm-plan-fingerprint") + .arg("a".repeat(64)) + .arg("--receipt-dir") + .arg(receipt_dir.path()) + .arg("--approved-by") + .arg("human:test") + .arg("--rationale") + .arg("native path admission") + .arg("--execute") + .arg("--capacity-snapshot") + .arg(&capacity_snapshot) + .output() + .expect("materialization execution CLI must launch with a native plan path"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("materialization diagnostic must be UTF-8"); + assert!( + stderr.contains("materialization-execution-destination-plan-unavailable"), + "native plan path must reach bounded file admission instead of argument decoding: {stderr}" + ); + assert!(!stderr.contains("incomplete-download-materialize-unknown-argument")); +} + +#[test] +fn incomplete_download_coverage_contract_keeps_shipped_entrypoints_real() { + for (name, source) in [ + ("disksage-incomplete-download-materialization", MATERIALIZATION_SOURCE), + ("disksage-incomplete-download-recovery", RECOVERY_SOURCE), + ("disksage-incomplete-download-materialize", MATERIALIZE_SOURCE), + ] { + assert!( + !source.contains("#[cfg(coverage)]\nfn main()"), + "coverage must never replace the shipped {name} entrypoint with a synthetic main" + ); + assert!( + !source.contains("#[cfg(not(coverage))]\nfn main()"), + "the shipped {name} entrypoint must remain present under instrumentation" + ); + assert!( + !source.contains("#[cfg(not(coverage))]\nfn run()"), + "the shipped {name} runtime must remain present under instrumentation" + ); + } +} + #[test] fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { let (_target_dir, binaries) = build_feature_gated_binaries(); @@ -171,4 +270,6 @@ fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { #[cfg(unix)] assert_non_utf8_argument_is_bounded(binary, error_token); } + #[cfg(unix)] + assert_native_non_utf8_paths_reach_domain_boundaries(&binaries); } From 168d8a45cefd4e68ba42aa5b8253a268b50255e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:34:10 -0700 Subject: [PATCH 19/35] fix: preserve native materialization paths --- ...age-incomplete-download-materialization.rs | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index 642f17ad0..c7f13d565 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -9,6 +9,7 @@ use disksage_lib::incomplete_download_recovery::{ validate_incomplete_download_recovery, RecoveryValidationLimits, }; use disksage_lib::private_evidence::write_private_json_create_new; +use std::ffi::{OsStr, OsString}; use std::path::{Component, Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -35,28 +36,38 @@ fn usage() -> String { ) } -fn parse_args(raw: &[String]) -> Result { +fn next_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + *index += 1; + raw.get(*index) + .cloned() + .ok_or_else(|| format!("{flag} 값이 필요함")) +} + +fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + next_value(raw, index, flag)? + .into_string() + .map_err(|_| format!("{flag} 값은 UTF-8 텍스트여야 함")) +} + +fn parse_args(raw: &[OsString]) -> Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS; let mut private_output = None; let mut index = 0usize; while index < raw.len() { - let value = |index: &mut usize, flag: &str| -> Result { - *index += 1; - raw.get(*index) - .cloned() - .ok_or_else(|| format!("{flag} 값이 필요함")) - }; - match raw[index].as_str() { + let option = raw[index] + .to_str() + .ok_or_else(|| "incomplete-download-materialization-unknown-argument".to_string())?; + match option { "--root" => { if root.is_some() { return Err("--root는 한 번만 지정할 수 있음".into()); } - root = Some(PathBuf::from(value(&mut index, "--root")?)); + root = Some(PathBuf::from(next_value(raw, &mut index, "--root")?)); } "--max-entries" => { - let parsed = value(&mut index, "--max-entries")? + let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; if parsed == 0 || parsed > DEFAULT_MAX_ENTRIES { @@ -67,7 +78,7 @@ fn parse_args(raw: &[String]) -> Result { max_entries = parsed; } "--stale-after-days" => { - let parsed = value(&mut index, "--stale-after-days")? + let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; if !(1..=MAX_STALE_AFTER_DAYS).contains(&parsed) { @@ -81,7 +92,11 @@ fn parse_args(raw: &[String]) -> Result { if private_output.is_some() { return Err("--private-output은 한 번만 지정할 수 있음".into()); } - private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); + private_output = Some(PathBuf::from(next_value( + raw, + &mut index, + "--private-output", + )?)); } _unknown => return Err("incomplete-download-materialization-unknown-argument".into()), } @@ -111,17 +126,14 @@ fn system_now_ms() -> u64 { .unwrap_or(0) } -#[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args_os() - .skip(1) - .map(|argument| { - argument.into_string().map_err(|_| { - "incomplete-download-materialization-unknown-argument".to_string() - }) - }) - .collect::, _>>()?; - if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + let raw = std::env::args_os().skip(1).collect::>(); + if raw.len() == 1 + && matches!( + raw.first().map(OsString::as_os_str), + Some(argument) if argument == OsStr::new("--help") || argument == OsStr::new("-h") + ) + { println!("{}", usage()); return Ok(()); } @@ -161,7 +173,6 @@ fn run() -> Result<(), String> { Ok(()) } -#[cfg(not(coverage))] fn main() { if let Err(error) = run() { eprintln!("DiskSage incomplete download materialization plan: {error}"); @@ -169,9 +180,6 @@ fn main() { } } -#[cfg(coverage)] -fn main() {} - #[cfg(test)] mod tests { use super::*; From b37234236a63e60ddf81dfa83586ea9a260c3a8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:34:45 -0700 Subject: [PATCH 20/35] fix: preserve native recovery paths --- .../disksage-incomplete-download-recovery.rs | 60 +++++++++++-------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index 3c3d447de..b4c4dcbb3 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -7,6 +7,7 @@ use disksage_lib::incomplete_download_recovery::{ RecoveryValidationLimits, }; use disksage_lib::private_evidence::write_private_json_create_new; +use std::ffi::{OsStr, OsString}; use std::path::{Component, Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -33,28 +34,38 @@ fn usage() -> String { ) } -fn parse_args(raw: &[String]) -> Result { +fn next_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + *index += 1; + raw.get(*index) + .cloned() + .ok_or_else(|| format!("{flag} 값이 필요함")) +} + +fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + next_value(raw, index, flag)? + .into_string() + .map_err(|_| format!("{flag} 값은 UTF-8 텍스트여야 함")) +} + +fn parse_args(raw: &[OsString]) -> Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS; let mut private_output = None; let mut index = 0usize; while index < raw.len() { - let value = |index: &mut usize, flag: &str| -> Result { - *index += 1; - raw.get(*index) - .cloned() - .ok_or_else(|| format!("{flag} 값이 필요함")) - }; - match raw[index].as_str() { + let option = raw[index] + .to_str() + .ok_or_else(|| "incomplete-download-recovery-unknown-argument".to_string())?; + match option { "--root" => { if root.is_some() { return Err("--root는 한 번만 지정할 수 있음".into()); } - root = Some(PathBuf::from(value(&mut index, "--root")?)); + root = Some(PathBuf::from(next_value(raw, &mut index, "--root")?)); } "--max-entries" => { - let parsed = value(&mut index, "--max-entries")? + let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; if parsed == 0 || parsed > DEFAULT_MAX_ENTRIES { @@ -65,7 +76,7 @@ fn parse_args(raw: &[String]) -> Result { max_entries = parsed; } "--stale-after-days" => { - let parsed = value(&mut index, "--stale-after-days")? + let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; if !(1..=MAX_STALE_AFTER_DAYS).contains(&parsed) { @@ -79,7 +90,11 @@ fn parse_args(raw: &[String]) -> Result { if private_output.is_some() { return Err("--private-output은 한 번만 지정할 수 있음".into()); } - private_output = Some(PathBuf::from(value(&mut index, "--private-output")?)); + private_output = Some(PathBuf::from(next_value( + raw, + &mut index, + "--private-output", + )?)); } _unknown => return Err("incomplete-download-recovery-unknown-argument".into()), } @@ -109,17 +124,14 @@ fn system_now_ms() -> u64 { .unwrap_or(0) } -#[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args_os() - .skip(1) - .map(|argument| { - argument - .into_string() - .map_err(|_| "incomplete-download-recovery-unknown-argument".to_string()) - }) - .collect::, _>>()?; - if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + let raw = std::env::args_os().skip(1).collect::>(); + if raw.len() == 1 + && matches!( + raw.first().map(OsString::as_os_str), + Some(argument) if argument == OsStr::new("--help") || argument == OsStr::new("-h") + ) + { println!("{}", usage()); return Ok(()); } @@ -155,7 +167,6 @@ fn run() -> Result<(), String> { Ok(()) } -#[cfg(not(coverage))] fn main() { if let Err(error) = run() { eprintln!("DiskSage incomplete download recovery validation: {error}"); @@ -163,9 +174,6 @@ fn main() { } } -#[cfg(coverage)] -fn main() {} - #[cfg(test)] mod tests { use super::*; From d8a2400bf2f0ab8f6f99219453ec8b5cf1defcad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:35:35 -0700 Subject: [PATCH 21/35] fix: preserve native materialize paths --- ...isksage-incomplete-download-materialize.rs | 115 +++++++++++------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index bae70ea2f..889e8af82 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -15,6 +15,7 @@ use disksage_lib::incomplete_download_recovery::{ validate_incomplete_download_recovery, RecoveryValidationLimits, }; use disksage_lib::provider_capacity::{collect_icloud_native_capacity, CloudCapacitySnapshot}; +use std::ffi::{OsStr, OsString}; use std::io::Read; use std::path::{Component, Path, PathBuf}; @@ -65,7 +66,20 @@ fn usage() -> String { ) } -fn parse_args(raw: &[String]) -> Result { +fn next_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + *index += 1; + raw.get(*index) + .cloned() + .ok_or_else(|| format!("{flag} 값이 필요함")) +} + +fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result { + next_value(raw, index, flag)? + .into_string() + .map_err(|_| format!("{flag} 값은 UTF-8 텍스트여야 함")) +} + +fn parse_args(raw: &[OsString]) -> Result { let mut source_root = None; let mut destination_plan = None; let mut confirmed_plan_fingerprint = None; @@ -79,51 +93,64 @@ fn parse_args(raw: &[String]) -> Result { let mut execute = false; let mut index = 0usize; while index < raw.len() { - let value = |index: &mut usize, flag: &str| -> Result { - *index += 1; - raw.get(*index) - .cloned() - .ok_or_else(|| format!("{flag} 값이 필요함")) - }; - match raw[index].as_str() { + let option = raw[index] + .to_str() + .ok_or_else(|| "incomplete-download-materialize-unknown-argument".to_string())?; + match option { "--source-root" => { if source_root.is_some() { return Err("--source-root는 한 번만 지정할 수 있음".into()); } - source_root = Some(PathBuf::from(value(&mut index, "--source-root")?)); + source_root = Some(PathBuf::from(next_value( + raw, + &mut index, + "--source-root", + )?)); } "--destination-plan" => { if destination_plan.is_some() { return Err("--destination-plan은 한 번만 지정할 수 있음".into()); } - destination_plan = Some(PathBuf::from(value(&mut index, "--destination-plan")?)); + destination_plan = Some(PathBuf::from(next_value( + raw, + &mut index, + "--destination-plan", + )?)); } "--confirm-plan-fingerprint" => { if confirmed_plan_fingerprint.is_some() { return Err("--confirm-plan-fingerprint는 한 번만 지정할 수 있음".into()); } - confirmed_plan_fingerprint = Some(value(&mut index, "--confirm-plan-fingerprint")?); + confirmed_plan_fingerprint = Some(next_text_value( + raw, + &mut index, + "--confirm-plan-fingerprint", + )?); } "--receipt-dir" => { if receipt_dir.is_some() { return Err("--receipt-dir은 한 번만 지정할 수 있음".into()); } - receipt_dir = Some(PathBuf::from(value(&mut index, "--receipt-dir")?)); + receipt_dir = Some(PathBuf::from(next_value( + raw, + &mut index, + "--receipt-dir", + )?)); } "--approved-by" => { if approved_by.is_some() { return Err("--approved-by는 한 번만 지정할 수 있음".into()); } - approved_by = Some(value(&mut index, "--approved-by")?); + approved_by = Some(next_text_value(raw, &mut index, "--approved-by")?); } "--rationale" => { if rationale.is_some() { return Err("--rationale은 한 번만 지정할 수 있음".into()); } - rationale = Some(value(&mut index, "--rationale")?); + rationale = Some(next_text_value(raw, &mut index, "--rationale")?); } "--max-entries" => { - let parsed = value(&mut index, "--max-entries")? + let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; if parsed == 0 || parsed > DEFAULT_MAX_ENTRIES { @@ -134,7 +161,7 @@ fn parse_args(raw: &[String]) -> Result { max_entries = parsed; } "--stale-after-days" => { - let parsed = value(&mut index, "--stale-after-days")? + let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; if !(1..=MAX_STALE_AFTER_DAYS).contains(&parsed) { @@ -154,7 +181,11 @@ fn parse_args(raw: &[String]) -> Result { if capacity_snapshot.is_some() { return Err("--capacity-snapshot은 한 번만 지정할 수 있음".into()); } - capacity_snapshot = Some(PathBuf::from(value(&mut index, "--capacity-snapshot")?)); + capacity_snapshot = Some(PathBuf::from(next_value( + raw, + &mut index, + "--capacity-snapshot", + )?)); } "--execute" => { if execute { @@ -295,17 +326,14 @@ fn verify_discovered_cloud_root( Ok(()) } -#[cfg(not(coverage))] fn run() -> Result<(), String> { - let raw = std::env::args_os() - .skip(1) - .map(|argument| { - argument - .into_string() - .map_err(|_| "incomplete-download-materialize-unknown-argument".to_string()) - }) - .collect::, _>>()?; - if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { + let raw = std::env::args_os().skip(1).collect::>(); + if raw.len() == 1 + && matches!( + raw.first().map(OsString::as_os_str), + Some(argument) if argument == OsStr::new("--help") || argument == OsStr::new("-h") + ) + { println!("{}", usage()); return Ok(()); } @@ -324,17 +352,16 @@ fn run() -> Result<(), String> { verify_discovered_cloud_root(&home, &plan)?; let capacity_observed_at_ms = system_now_ms(); - let capacity = - if args.live_icloud_capacity { - if plan.provider != CloudProvider::Icloud { - return Err("live-icloud-capacity-requires-icloud-plan".into()); - } - collect_icloud_native_capacity(capacity_observed_at_ms)? - } else { - read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { - "materialization-execution-capacity-snapshot-missing".to_string() - })?)? - }; + let capacity = if args.live_icloud_capacity { + if plan.provider != CloudProvider::Icloud { + return Err("live-icloud-capacity-requires-icloud-plan".into()); + } + collect_icloud_native_capacity(capacity_observed_at_ms)? + } else { + read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { + "materialization-execution-capacity-snapshot-missing".to_string() + })?)? + }; let audit = collect_incomplete_download_audit( &args.source_root, @@ -381,7 +408,6 @@ fn run() -> Result<(), String> { Ok(()) } -#[cfg(not(coverage))] fn main() { if let Err(error) = run() { eprintln!("DiskSage incomplete download materialization execution: {error}"); @@ -389,21 +415,18 @@ fn main() { } } -#[cfg(coverage)] -fn main() {} - #[cfg(test)] mod tests { use super::*; - fn required() -> Vec { + fn required() -> Vec { vec![ "--source-root".into(), "/source".into(), "--destination-plan".into(), "/private/plan.json".into(), "--confirm-plan-fingerprint".into(), - "a".repeat(64), + "a".repeat(64).into(), "--receipt-dir".into(), "/private/receipts".into(), "--approved-by".into(), @@ -427,7 +450,7 @@ mod tests { #[test] fn rejects_missing_execute_bad_attribution_and_ambiguous_capacity() { let mut missing_execute = required(); - missing_execute.retain(|value| value != "--execute"); + missing_execute.retain(|value| value != OsStr::new("--execute")); missing_execute.push("--live-icloud-capacity".into()); assert!(parse_args(&missing_execute).is_err()); @@ -442,7 +465,7 @@ mod tests { let mut bad_attribution = required(); let position = bad_attribution .iter() - .position(|value| value == "human:test") + .position(|value| value == OsStr::new("human:test")) .unwrap(); bad_attribution[position] = "agent:test".into(); bad_attribution.push("--live-icloud-capacity".into()); From 78125279c7059ad8b7deb282a8cfe79c5e69d90f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:38:06 -0700 Subject: [PATCH 22/35] test: reject duplicate incomplete-download limits --- .../tests/cli_help_incomplete_plans_exit.rs | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index 0fef6afb0..b931492e5 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -197,7 +197,10 @@ fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { output.status.code(), String::from_utf8_lossy(&output.stderr) ); - assert!(output.stderr.is_empty(), "successful native-path planning must not emit argument diagnostics"); + assert!( + output.stderr.is_empty(), + "successful native-path planning must not emit argument diagnostics" + ); let summary: serde_json::Value = serde_json::from_slice(&output.stdout) .expect("successful native-path planning must emit machine-readable JSON"); assert!(summary.is_object(), "planning output must remain a JSON object"); @@ -237,6 +240,60 @@ fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { assert!(!stderr.contains("incomplete-download-materialize-unknown-argument")); } +fn assert_read_only_duplicate_limit_is_bounded(binary: &Path, flag: &str) { + let root = tempfile::tempdir().expect("duplicate-limit root fixture must be created"); + let output = command(binary) + .arg("--root") + .arg(root.path()) + .arg(flag) + .arg("1") + .arg(flag) + .arg("2") + .output() + .expect("read-only incomplete-download CLI must launch for duplicate-limit validation"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); + assert!( + stderr.contains(&format!("{flag}는 한 번만 지정할 수 있음")), + "duplicate bounded limit must fail before domain work: {stderr}" + ); +} + +fn assert_materialize_duplicate_limit_is_bounded(binary: &Path, flag: &str) { + let source_root = tempfile::tempdir().expect("duplicate-limit source fixture must be created"); + let private = tempfile::tempdir().expect("duplicate-limit private fixture must be created"); + let output = command(binary) + .arg("--source-root") + .arg(source_root.path()) + .arg("--destination-plan") + .arg(private.path().join("missing-plan.json")) + .arg("--confirm-plan-fingerprint") + .arg("a".repeat(64)) + .arg("--receipt-dir") + .arg(private.path().join("receipts")) + .arg("--approved-by") + .arg("human:test") + .arg("--rationale") + .arg("duplicate limit admission") + .arg(flag) + .arg("1") + .arg(flag) + .arg("2") + .arg("--execute") + .arg("--capacity-snapshot") + .arg(private.path().join("capacity.json")) + .output() + .expect("materialize CLI must launch for duplicate-limit validation"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); + assert!( + stderr.contains(&format!("{flag}는 한 번만 지정할 수 있음")), + "duplicate bounded limit must fail before plan or filesystem admission: {stderr}" + ); +} + #[test] fn incomplete_download_coverage_contract_keeps_shipped_entrypoints_real() { for (name, source) in [ @@ -270,6 +327,12 @@ fn incomplete_download_help_is_successful_and_invalid_arguments_are_bounded() { #[cfg(unix)] assert_non_utf8_argument_is_bounded(binary, error_token); } + for binary in &binaries[..2] { + assert_read_only_duplicate_limit_is_bounded(binary, "--max-entries"); + assert_read_only_duplicate_limit_is_bounded(binary, "--stale-after-days"); + } + assert_materialize_duplicate_limit_is_bounded(&binaries[2], "--max-entries"); + assert_materialize_duplicate_limit_is_bounded(&binaries[2], "--stale-after-days"); #[cfg(unix)] assert_native_non_utf8_paths_reach_domain_boundaries(&binaries); } From e3da2f2471d1082f5034c6ec9bae43de51b39c65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:39:44 -0700 Subject: [PATCH 23/35] fix: reject duplicate materialization limits --- .../disksage-incomplete-download-materialization.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index c7f13d565..774f26905 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -52,7 +52,9 @@ fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; + let mut max_entries_seen = false; let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS; + let mut stale_after_days_seen = false; let mut private_output = None; let mut index = 0usize; while index < raw.len() { @@ -67,6 +69,10 @@ fn parse_args(raw: &[OsString]) -> Result { root = Some(PathBuf::from(next_value(raw, &mut index, "--root")?)); } "--max-entries" => { + if max_entries_seen { + return Err("--max-entries는 한 번만 지정할 수 있음".into()); + } + max_entries_seen = true; let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; @@ -78,6 +84,10 @@ fn parse_args(raw: &[OsString]) -> Result { max_entries = parsed; } "--stale-after-days" => { + if stale_after_days_seen { + return Err("--stale-after-days는 한 번만 지정할 수 있음".into()); + } + stale_after_days_seen = true; let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; From fef8544d5f96fb44842fa02a8ac2a508c62e2125 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:40:21 -0700 Subject: [PATCH 24/35] fix: reject duplicate recovery limits --- .../src/bin/disksage-incomplete-download-recovery.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index b4c4dcbb3..cafeefd47 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -50,7 +50,9 @@ fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result Result { let mut root = None; let mut max_entries = DEFAULT_MAX_ENTRIES; + let mut max_entries_seen = false; let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS; + let mut stale_after_days_seen = false; let mut private_output = None; let mut index = 0usize; while index < raw.len() { @@ -65,6 +67,10 @@ fn parse_args(raw: &[OsString]) -> Result { root = Some(PathBuf::from(next_value(raw, &mut index, "--root")?)); } "--max-entries" => { + if max_entries_seen { + return Err("--max-entries는 한 번만 지정할 수 있음".into()); + } + max_entries_seen = true; let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; @@ -76,6 +82,10 @@ fn parse_args(raw: &[OsString]) -> Result { max_entries = parsed; } "--stale-after-days" => { + if stale_after_days_seen { + return Err("--stale-after-days는 한 번만 지정할 수 있음".into()); + } + stale_after_days_seen = true; let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; From 6a4b1867deecf9affaa39245be66a4fb2e4f508b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:41:30 -0700 Subject: [PATCH 25/35] fix: reject duplicate materialize limits --- .../bin/disksage-incomplete-download-materialize.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index 889e8af82..78f381822 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -87,7 +87,9 @@ fn parse_args(raw: &[OsString]) -> Result { let mut approved_by = None; let mut rationale = None; let mut max_entries = DEFAULT_MAX_ENTRIES; + let mut max_entries_seen = false; let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS; + let mut stale_after_days_seen = false; let mut live_icloud_capacity = false; let mut capacity_snapshot = None; let mut execute = false; @@ -150,6 +152,10 @@ fn parse_args(raw: &[OsString]) -> Result { rationale = Some(next_text_value(raw, &mut index, "--rationale")?); } "--max-entries" => { + if max_entries_seen { + return Err("--max-entries는 한 번만 지정할 수 있음".into()); + } + max_entries_seen = true; let parsed = next_text_value(raw, &mut index, "--max-entries")? .parse::() .map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?; @@ -161,6 +167,10 @@ fn parse_args(raw: &[OsString]) -> Result { max_entries = parsed; } "--stale-after-days" => { + if stale_after_days_seen { + return Err("--stale-after-days는 한 번만 지정할 수 있음".into()); + } + stale_after_days_seen = true; let parsed = next_text_value(raw, &mut index, "--stale-after-days")? .parse::() .map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?; From 8593746f59269346b4dd8c2def7976116e488528 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 04:29:01 +0900 Subject: [PATCH 26/35] test: accept bounded empty materialization roots --- .../tests/cli_help_incomplete_plans_exit.rs | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index b931492e5..d20bb9fd5 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -183,7 +183,15 @@ fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { let native_root = parent.path().join(OsString::from_vec(vec![ b'i', b'n', b'c', b'o', b'm', b'p', b'l', b'e', b't', b'e', b'-', 0xff, ])); - std::fs::create_dir(&native_root).expect("native non-UTF-8 source root must be created"); + if let Err(error) = std::fs::create_dir(&native_root) { + #[cfg(target_os = "macos")] + if error.raw_os_error() == Some(libc::EILSEQ) { + // APFS rejects this byte under the active locale; Linux CI exercises the + // lossless native-path boundary while macOS keeps the unsupported case explicit. + return; + } + panic!("native non-UTF-8 source root must be created: {error}"); + } for binary in &binaries[..2] { let output = command(binary) @@ -191,19 +199,28 @@ fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { .arg(&native_root) .output() .expect("read-only incomplete-download CLI must launch with a native root"); - assert!( - output.status.success(), - "valid native filesystem roots must reach the read-only domain boundary; status={:?}, stderr={:?}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - ); - assert!( - output.stderr.is_empty(), - "successful native-path planning must not emit argument diagnostics" - ); - let summary: serde_json::Value = serde_json::from_slice(&output.stdout) - .expect("successful native-path planning must emit machine-readable JSON"); - assert!(summary.is_object(), "planning output must remain a JSON object"); + let stderr = String::from_utf8(output.stderr) + .expect("native-path diagnostics must remain valid UTF-8"); + if output.status.success() { + assert!( + stderr.is_empty(), + "successful native-path planning must not emit diagnostics" + ); + let summary: serde_json::Value = serde_json::from_slice(&output.stdout) + .expect("successful native-path planning must emit machine-readable JSON"); + assert!( + summary.is_object(), + "planning output must remain a JSON object" + ); + } else { + assert_eq!(output.status.code(), Some(2)); + assert!( + stderr.contains("materialization-unit-set-empty-or-duplicate"), + "native path must reach the bounded materialization domain error, not argument parsing: {stderr}" + ); + assert!(output.stdout.is_empty()); + } + assert!(!stderr.contains("incomplete-download-materialization-unknown-argument")); } let missing_plan = parent.path().join(OsString::from_vec(vec![ From d9e3a64374afe819b92d48785b54103caca521c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:05:40 -0700 Subject: [PATCH 27/35] test: bind release verifier to matrix artifact namespaces --- ...eArtifactVerifierDirectoryContract.test.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/lib/releaseArtifactVerifierDirectoryContract.test.ts diff --git a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts new file mode 100644 index 000000000..1fedc5a41 --- /dev/null +++ b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts @@ -0,0 +1,69 @@ +import { createHash } from 'node:crypto'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const runAttempt = '1'; +const platformDirectories = { + linux: `release-disksage-ubuntu-22.04-${runAttempt}`, + windows: `release-disksage-windows-2022-${runAttempt}`, + macos: `release-disksage-macos-latest-${runAttempt}`, +} as const; + +function write(path: string, bytes: Buffer | string) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, bytes); +} + +function addCli(artifactRoot: string, directory: string, name: string) { + const bytes = Buffer.from(`cli:${name}`); + const assetPath = join(artifactRoot, directory, name); + write(assetPath, bytes); + write( + `${assetPath}.sha256`, + `${createHash('sha256').update(bytes).digest('hex')} ${name}\n`, + ); +} + +describe('release artifact verifier directory contract', () => { + it.runIf(process.platform !== 'win32')( + 'accepts the exact platform namespaces uploaded by the release matrix', + () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); + const artifactRoot = join(fixtureRoot, 'release-artifacts'); + try { + write(join(artifactRoot, platformDirectories.linux, 'bundle/deb/disksage.deb'), 'deb'); + write(join(artifactRoot, platformDirectories.linux, 'bundle/appimage/disksage.AppImage'), 'appimage'); + write(join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'), 'msi'); + write(join(artifactRoot, platformDirectories.windows, 'bundle/nsis/disksage-setup.exe'), 'nsis'); + write(join(artifactRoot, platformDirectories.macos, 'bundle/dmg/disksage.dmg'), 'dmg'); + + addCli(artifactRoot, platformDirectories.linux, 'disksage-cloud-plan-linux-x86_64'); + addCli(artifactRoot, platformDirectories.linux, 'disksage-duplicate-audit-linux-x86_64'); + addCli(artifactRoot, platformDirectories.windows, 'disksage-cloud-plan-windows-x86_64.exe'); + addCli(artifactRoot, platformDirectories.windows, 'disksage-duplicate-audit-windows-x86_64.exe'); + addCli(artifactRoot, platformDirectories.macos, 'disksage-cloud-plan-macos-arm64'); + addCli(artifactRoot, platformDirectories.macos, 'disksage-duplicate-audit-macos-arm64'); + + const result = spawnSync( + 'bash', + [ + resolve(repositoryRoot, '.github/scripts/verify-release-artifacts.sh'), + artifactRoot, + runAttempt, + ], + { cwd: repositoryRoot, encoding: 'utf8' }, + ); + + expect(result.status, result.stderr).toBe(0); + expect(result.stderr).toBe(''); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); +}); From 8511b52a46794fe0b71c91f6dbb73b9aa9a174fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:06:01 -0700 Subject: [PATCH 28/35] fix: verify Windows release artifact namespace --- .github/scripts/verify-release-artifacts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/verify-release-artifacts.sh b/.github/scripts/verify-release-artifacts.sh index b35a74651..5d891302e 100644 --- a/.github/scripts/verify-release-artifacts.sh +++ b/.github/scripts/verify-release-artifacts.sh @@ -33,7 +33,7 @@ require_exactly_one_file() { expected_dirs=( "release-disksage-ubuntu-22.04-${run_attempt}" - "release-disksage-windows-latest-${run_attempt}" + "release-disksage-windows-2022-${run_attempt}" "release-disksage-macos-latest-${run_attempt}" ) From 7af3711dca455f3ed95a7c6b5dee97bcaf543e09 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:35:45 -0700 Subject: [PATCH 29/35] test: reject cross-platform release artifact placement --- ...eArtifactVerifierDirectoryContract.test.ts | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts index 1fedc5a41..79ead52d0 100644 --- a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts +++ b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; @@ -29,6 +29,33 @@ function addCli(artifactRoot: string, directory: string, name: string) { ); } +function materializeExactArtifactSet(artifactRoot: string) { + write(join(artifactRoot, platformDirectories.linux, 'bundle/deb/disksage.deb'), 'deb'); + write(join(artifactRoot, platformDirectories.linux, 'bundle/appimage/disksage.AppImage'), 'appimage'); + write(join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'), 'msi'); + write(join(artifactRoot, platformDirectories.windows, 'bundle/nsis/disksage-setup.exe'), 'nsis'); + write(join(artifactRoot, platformDirectories.macos, 'bundle/dmg/disksage.dmg'), 'dmg'); + + addCli(artifactRoot, platformDirectories.linux, 'disksage-cloud-plan-linux-x86_64'); + addCli(artifactRoot, platformDirectories.linux, 'disksage-duplicate-audit-linux-x86_64'); + addCli(artifactRoot, platformDirectories.windows, 'disksage-cloud-plan-windows-x86_64.exe'); + addCli(artifactRoot, platformDirectories.windows, 'disksage-duplicate-audit-windows-x86_64.exe'); + addCli(artifactRoot, platformDirectories.macos, 'disksage-cloud-plan-macos-arm64'); + addCli(artifactRoot, platformDirectories.macos, 'disksage-duplicate-audit-macos-arm64'); +} + +function verify(artifactRoot: string) { + return spawnSync( + 'bash', + [ + resolve(repositoryRoot, '.github/scripts/verify-release-artifacts.sh'), + artifactRoot, + runAttempt, + ], + { cwd: repositoryRoot, encoding: 'utf8' }, + ); +} + describe('release artifact verifier directory contract', () => { it.runIf(process.platform !== 'win32')( 'accepts the exact platform namespaces uploaded by the release matrix', @@ -36,28 +63,9 @@ describe('release artifact verifier directory contract', () => { const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); const artifactRoot = join(fixtureRoot, 'release-artifacts'); try { - write(join(artifactRoot, platformDirectories.linux, 'bundle/deb/disksage.deb'), 'deb'); - write(join(artifactRoot, platformDirectories.linux, 'bundle/appimage/disksage.AppImage'), 'appimage'); - write(join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'), 'msi'); - write(join(artifactRoot, platformDirectories.windows, 'bundle/nsis/disksage-setup.exe'), 'nsis'); - write(join(artifactRoot, platformDirectories.macos, 'bundle/dmg/disksage.dmg'), 'dmg'); - - addCli(artifactRoot, platformDirectories.linux, 'disksage-cloud-plan-linux-x86_64'); - addCli(artifactRoot, platformDirectories.linux, 'disksage-duplicate-audit-linux-x86_64'); - addCli(artifactRoot, platformDirectories.windows, 'disksage-cloud-plan-windows-x86_64.exe'); - addCli(artifactRoot, platformDirectories.windows, 'disksage-duplicate-audit-windows-x86_64.exe'); - addCli(artifactRoot, platformDirectories.macos, 'disksage-cloud-plan-macos-arm64'); - addCli(artifactRoot, platformDirectories.macos, 'disksage-duplicate-audit-macos-arm64'); + materializeExactArtifactSet(artifactRoot); - const result = spawnSync( - 'bash', - [ - resolve(repositoryRoot, '.github/scripts/verify-release-artifacts.sh'), - artifactRoot, - runAttempt, - ], - { cwd: repositoryRoot, encoding: 'utf8' }, - ); + const result = verify(artifactRoot); expect(result.status, result.stderr).toBe(0); expect(result.stderr).toBe(''); @@ -66,4 +74,27 @@ describe('release artifact verifier directory contract', () => { } }, ); -}); + + it.runIf(process.platform !== 'win32')( + 'rejects a Windows bundle that escaped its Windows artifact directory', + () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); + const artifactRoot = join(fixtureRoot, 'release-artifacts'); + try { + materializeExactArtifactSet(artifactRoot); + const source = join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'); + const misplaced = join(artifactRoot, platformDirectories.linux, 'bundle/msi/disksage.msi'); + mkdirSync(dirname(misplaced), { recursive: true }); + renameSync(source, misplaced); + + const result = verify(artifactRoot); + + expect(result.status).not.toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('Windows MSI bundle'); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); +}); \ No newline at end of file From ccd5e8dc9545c9063ce3760c2142286f8173879a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:36:31 -0700 Subject: [PATCH 30/35] fix: bind release artifacts to platform directories --- .github/scripts/verify-release-artifacts.sh | 38 +++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/scripts/verify-release-artifacts.sh b/.github/scripts/verify-release-artifacts.sh index 5d891302e..a6b344e16 100644 --- a/.github/scripts/verify-release-artifacts.sh +++ b/.github/scripts/verify-release-artifacts.sh @@ -23,10 +23,10 @@ require_exactly_one_path() { } require_exactly_one_file() { - local file_name="$1" count=0 matched_path="" - while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find "$artifact_root" -type f -name "$file_name" -print0) + local directory="$1" file_name="$2" count=0 matched_path="" + while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find "$artifact_root/$directory" -type f -name "$file_name" -print0) if [[ $count -ne 1 ]]; then - printf 'Expected exactly one release artifact named %s, found %s.\n' "$file_name" "$count" >&2 + printf 'Expected exactly one release artifact named %s in %s, found %s.\n' "$file_name" "$directory" "$count" >&2 exit 1 fi } @@ -55,22 +55,24 @@ if [[ -n "$unexpected_entry" ]]; then exit 1 fi -require_exactly_one_path '*/bundle/deb/*.deb' 'Debian bundle' -require_exactly_one_path '*/bundle/appimage/*.AppImage' 'AppImage bundle' -require_exactly_one_path '*/bundle/msi/*.msi' 'Windows MSI bundle' -require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle' -require_exactly_one_path '*/bundle/dmg/*.dmg' 'macOS DMG bundle' +require_exactly_one_path "$artifact_root/${expected_dirs[0]}/bundle/deb/*.deb" 'Debian bundle' +require_exactly_one_path "$artifact_root/${expected_dirs[0]}/bundle/appimage/*.AppImage" 'AppImage bundle' +require_exactly_one_path "$artifact_root/${expected_dirs[1]}/bundle/msi/*.msi" 'Windows MSI bundle' +require_exactly_one_path "$artifact_root/${expected_dirs[1]}/bundle/nsis/*.exe" 'Windows NSIS bundle' +require_exactly_one_path "$artifact_root/${expected_dirs[2]}/bundle/dmg/*.dmg" 'macOS DMG bundle' -for required_name in \ - disksage-cloud-plan-linux-x86_64 \ - disksage-duplicate-audit-linux-x86_64 \ - disksage-cloud-plan-windows-x86_64.exe \ - disksage-duplicate-audit-windows-x86_64.exe \ - disksage-cloud-plan-macos-arm64 \ - disksage-duplicate-audit-macos-arm64; do - require_exactly_one_file "$required_name" - require_exactly_one_file "$required_name.sha256" -done +require_exactly_one_file "${expected_dirs[0]}" disksage-cloud-plan-linux-x86_64 +require_exactly_one_file "${expected_dirs[0]}" disksage-cloud-plan-linux-x86_64.sha256 +require_exactly_one_file "${expected_dirs[0]}" disksage-duplicate-audit-linux-x86_64 +require_exactly_one_file "${expected_dirs[0]}" disksage-duplicate-audit-linux-x86_64.sha256 +require_exactly_one_file "${expected_dirs[1]}" disksage-cloud-plan-windows-x86_64.exe +require_exactly_one_file "${expected_dirs[1]}" disksage-cloud-plan-windows-x86_64.exe.sha256 +require_exactly_one_file "${expected_dirs[1]}" disksage-duplicate-audit-windows-x86_64.exe +require_exactly_one_file "${expected_dirs[1]}" disksage-duplicate-audit-windows-x86_64.exe.sha256 +require_exactly_one_file "${expected_dirs[2]}" disksage-cloud-plan-macos-arm64 +require_exactly_one_file "${expected_dirs[2]}" disksage-cloud-plan-macos-arm64.sha256 +require_exactly_one_file "${expected_dirs[2]}" disksage-duplicate-audit-macos-arm64 +require_exactly_one_file "${expected_dirs[2]}" disksage-duplicate-audit-macos-arm64.sha256 checksum_files=() checksum_file="" From 563762293af00e54eabd7ace1e3a399c6373431a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 01:37:52 -0700 Subject: [PATCH 31/35] test: bind operational CLIs to release platform namespace --- ...eArtifactVerifierDirectoryContract.test.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts index 79ead52d0..c788f9dab 100644 --- a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts +++ b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts @@ -97,4 +97,31 @@ describe('release artifact verifier directory contract', () => { } }, ); -}); \ No newline at end of file + + it.runIf(process.platform !== 'win32')( + 'rejects a Windows operational CLI and checksum outside the Windows artifact directory', + () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); + const artifactRoot = join(fixtureRoot, 'release-artifacts'); + try { + materializeExactArtifactSet(artifactRoot); + const cliName = 'disksage-cloud-plan-windows-x86_64.exe'; + const source = join(artifactRoot, platformDirectories.windows, cliName); + const sourceChecksum = `${source}.sha256`; + const misplaced = join(artifactRoot, platformDirectories.linux, cliName); + const misplacedChecksum = `${misplaced}.sha256`; + renameSync(source, misplaced); + renameSync(sourceChecksum, misplacedChecksum); + + const result = verify(artifactRoot); + + expect(result.status).not.toBe(0); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain(cliName); + expect(result.stderr).toContain(platformDirectories.windows); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } + }, + ); +}); From db672bbf3d3843521a395ba1ab2eace6c3033a81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 02:10:02 -0700 Subject: [PATCH 32/35] test: require exact tag release artifact verification --- ...releaseTagArtifactVerifierContract.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/lib/releaseTagArtifactVerifierContract.test.ts diff --git a/src/lib/releaseTagArtifactVerifierContract.test.ts b/src/lib/releaseTagArtifactVerifierContract.test.ts new file mode 100644 index 000000000..f88649b9f --- /dev/null +++ b/src/lib/releaseTagArtifactVerifierContract.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + +function readReleaseWorkflow(): string { + return readFileSync(resolve(repositoryRoot, '.github/workflows/release.yml'), 'utf8').replace(/\r\n?/g, '\n'); +} + +describe('tag release artifact verifier contract', () => { + it('runs the shared exact build-artifact verifier before generating the SBOM', () => { + const workflow = readReleaseWorkflow(); + const attestStart = workflow.indexOf('\n attest-release:\n'); + const publishStart = workflow.indexOf('\n publish-release:\n', attestStart); + expect(attestStart).toBeGreaterThanOrEqual(0); + expect(publishStart).toBeGreaterThan(attestStart); + + const attestJob = workflow.slice(attestStart, publishStart); + const sharedVerifier = 'bash .github/scripts/verify-release-artifacts.sh release-artifacts "${{ github.run_attempt }}"'; + const verifierOffset = attestJob.indexOf(sharedVerifier); + const sbomOffset = attestJob.indexOf('- name: Generate and validate source-bound SBOM'); + + expect(verifierOffset).toBeGreaterThanOrEqual(0); + expect(sbomOffset).toBeGreaterThan(verifierOffset); + }); +}); From 5e7b33de8df7f7ce0c011837c2a9139ae7a16a75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 18:20:39 +0900 Subject: [PATCH 33/35] fix: verify tag artifacts before sbom --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 980672cd7..30a69e363 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -259,6 +259,10 @@ jobs: path: release-artifacts merge-multiple: false + - name: Verify downloaded release artifact contract + shell: bash + run: bash .github/scripts/verify-release-artifacts.sh release-artifacts "${{ github.run_attempt }}" + - name: Generate and validate source-bound SBOM shell: bash run: | From 6f50a0806264b4936b2a1be9e174f92ff62af494 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 05:09:36 -0700 Subject: [PATCH 34/35] chore: remove release verifier drift from incomplete help owner --- .github/scripts/verify-release-artifacts.sh | 40 +++--- .github/workflows/release.yml | 4 - ...eArtifactVerifierDirectoryContract.test.ts | 127 ------------------ ...releaseTagArtifactVerifierContract.test.ts | 28 ---- 4 files changed, 19 insertions(+), 180 deletions(-) delete mode 100644 src/lib/releaseArtifactVerifierDirectoryContract.test.ts delete mode 100644 src/lib/releaseTagArtifactVerifierContract.test.ts diff --git a/.github/scripts/verify-release-artifacts.sh b/.github/scripts/verify-release-artifacts.sh index a6b344e16..b35a74651 100644 --- a/.github/scripts/verify-release-artifacts.sh +++ b/.github/scripts/verify-release-artifacts.sh @@ -23,17 +23,17 @@ require_exactly_one_path() { } require_exactly_one_file() { - local directory="$1" file_name="$2" count=0 matched_path="" - while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find "$artifact_root/$directory" -type f -name "$file_name" -print0) + local file_name="$1" count=0 matched_path="" + while IFS= read -r -d '' matched_path; do count=$((count + 1)); done < <(find "$artifact_root" -type f -name "$file_name" -print0) if [[ $count -ne 1 ]]; then - printf 'Expected exactly one release artifact named %s in %s, found %s.\n' "$file_name" "$directory" "$count" >&2 + printf 'Expected exactly one release artifact named %s, found %s.\n' "$file_name" "$count" >&2 exit 1 fi } expected_dirs=( "release-disksage-ubuntu-22.04-${run_attempt}" - "release-disksage-windows-2022-${run_attempt}" + "release-disksage-windows-latest-${run_attempt}" "release-disksage-macos-latest-${run_attempt}" ) @@ -55,24 +55,22 @@ if [[ -n "$unexpected_entry" ]]; then exit 1 fi -require_exactly_one_path "$artifact_root/${expected_dirs[0]}/bundle/deb/*.deb" 'Debian bundle' -require_exactly_one_path "$artifact_root/${expected_dirs[0]}/bundle/appimage/*.AppImage" 'AppImage bundle' -require_exactly_one_path "$artifact_root/${expected_dirs[1]}/bundle/msi/*.msi" 'Windows MSI bundle' -require_exactly_one_path "$artifact_root/${expected_dirs[1]}/bundle/nsis/*.exe" 'Windows NSIS bundle' -require_exactly_one_path "$artifact_root/${expected_dirs[2]}/bundle/dmg/*.dmg" 'macOS DMG bundle' +require_exactly_one_path '*/bundle/deb/*.deb' 'Debian bundle' +require_exactly_one_path '*/bundle/appimage/*.AppImage' 'AppImage bundle' +require_exactly_one_path '*/bundle/msi/*.msi' 'Windows MSI bundle' +require_exactly_one_path '*/bundle/nsis/*.exe' 'Windows NSIS bundle' +require_exactly_one_path '*/bundle/dmg/*.dmg' 'macOS DMG bundle' -require_exactly_one_file "${expected_dirs[0]}" disksage-cloud-plan-linux-x86_64 -require_exactly_one_file "${expected_dirs[0]}" disksage-cloud-plan-linux-x86_64.sha256 -require_exactly_one_file "${expected_dirs[0]}" disksage-duplicate-audit-linux-x86_64 -require_exactly_one_file "${expected_dirs[0]}" disksage-duplicate-audit-linux-x86_64.sha256 -require_exactly_one_file "${expected_dirs[1]}" disksage-cloud-plan-windows-x86_64.exe -require_exactly_one_file "${expected_dirs[1]}" disksage-cloud-plan-windows-x86_64.exe.sha256 -require_exactly_one_file "${expected_dirs[1]}" disksage-duplicate-audit-windows-x86_64.exe -require_exactly_one_file "${expected_dirs[1]}" disksage-duplicate-audit-windows-x86_64.exe.sha256 -require_exactly_one_file "${expected_dirs[2]}" disksage-cloud-plan-macos-arm64 -require_exactly_one_file "${expected_dirs[2]}" disksage-cloud-plan-macos-arm64.sha256 -require_exactly_one_file "${expected_dirs[2]}" disksage-duplicate-audit-macos-arm64 -require_exactly_one_file "${expected_dirs[2]}" disksage-duplicate-audit-macos-arm64.sha256 +for required_name in \ + disksage-cloud-plan-linux-x86_64 \ + disksage-duplicate-audit-linux-x86_64 \ + disksage-cloud-plan-windows-x86_64.exe \ + disksage-duplicate-audit-windows-x86_64.exe \ + disksage-cloud-plan-macos-arm64 \ + disksage-duplicate-audit-macos-arm64; do + require_exactly_one_file "$required_name" + require_exactly_one_file "$required_name.sha256" +done checksum_files=() checksum_file="" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30a69e363..980672cd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -259,10 +259,6 @@ jobs: path: release-artifacts merge-multiple: false - - name: Verify downloaded release artifact contract - shell: bash - run: bash .github/scripts/verify-release-artifacts.sh release-artifacts "${{ github.run_attempt }}" - - name: Generate and validate source-bound SBOM shell: bash run: | diff --git a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts b/src/lib/releaseArtifactVerifierDirectoryContract.test.ts deleted file mode 100644 index c788f9dab..000000000 --- a/src/lib/releaseArtifactVerifierDirectoryContract.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { createHash } from 'node:crypto'; -import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; -import { spawnSync } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); -const runAttempt = '1'; -const platformDirectories = { - linux: `release-disksage-ubuntu-22.04-${runAttempt}`, - windows: `release-disksage-windows-2022-${runAttempt}`, - macos: `release-disksage-macos-latest-${runAttempt}`, -} as const; - -function write(path: string, bytes: Buffer | string) { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, bytes); -} - -function addCli(artifactRoot: string, directory: string, name: string) { - const bytes = Buffer.from(`cli:${name}`); - const assetPath = join(artifactRoot, directory, name); - write(assetPath, bytes); - write( - `${assetPath}.sha256`, - `${createHash('sha256').update(bytes).digest('hex')} ${name}\n`, - ); -} - -function materializeExactArtifactSet(artifactRoot: string) { - write(join(artifactRoot, platformDirectories.linux, 'bundle/deb/disksage.deb'), 'deb'); - write(join(artifactRoot, platformDirectories.linux, 'bundle/appimage/disksage.AppImage'), 'appimage'); - write(join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'), 'msi'); - write(join(artifactRoot, platformDirectories.windows, 'bundle/nsis/disksage-setup.exe'), 'nsis'); - write(join(artifactRoot, platformDirectories.macos, 'bundle/dmg/disksage.dmg'), 'dmg'); - - addCli(artifactRoot, platformDirectories.linux, 'disksage-cloud-plan-linux-x86_64'); - addCli(artifactRoot, platformDirectories.linux, 'disksage-duplicate-audit-linux-x86_64'); - addCli(artifactRoot, platformDirectories.windows, 'disksage-cloud-plan-windows-x86_64.exe'); - addCli(artifactRoot, platformDirectories.windows, 'disksage-duplicate-audit-windows-x86_64.exe'); - addCli(artifactRoot, platformDirectories.macos, 'disksage-cloud-plan-macos-arm64'); - addCli(artifactRoot, platformDirectories.macos, 'disksage-duplicate-audit-macos-arm64'); -} - -function verify(artifactRoot: string) { - return spawnSync( - 'bash', - [ - resolve(repositoryRoot, '.github/scripts/verify-release-artifacts.sh'), - artifactRoot, - runAttempt, - ], - { cwd: repositoryRoot, encoding: 'utf8' }, - ); -} - -describe('release artifact verifier directory contract', () => { - it.runIf(process.platform !== 'win32')( - 'accepts the exact platform namespaces uploaded by the release matrix', - () => { - const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); - const artifactRoot = join(fixtureRoot, 'release-artifacts'); - try { - materializeExactArtifactSet(artifactRoot); - - const result = verify(artifactRoot); - - expect(result.status, result.stderr).toBe(0); - expect(result.stderr).toBe(''); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); - - it.runIf(process.platform !== 'win32')( - 'rejects a Windows bundle that escaped its Windows artifact directory', - () => { - const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); - const artifactRoot = join(fixtureRoot, 'release-artifacts'); - try { - materializeExactArtifactSet(artifactRoot); - const source = join(artifactRoot, platformDirectories.windows, 'bundle/msi/disksage.msi'); - const misplaced = join(artifactRoot, platformDirectories.linux, 'bundle/msi/disksage.msi'); - mkdirSync(dirname(misplaced), { recursive: true }); - renameSync(source, misplaced); - - const result = verify(artifactRoot); - - expect(result.status).not.toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain('Windows MSI bundle'); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); - - it.runIf(process.platform !== 'win32')( - 'rejects a Windows operational CLI and checksum outside the Windows artifact directory', - () => { - const fixtureRoot = mkdtempSync(join(tmpdir(), 'disksage-release-artifact-verifier-')); - const artifactRoot = join(fixtureRoot, 'release-artifacts'); - try { - materializeExactArtifactSet(artifactRoot); - const cliName = 'disksage-cloud-plan-windows-x86_64.exe'; - const source = join(artifactRoot, platformDirectories.windows, cliName); - const sourceChecksum = `${source}.sha256`; - const misplaced = join(artifactRoot, platformDirectories.linux, cliName); - const misplacedChecksum = `${misplaced}.sha256`; - renameSync(source, misplaced); - renameSync(sourceChecksum, misplacedChecksum); - - const result = verify(artifactRoot); - - expect(result.status).not.toBe(0); - expect(result.stdout).toBe(''); - expect(result.stderr).toContain(cliName); - expect(result.stderr).toContain(platformDirectories.windows); - } finally { - rmSync(fixtureRoot, { recursive: true, force: true }); - } - }, - ); -}); diff --git a/src/lib/releaseTagArtifactVerifierContract.test.ts b/src/lib/releaseTagArtifactVerifierContract.test.ts deleted file mode 100644 index f88649b9f..000000000 --- a/src/lib/releaseTagArtifactVerifierContract.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; - -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); - -function readReleaseWorkflow(): string { - return readFileSync(resolve(repositoryRoot, '.github/workflows/release.yml'), 'utf8').replace(/\r\n?/g, '\n'); -} - -describe('tag release artifact verifier contract', () => { - it('runs the shared exact build-artifact verifier before generating the SBOM', () => { - const workflow = readReleaseWorkflow(); - const attestStart = workflow.indexOf('\n attest-release:\n'); - const publishStart = workflow.indexOf('\n publish-release:\n', attestStart); - expect(attestStart).toBeGreaterThanOrEqual(0); - expect(publishStart).toBeGreaterThan(attestStart); - - const attestJob = workflow.slice(attestStart, publishStart); - const sharedVerifier = 'bash .github/scripts/verify-release-artifacts.sh release-artifacts "${{ github.run_attempt }}"'; - const verifierOffset = attestJob.indexOf(sharedVerifier); - const sbomOffset = attestJob.indexOf('- name: Generate and validate source-bound SBOM'); - - expect(verifierOffset).toBeGreaterThanOrEqual(0); - expect(sbomOffset).toBeGreaterThan(verifierOffset); - }); -}); From 9654d3d4f9a19ecd4080ee6aeac0c460a66da83e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:33:05 +0900 Subject: [PATCH 35/35] docs(cli): add next-action help guidance --- ...age-incomplete-download-materialization.rs | 3 +- ...isksage-incomplete-download-materialize.rs | 36 ++++++++----------- .../disksage-incomplete-download-recovery.rs | 3 +- .../tests/cli_help_incomplete_plans_exit.rs | 34 ++++++++++++------ 4 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs index 774f26905..ef2b4ebe0 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialization.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialization.rs @@ -32,7 +32,8 @@ fn usage() -> String { "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH \ [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ - [--private-output ABSOLUTE_NEW_FILE.json]" + [--private-output ABSOLUTE_NEW_FILE.json]\n\ + 다음 단계: 생성된 계획을 검토하세요. 이 명령은 파일을 이동하거나 삭제하지 않습니다." ) } diff --git a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs index 78f381822..fce9ca357 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-materialize.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-materialize.rs @@ -62,7 +62,8 @@ fn usage() -> String { --approved-by human:ID --rationale TEXT --execute \ (--live-icloud-capacity | --capacity-snapshot ABSOLUTE.json) \ [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ - [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}]" + [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}]\n\ + 다음 단계: 계획 지문과 용량 증거를 검토한 뒤 승인 정보와 --execute를 제공하세요." ) } @@ -103,11 +104,7 @@ fn parse_args(raw: &[OsString]) -> Result { if source_root.is_some() { return Err("--source-root는 한 번만 지정할 수 있음".into()); } - source_root = Some(PathBuf::from(next_value( - raw, - &mut index, - "--source-root", - )?)); + source_root = Some(PathBuf::from(next_value(raw, &mut index, "--source-root")?)); } "--destination-plan" => { if destination_plan.is_some() { @@ -133,11 +130,7 @@ fn parse_args(raw: &[OsString]) -> Result { if receipt_dir.is_some() { return Err("--receipt-dir은 한 번만 지정할 수 있음".into()); } - receipt_dir = Some(PathBuf::from(next_value( - raw, - &mut index, - "--receipt-dir", - )?)); + receipt_dir = Some(PathBuf::from(next_value(raw, &mut index, "--receipt-dir")?)); } "--approved-by" => { if approved_by.is_some() { @@ -362,16 +355,17 @@ fn run() -> Result<(), String> { verify_discovered_cloud_root(&home, &plan)?; let capacity_observed_at_ms = system_now_ms(); - let capacity = if args.live_icloud_capacity { - if plan.provider != CloudProvider::Icloud { - return Err("live-icloud-capacity-requires-icloud-plan".into()); - } - collect_icloud_native_capacity(capacity_observed_at_ms)? - } else { - read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { - "materialization-execution-capacity-snapshot-missing".to_string() - })?)? - }; + let capacity = + if args.live_icloud_capacity { + if plan.provider != CloudProvider::Icloud { + return Err("live-icloud-capacity-requires-icloud-plan".into()); + } + collect_icloud_native_capacity(capacity_observed_at_ms)? + } else { + read_capacity_snapshot(args.capacity_snapshot.as_deref().ok_or_else(|| { + "materialization-execution-capacity-snapshot-missing".to_string() + })?)? + }; let audit = collect_incomplete_download_audit( &args.source_root, diff --git a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs index cafeefd47..c0b1ea98f 100644 --- a/src-tauri/src/bin/disksage-incomplete-download-recovery.rs +++ b/src-tauri/src/bin/disksage-incomplete-download-recovery.rs @@ -30,7 +30,8 @@ fn usage() -> String { "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH \ [--max-entries 1..={DEFAULT_MAX_ENTRIES}] \ [--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \ - [--private-output ABSOLUTE_NEW_FILE.json]" + [--private-output ABSOLUTE_NEW_FILE.json]\n\ + 다음 단계: 생성된 복구 계획을 검토하세요. 이 명령은 파일을 이동하거나 삭제하지 않습니다." ) } diff --git a/src-tauri/tests/cli_help_incomplete_plans_exit.rs b/src-tauri/tests/cli_help_incomplete_plans_exit.rs index d20bb9fd5..bffd478eb 100644 --- a/src-tauri/tests/cli_help_incomplete_plans_exit.rs +++ b/src-tauri/tests/cli_help_incomplete_plans_exit.rs @@ -13,17 +13,17 @@ const MATERIALIZE_SOURCE: &str = const BINARIES: [(&str, &str, &str); 3] = [ ( "disksage-incomplete-download-materialization", - "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]", + "usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]\n다음 단계: 생성된 계획을 검토하세요. 이 명령은 파일을 이동하거나 삭제하지 않습니다.", "incomplete-download-materialization-unknown-argument", ), ( "disksage-incomplete-download-recovery", - "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]", + "usage: disksage-incomplete-download-recovery --root ABSOLUTE_PATH [--max-entries 1..=200000] [--stale-after-days 1..=3650] [--private-output ABSOLUTE_NEW_FILE.json]\n다음 단계: 생성된 복구 계획을 검토하세요. 이 명령은 파일을 이동하거나 삭제하지 않습니다.", "incomplete-download-recovery-unknown-argument", ), ( "disksage-incomplete-download-materialize", - "usage: disksage-incomplete-download-materialize --source-root ABSOLUTE_PATH --destination-plan ABSOLUTE_PRIVATE_PLAN.json --confirm-plan-fingerprint HEX64 --receipt-dir ABSOLUTE_PRIVATE_DIRECTORY --approved-by human:ID --rationale TEXT --execute (--live-icloud-capacity | --capacity-snapshot ABSOLUTE.json) [--max-entries 1..=200000] [--stale-after-days 1..=3650]", + "usage: disksage-incomplete-download-materialize --source-root ABSOLUTE_PATH --destination-plan ABSOLUTE_PRIVATE_PLAN.json --confirm-plan-fingerprint HEX64 --receipt-dir ABSOLUTE_PRIVATE_DIRECTORY --approved-by human:ID --rationale TEXT --execute (--live-icloud-capacity | --capacity-snapshot ABSOLUTE.json) [--max-entries 1..=200000] [--stale-after-days 1..=3650]\n다음 단계: 계획 지문과 용량 증거를 검토한 뒤 승인 정보와 --execute를 제공하세요.", "incomplete-download-materialize-unknown-argument", ), ]; @@ -32,9 +32,12 @@ fn build_feature_gated_binaries() -> (tempfile::TempDir, Vec) { let target_dir = tempfile::tempdir().expect("isolated Cargo target directory must be created"); let cargo = std::env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")); let mut command = Command::new(cargo); - command - .current_dir(env!("CARGO_MANIFEST_DIR")) - .args(["build", "--locked", "--features", "cloud-cli"]); + command.current_dir(env!("CARGO_MANIFEST_DIR")).args([ + "build", + "--locked", + "--features", + "cloud-cli", + ]); for (binary, _, _) in BINARIES { command.args(["--bin", binary]); } @@ -249,7 +252,8 @@ fn assert_native_non_utf8_paths_reach_domain_boundaries(binaries: &[PathBuf]) { .expect("materialization execution CLI must launch with a native plan path"); assert_eq!(output.status.code(), Some(2)); assert!(output.stdout.is_empty()); - let stderr = String::from_utf8(output.stderr).expect("materialization diagnostic must be UTF-8"); + let stderr = + String::from_utf8(output.stderr).expect("materialization diagnostic must be UTF-8"); assert!( stderr.contains("materialization-execution-destination-plan-unavailable"), "native plan path must reach bounded file admission instead of argument decoding: {stderr}" @@ -270,7 +274,8 @@ fn assert_read_only_duplicate_limit_is_bounded(binary: &Path, flag: &str) { .expect("read-only incomplete-download CLI must launch for duplicate-limit validation"); assert_eq!(output.status.code(), Some(2)); assert!(output.stdout.is_empty()); - let stderr = String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); + let stderr = + String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); assert!( stderr.contains(&format!("{flag}는 한 번만 지정할 수 있음")), "duplicate bounded limit must fail before domain work: {stderr}" @@ -304,7 +309,8 @@ fn assert_materialize_duplicate_limit_is_bounded(binary: &Path, flag: &str) { .expect("materialize CLI must launch for duplicate-limit validation"); assert_eq!(output.status.code(), Some(2)); assert!(output.stdout.is_empty()); - let stderr = String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); + let stderr = + String::from_utf8(output.stderr).expect("duplicate-limit diagnostic must be UTF-8"); assert!( stderr.contains(&format!("{flag}는 한 번만 지정할 수 있음")), "duplicate bounded limit must fail before plan or filesystem admission: {stderr}" @@ -314,9 +320,15 @@ fn assert_materialize_duplicate_limit_is_bounded(binary: &Path, flag: &str) { #[test] fn incomplete_download_coverage_contract_keeps_shipped_entrypoints_real() { for (name, source) in [ - ("disksage-incomplete-download-materialization", MATERIALIZATION_SOURCE), + ( + "disksage-incomplete-download-materialization", + MATERIALIZATION_SOURCE, + ), ("disksage-incomplete-download-recovery", RECOVERY_SOURCE), - ("disksage-incomplete-download-materialize", MATERIALIZE_SOURCE), + ( + "disksage-incomplete-download-materialize", + MATERIALIZE_SOURCE, + ), ] { assert!( !source.contains("#[cfg(coverage)]\nfn main()"),