From 763c2a379180f37cda6528c1166f43b736857dc8 Mon Sep 17 00:00:00 2001 From: David Leong Date: Sat, 15 Aug 2026 17:01:50 +0000 Subject: [PATCH] test(cli): pin grandchild output capture and exit propagation Signed-off-by: David Leong --- crates/openjd-cli/tests/cli_tests.rs | 39 ++++++++++++++++++ .../tests/templates/self_asserting_task.yaml | 40 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 crates/openjd-cli/tests/templates/self_asserting_task.yaml diff --git a/crates/openjd-cli/tests/cli_tests.rs b/crates/openjd-cli/tests/cli_tests.rs index 8edec751..5407fe03 100644 --- a/crates/openjd-cli/tests/cli_tests.rs +++ b/crates/openjd-cli/tests/cli_tests.rs @@ -789,6 +789,45 @@ mod redacted_env { } } +// ============================================================ +// Group 13: Self-asserting tasks (openjd-specifications conformance shape) +// ============================================================ + +/// The conformance suite's single-task job fixtures assert their own output: the +/// `onRun` action spawns the case's command as a child, reproduces its output, and +/// exits non-zero when that output does not match. That only holds if we capture a +/// grandchild's output and propagate the action's exit status, so pin both +/// directions here instead of relying on the external suite to catch a regression. +mod self_asserting_task { + use super::*; + + #[test] + fn test_grandchild_output_captured_and_assertion_passes() { + let template = templates_dir().join("self_asserting_task.yaml"); + let (code, stdout, stderr) = run_cli(&["run", template.to_str().unwrap()]); + assert_eq!(code, 0, "should succeed. stderr: {stderr}"); + // Printed by the grandchild and echoed by the action. Missing means output + // from a process we did not spawn ourselves was dropped. + assert!(stdout.contains("OUTPUT:EXPECTED_VALUE"), "stdout: {stdout}"); + assert!(!stdout.contains("ASSERT_FAILED"), "stdout: {stdout}"); + } + + #[test] + fn test_assertion_failure_fails_the_run() { + let template = templates_dir().join("self_asserting_task.yaml"); + let (code, stdout, stderr) = run_cli(&[ + "run", + template.to_str().unwrap(), + "-p", + "Printed=WRONG_VALUE", + ]); + let output = format!("{stdout}{stderr}"); + assert_ne!(code, 0, "should fail. stdout: {stdout} stderr: {stderr}"); + assert!(output.contains("ASSERT_FAILED"), "output: {output}"); + assert!(output.contains("OUTPUT:WRONG_VALUE"), "output: {output}"); + } +} + // ============================================================ // Group 6: Run Command (test_run_command.py) // ============================================================ diff --git a/crates/openjd-cli/tests/templates/self_asserting_task.yaml b/crates/openjd-cli/tests/templates/self_asserting_task.yaml new file mode 100644 index 00000000..cd7aaab1 --- /dev/null +++ b/crates/openjd-cli/tests/templates/self_asserting_task.yaml @@ -0,0 +1,40 @@ +specificationVersion: "jobtemplate-2023-09" +name: Self Asserting Task +description: > + An onRun action that spawns its own child process, reproduces that child's + output, and turns a comparison of it into the task's exit status. This is the + shape the openjd-specifications conformance suite uses for its single-task job + fixtures, so losing grandchild output or dropping the action's exit status + silently weakens that whole suite. + +parameterDefinitions: + - name: Printed + type: STRING + default: "EXPECTED_VALUE" + +steps: + - name: Assert + script: + actions: + onRun: + command: python + args: ["{{Task.File.Assert}}"] + embeddedFiles: + - name: Assert + type: TEXT + data: | + import subprocess + import sys + + COMMAND = [sys.executable, "-c", "print(r'OUTPUT:{{Param.Printed}}')"] + EXPECTED = ["OUTPUT:EXPECTED_VALUE"] + + completed = subprocess.run(COMMAND, capture_output=True, text=True) + sys.stdout.write(completed.stdout) + sys.stderr.write(completed.stderr) + output = completed.stdout + completed.stderr + + missing = [line for line in EXPECTED if line not in output] + for _ in missing: + sys.stderr.write("ASSERT_FAILED: expected output line not found\n") + sys.exit(1 if missing else 0)