diff --git a/crates/automation/src/a11y.rs b/crates/automation/src/a11y.rs index b58b2d587b..81f79d14ee 100644 --- a/crates/automation/src/a11y.rs +++ b/crates/automation/src/a11y.rs @@ -3,7 +3,7 @@ use pdfcraft_engine::a11y::{Category, Options, Report, Rule, Status}; use serde_json::{Value, json}; -use crate::{Args, Automation, Result, ToolError, child, failed, write_atomic}; +use crate::{Args, Automation, Result, ToolError, child, failed, unused, write_atomic}; fn status_id(s: Status) -> &'static str { match s { @@ -347,6 +347,16 @@ impl Automation { out.push(match result { Ok(r) => { let target = child(&folder, &name); + let target = if target.exists() { + let stem = + std::path::Path::new(&name).file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_else(|| "document".into()); + unused(&folder, &stem) + } else { + target + }; + if target.exists() { + return Err(failed(format!("{}: no unused output name available", target.display()))); + } write_atomic(&target, &r.bytes)?; json!({ "path": p, "output": target.to_string_lossy(), "log": r.log }) } diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 6d0e8262f7..c509668474 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -3026,6 +3026,36 @@ fn actions_through_tools() { assert!(a.call("action_run", &json!({ "steps": [{ "step": "fly" }], "paths": ["a.pdf"], "folder": "x" })).is_err()); } +#[test] +fn action_run_keeps_outputs_with_duplicate_input_basenames() { + let dir = workdir("action-duplicate-basenames"); + std::fs::create_dir_all(dir.join("left")).unwrap(); + std::fs::create_dir_all(dir.join("right")).unwrap(); + std::fs::write(dir.join("left/report.pdf"), fixture(1)).unwrap(); + std::fs::write(dir.join("right/report.pdf"), fixture(2)).unwrap(); + let mut a = auto(&dir); + let args = json!({ + "steps": [{ "step": "set_title", "arg": "Processed" }], + "paths": ["left/report.pdf", "right/report.pdf"], + "folder": "results" + }); + let result = ok(&mut a, "action_run", args.clone()); + let files = result["files"].as_array().unwrap(); + assert_eq!(files.len(), 2); + let first_path = files[0]["output"].as_str().unwrap(); + let second_path = files[1]["output"].as_str().unwrap(); + assert!(first_path.ends_with("report.pdf"), "{first_path}"); + assert!(second_path.ends_with("report (2).pdf"), "{second_path}"); + let first_bytes = std::fs::read(first_path).unwrap(); + assert_eq!(ok(&mut a, "doc_open", json!({ "path": first_path }))["pages"], 1); + assert_eq!(ok(&mut a, "doc_open", json!({ "path": second_path }))["pages"], 2); + + let repeated = ok(&mut a, "action_run", args); + assert!(repeated["files"][0]["output"].as_str().unwrap().ends_with("report (3).pdf")); + assert!(repeated["files"][1]["output"].as_str().unwrap().ends_with("report (4).pdf")); + assert_eq!(std::fs::read(first_path).unwrap(), first_bytes, "an earlier output must not be replaced"); +} + #[test] fn pdfa_through_tools() { let dir = workdir("pdfa");