From 4adb0e9757f125d390d8d74105e09998c41d0d1b Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 7 Oct 2026 05:17:38 -0700 Subject: [PATCH] fix: preserve caller paths for the standalone operation runner --- CHANGELOG.md | 2 + README.md | 2 + xtask/src/main.rs | 15 +++--- xtask/tests/run_edict_operation.rs | 86 ++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b88b818f1..83a753c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Fixed +- `xtask run-edict-operation` runs outside Git and resolves relative artifact and WAL paths from the caller directory. Repository maintenance commands retain their Git-root behavior. + - `WorldlineState::state_root` documentation now states its reachable-state boundary. Detached create-if-absent writes remain bound by patch and commit identities. Hash bytes are unchanged. - `ReadView::at` validates the aperture and derives the selected current frontier diff --git a/README.md b/README.md index 76ad26588..87a61d38f 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,8 @@ compiler-declared maximum result size is capped at 65,536 before private scheduler evaluation. Produced canonical result bytes are measured against that declared ceiling during evaluation. +The built `xtask run-edict-operation` binary keeps the caller's working directory and can run outside a Git checkout. Relative artifact and WAL paths resolve from that directory. `cargo xtask` still requires a Cargo workspace. Repository maintenance commands continue to resolve paths from their Git root. + ## Contracts And Boundaries Echo core is intentionally generic. Application nouns belong in authored diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 07d9e3817..e0a9f4c94 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -452,15 +452,16 @@ struct PrPreflightArgs { } fn main() -> Result<()> { - // Ensure CWD is the repo root so that relative paths like "docs/", - // "scripts/ensure_spdx.sh", and git-ls-files all work regardless of - // where `cargo xtask` is invoked from. - let repo_root = find_repo_root()?; - std::env::set_current_dir(&repo_root) - .with_context(|| format!("failed to chdir to {}", repo_root.display()))?; - let cli = Cli::parse(); + // Maintenance commands resolve repository paths from the Git root. The + // artifact runner resolves supplied paths from the caller's directory. + if !matches!(&cli.command, Commands::RunEdictOperation(_)) { + let repo_root = find_repo_root()?; + std::env::set_current_dir(&repo_root) + .with_context(|| format!("failed to chdir to {}", repo_root.display()))?; + } + match cli.command { Commands::Bench(args) => run_bench(args), Commands::Doghouse(args) => run_doghouse(args), diff --git a/xtask/tests/run_edict_operation.rs b/xtask/tests/run_edict_operation.rs index 80e0a9036..4d6e565b2 100644 --- a/xtask/tests/run_edict_operation.rs +++ b/xtask/tests/run_edict_operation.rs @@ -27,6 +27,14 @@ impl TempRunDir { .expect("xtask belongs to the Echo workspace") .join("target") .join("xtask-test-tmp"); + Self::new_in(root) + } + + fn outside_repo() -> Self { + Self::new_in(std::env::temp_dir().join("echo-xtask-test")) + } + + fn new_in(root: PathBuf) -> Self { fs::create_dir_all(&root).expect("the xtask fixture root is creatable"); for _ in 0..1024 { let ordinal = RUN_COUNTER.fetch_add(1, Ordering::Relaxed); @@ -142,6 +150,84 @@ fn assert_rejected(output: &Output, expected_reason: &str) { ); } +#[test] +fn runner_accepts_absolute_artifacts_outside_git() { + let run_dir = TempRunDir::outside_repo(); + let git_probe = Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .current_dir(run_dir.path()) + .output() + .expect("the Git fixture probe starts"); + assert!(!git_probe.status.success(), "fixture must be outside Git"); + let output = runner_command( + &fixture_path("executable-operation-package.cbor"), + &fixture_path("verification-report.cbor"), + &fixture_path("input.json"), + &run_dir.path().join("wal"), + ) + .current_dir(run_dir.path()) + .output() + .expect("the standalone runner starts"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("runner reports JSON"); + assert_eq!(report["recovery"]["stateRecovered"], true); + assert_eq!(report["state"]["valueUtf8"], "Hello Echo"); +} + +#[test] +fn runner_preserves_relative_paths_in_an_unrelated_nested_repository() { + let run_dir = TempRunDir::outside_repo(); + assert!(Command::new("git") + .args(["init", "-q"]) + .current_dir(run_dir.path()) + .status() + .expect("fixture Git initialization starts") + .success()); + let nested = run_dir.path().join("nested"); + fs::create_dir(&nested).expect("caller directory is creatable"); + fs::copy(fixture_path("input.json"), nested.join("input.json")).expect("input is copied"); + let output = runner_command( + &fixture_path("executable-operation-package.cbor"), + &fixture_path("verification-report.cbor"), + Path::new("input.json"), + Path::new("wal"), + ) + .current_dir(&nested) + .output() + .expect("the nested runner starts"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let report: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("runner reports JSON"); + assert_eq!(report["recovery"]["stateRecovered"], true); + assert!(nested.join("wal").is_dir()); + assert!(!run_dir.path().join("wal").exists()); +} + +#[test] +fn repository_commands_still_resolve_the_echo_root() { + let run_dir = TempRunDir::new(); + let output = Command::new(env!("CARGO_BIN_EXE_xtask")) + .arg("lint-dead-refs") + .current_dir(run_dir.path()) + .output() + .expect("repository command starts"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(String::from_utf8_lossy(&output.stdout).contains("all links OK")); +} + #[test] fn compiler_emitted_operation_runs_durably_without_native_callbacks() { let run_dir = TempRunDir::new();