Test sh redacts resolved secrets in recorded output
Existing redact unit tests verify the function in isolation. This is
the missing end-to-end check: when a Lua run-fn calls (secret :name)
followed by (sh ["echo" tok]), the value returned to the script is
raw (so the script can use it programmatically) but the copy stored
on Runtime.outputs (the path that flows to CRI logs and sh_events
.cmd) has the value replaced with `{{ name }}`.

Without this, the integration claim of the redaction work was only
verified by code inspection.

Assisted-by: Claude Opus 4.7 (1M context)
change wmmnmpyzzvxzlruvuqptrryworryxrpo
commit 6ed550ea049b48f0dfe2e1d336a0783d6396d29f
author Alpha Chen <alpha@kejadlen.dev>
date
parent ylrvvmmx
diff --git a/src/ci/runtime.rs b/src/ci/runtime.rs
index 05bfdea..2fc3671 100644
--- a/src/ci/runtime.rs
+++ b/src/ci/runtime.rs
@@ -633,6 +633,57 @@ mod tests {
         runtime.lua().from_value(value).expect("decode ShOutput")
     }
 
+    #[test]
+    fn sh_redacts_secret_in_recorded_output() {
+        let mut secrets = HashMap::new();
+        secrets.insert(
+            "github_token".to_string(),
+            SecretString::from_plain("ghp_long_secret_value"),
+        );
+        let source = r#"(local ci (require :quire.ci))
+(ci.job :go [:quire/push]
+  (fn [{: sh : secret}]
+    (let [tok (secret :github_token)]
+      (sh ["echo" tok]))))"#;
+        let (runtime, run_fn) = rt(source, secrets);
+        let handle = RuntimeHandle(runtime.clone())
+            .into_lua(runtime.lua())
+            .expect("install runtime");
+
+        // Mark a current job so sh records into outputs. for_test seeds
+        // an empty inputs map, so enter_job would panic; bypass the
+        // assertion by writing the field directly.
+        *runtime.current_job.borrow_mut() = Some("go".to_string());
+
+        let value: mlua::Value = run_fn.call(handle).expect("sh call");
+        let returned: ShOutput = runtime.lua().from_value(value).expect("decode");
+
+        // The Lua caller still sees the raw value — echo printed it.
+        assert!(returned.stdout.contains("ghp_long_secret_value"));
+
+        // The recorded copy is redacted.
+        let outputs = runtime.take_outputs();
+        let recorded = outputs
+            .get("go")
+            .and_then(|v| v.first())
+            .expect("recorded sh output for 'go'");
+        assert!(
+            !recorded.stdout.contains("ghp_long_secret_value"),
+            "recorded stdout must not contain raw secret: {}",
+            recorded.stdout
+        );
+        assert!(
+            recorded.stdout.contains("{{ github_token }}"),
+            "recorded stdout must contain redaction marker: {}",
+            recorded.stdout
+        );
+        assert!(
+            !recorded.cmd.contains("ghp_long_secret_value"),
+            "recorded cmd must not contain raw secret: {}",
+            recorded.cmd
+        );
+    }
+
     #[test]
     fn sh_runs_argv_and_captures_stdout() {
         let r = run_sh_via_job(