diff --git a/.changeset/refusals-to-stderr.md b/.changeset/refusals-to-stderr.md new file mode 100644 index 0000000..07e14f8 --- /dev/null +++ b/.changeset/refusals-to-stderr.md @@ -0,0 +1,9 @@ +--- +'@platforma-open/milaboratories.sort-seq-analysis.software': patch +--- + +Write refusals to stderr as well as stdout, so the platform's error dialog shows them + +The k8s runner fills a failed command's "Latest output" from the stderr file alone. A +refusal printed only to stdout therefore reached the user as a blank "exited with code 1", +with the actual reason sitting in the run's log panel. diff --git a/block/logos/organization-logo.png b/block/logos/organization-logo.png index e03221e..850d820 100644 Binary files a/block/logos/organization-logo.png and b/block/logos/organization-logo.png differ diff --git a/software/src/main.py b/software/src/main.py index ecf3b9c..7542b1b 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -14,8 +14,14 @@ would multiply startup, leave the caller merging N manifests, and make the one-condition case a loop's degenerate iteration. -**Diagnostics go to stdout**, including failures: the workflow layer does not capture -stderr, so anything written there would be lost. +**Diagnostics go to both streams, and that is deliberate.** They serve two different +readers. Stdout is the run's audit trail: the block saves it as a log stream and the UI +shows it, so the successful-run summary belongs there. Stderr is what the platform reads +back when the command fails — the k8s runner fills the error dialog's "Latest output" +from the stderr file alone. A refusal printed only to stdout therefore reaches the user +as a blank "exited with code 1", with the reason sitting in a log panel nobody was told +to open. So a refusal is written to both: stdout because it is part of the run's record, +stderr because it is the failure the platform is about to report. """ from __future__ import annotations @@ -59,7 +65,12 @@ def main(argv: list[str] | None = None) -> int: except Refusal as refusal: # A data-value violation. Exit non-zero naming the offending values, having written # no file — nothing partial is produced. - print(f"REFUSED: {refusal}") + # + # Both streams, per the module docstring: stdout keeps the run's record complete, + # stderr is the only stream the platform quotes back in the error it shows. + message = f"REFUSED: {refusal}" + print(message) + print(message, file=sys.stderr) return 1 _report(manifest) diff --git a/software/tests/integration/test_cli.py b/software/tests/integration/test_cli.py index 086814c..ac0de5b 100644 --- a/software/tests/integration/test_cli.py +++ b/software/tests/integration/test_cli.py @@ -404,8 +404,12 @@ def test_over_summing_fractions_exits_non_zero_and_writes_nothing(tmp_path, caps assert code == 1 assert manifest is None assert not out_dir.exists() - # Diagnostics go to stdout — the workflow layer does not capture stderr. - assert "REFUSED" in capsys.readouterr().out + # Both streams, and both are pinned. Stdout is the run's record; stderr is the only + # stream the platform quotes back in the error it shows the user, so a refusal that + # reaches stdout alone is reported as a blank non-zero exit. + captured = capsys.readouterr() + assert "REFUSED" in captured.out + assert "REFUSED" in captured.err def test_replicate_samples_exit_non_zero_and_write_nothing(tmp_path, capsys): @@ -429,7 +433,9 @@ def test_replicate_samples_exit_non_zero_and_write_nothing(tmp_path, capsys): assert code == 1 assert manifest is None assert not out_dir.exists() - assert "replicate" in capsys.readouterr().out + captured = capsys.readouterr() + assert "replicate" in captured.out + assert "replicate" in captured.err def test_sort_fraction_column_missing_from_the_reads_table_fails(tmp_path, capsys): @@ -439,7 +445,9 @@ def test_sort_fraction_column_missing_from_the_reads_table_fails(tmp_path, capsy assert code == 1 assert not out_dir.exists() - assert "absent" in capsys.readouterr().out + captured = capsys.readouterr() + assert "absent" in captured.out + assert "absent" in captured.err # ---------------------------------------------------------------------------