Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/refusals-to-stderr.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file modified block/logos/organization-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 14 additions & 3 deletions software/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions software/tests/integration/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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


# ---------------------------------------------------------------------------
Expand Down
Loading