diff --git a/crates/pi-natives/src/shell/minimizer.rs b/crates/pi-natives/src/shell/minimizer.rs index 7e7fcc24f..c87ef3e94 100644 --- a/crates/pi-natives/src/shell/minimizer.rs +++ b/crates/pi-natives/src/shell/minimizer.rs @@ -97,6 +97,15 @@ impl MinimizerOutput { self } + /// Replace the transformed text while keeping minimization telemetry + /// coherent. + #[must_use] + pub fn with_text(mut self, text: String) -> Self { + self.output_bytes = text.len(); + self.text = text; + self + } + /// Byte count saved by this filter (0 for passthrough). #[allow(dead_code, reason = "test-only API surface")] pub const fn bytes_saved(&self) -> usize { diff --git a/crates/pi-natives/src/shell/minimizer/engine.rs b/crates/pi-natives/src/shell/minimizer/engine.rs index 14d003556..788a562b0 100644 --- a/crates/pi-natives/src/shell/minimizer/engine.rs +++ b/crates/pi-natives/src/shell/minimizer/engine.rs @@ -127,7 +127,7 @@ fn apply_identity( }; let label = program_label(&identity.program); let overlaid = apply_pipeline_overlay(config, &identity.program, rust_output, label); - return overlaid.with_original(captured); + return ensure_success_visible(overlaid, exit_code).with_original(captured); } if let Some(pipeline) = resolve_pipeline(config, &identity.program, subcommand) { @@ -139,15 +139,25 @@ fn apply_identity( if text == captured { return MinimizerOutput::passthrough(captured).labeled("pipeline-noop"); } - return MinimizerOutput::transformed(text, captured.len()) - .labeled("pipeline") - .with_original(captured); + return ensure_success_visible( + MinimizerOutput::transformed(text, captured.len()).labeled("pipeline"), + exit_code, + ) + .with_original(captured); } record_unknown_command(command); MinimizerOutput::passthrough(captured).labeled("unsupported") } +fn ensure_success_visible(output: MinimizerOutput, exit_code: i32) -> MinimizerOutput { + if exit_code == 0 && output.changed && output.text.trim().is_empty() { + output.with_text("OK\n".to_string()) + } else { + output + } +} + /// Per-program label for telemetry. Returns one of a fixed static set so the /// N-API boundary can carry it as `&'static str` without allocation. fn program_label(program: &str) -> &'static str { @@ -321,6 +331,33 @@ mod tests { assert!(out.text.contains("modified: 1")); } + #[test] + fn successful_minimization_keeps_visible_ok_when_filter_removes_all_lines() { + let cfg = MinimizerConfig { enabled: true, ..Default::default() }; + let out = apply( + "cargo build", + " Compiling app v0.1.0\n Finished `dev` profile [unoptimized + debuginfo] target(s) \ + in 1.23s\n", + 0, + &cfg, + ); + + assert!(out.changed); + assert_eq!(out.text, "OK\n"); + assert_eq!(out.output_bytes, out.text.len()); + assert!(out.original_text.is_some()); + } + + #[test] + fn failed_minimization_does_not_invent_ok_for_empty_output() { + let cfg = MinimizerConfig { enabled: true, ..Default::default() }; + let out = apply("cargo build", " Compiling app v0.1.0\n", 1, &cfg); + + assert!(out.changed); + assert_eq!(out.text, ""); + assert!(out.original_text.is_some()); + } + #[test] fn unknown_command_is_passthrough() { let cfg = MinimizerConfig { enabled: true, ..Default::default() }; diff --git a/packages/coding-agent/src/tools/recipe/runners/pkg.ts b/packages/coding-agent/src/tools/recipe/runners/pkg.ts index fd61ddaaa..57dd86f3d 100644 --- a/packages/coding-agent/src/tools/recipe/runners/pkg.ts +++ b/packages/coding-agent/src/tools/recipe/runners/pkg.ts @@ -106,11 +106,7 @@ function packageTaskName(packageName: string | undefined, packageDir: string, sc return `${packageName ?? packageDir}/${scriptName}`; } -function tasksForPackage(options: { - pkg: PackageJsonInfo; - packageDir: string; - namespaced: boolean; -}): RunnerTask[] { +function tasksForPackage(options: { pkg: PackageJsonInfo; packageDir: string; namespaced: boolean }): RunnerTask[] { return options.pkg.scripts.map(scriptName => ({ name: options.namespaced ? packageTaskName(options.pkg.name, options.packageDir, scriptName) : scriptName, doc: options.namespaced ? options.packageDir : undefined, diff --git a/packages/coding-agent/test/tools/recipe.test.ts b/packages/coding-agent/test/tools/recipe.test.ts index a0518ecc0..6737cfbce 100644 --- a/packages/coding-agent/test/tools/recipe.test.ts +++ b/packages/coding-agent/test/tools/recipe.test.ts @@ -152,9 +152,7 @@ describe("recipe", () => { "crate-b/bin/worker", ]); expect( - resolveCommand("cargo:crate-a/example/demo", [ - { id: "cargo", label: "Cargo", commandPrefix: "cargo", tasks }, - ]), + resolveCommand("cargo:crate-a/example/demo", [{ id: "cargo", label: "Cargo", commandPrefix: "cargo", tasks }]), ).toEqual({ command: "cargo run --package 'crate-a' --example 'demo'" }); }); diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index f549aacea..c42ee6d14 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Fixed + +- Fixed shell minimizer output so successful commands whose noise is fully stripped still return `OK` instead of an artifact-only result ## [14.5.6] - 2026-04-29