Skip to content

Commit 77743e8

Browse files
jawwad-aliclaude
andcommitted
fix(workflows): route 'workflow status --json' errors to stderr
The workflow_status run_id error paths (FileNotFoundError -> 'Run not found', ValueError -> invalid run) used the stdout console and fired before the json_output branch, so 'specify workflow status <bad-id> --json' wrote a Rich-rendered error to stdout and corrupted the JSON stream a consumer would json.loads(). Route both through _error_console(json_output) so they go to stderr under --json, matching the sibling 'workflow run'/'workflow resume' commands (which use the identical RunState.load try/except) and the documented stdout-purity contract. Test asserts the not-found error appears on stderr and stdout stays empty under --json (fails before: the error was on stdout). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent faeb956 commit 77743e8

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

src/specify_cli/workflows/_commands.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,14 +1262,18 @@ def workflow_status(
12621262
engine = WorkflowEngine(project_root)
12631263

12641264
if run_id:
1265+
# Route errors to stderr under --json so the stdout JSON stream stays
1266+
# parseable (mirrors `workflow run`/`workflow resume`); both handlers
1267+
# fire before the json_output branch below.
1268+
err = _error_console(json_output)
12651269
try:
12661270
from .engine import RunState
12671271
state = RunState.load(run_id, project_root)
12681272
except FileNotFoundError:
1269-
console.print(f"[red]Error:[/red] Run not found: {run_id}")
1273+
err.print(f"[red]Error:[/red] Run not found: {run_id}")
12701274
raise typer.Exit(1)
12711275
except ValueError as exc:
1272-
console.print(f"[red]Error:[/red] {_escape_markup(str(exc))}")
1276+
err.print(f"[red]Error:[/red] {_escape_markup(str(exc))}")
12731277
raise typer.Exit(1)
12741278

12751279
if json_output:

tests/test_workflows.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12455,6 +12455,29 @@ def test_status_run_not_found_unchanged(self, project_dir, monkeypatch):
1245512455
assert result.exit_code != 0
1245612456
assert "Run not found: nonexistent-run" in result.output
1245712457

12458+
def test_status_json_not_found_error_goes_to_stderr(
12459+
self, project_dir, monkeypatch, capsys
12460+
):
12461+
"""Under --json, the not-found/invalid-run error must go to stderr so the
12462+
stdout JSON stream stays parseable (empty on the error path) — mirroring
12463+
`workflow run`/`workflow resume`. Before this fix both handlers used the
12464+
stdout console, corrupting a consumer's json.loads(stdout)."""
12465+
import typer
12466+
from specify_cli.workflows import _commands
12467+
12468+
(project_dir / ".specify" / "workflows").mkdir(parents=True, exist_ok=True)
12469+
monkeypatch.setattr(
12470+
_commands, "_require_specify_project", lambda: project_dir
12471+
)
12472+
with pytest.raises(typer.Exit) as exc:
12473+
_commands.workflow_status("does-not-exist", json_output=True)
12474+
assert exc.value.exit_code == 1
12475+
captured = capsys.readouterr()
12476+
assert "Run not found" in captured.err
12477+
assert "Run not found" not in captured.out
12478+
# stdout carries no partial/corrupt JSON on the error path.
12479+
assert captured.out.strip() == ""
12480+
1245812481
def test_status_no_run_id_list_path_unaffected(self, project_dir, monkeypatch):
1245912482
"""The no-run-id list-all-runs path must remain unaffected by the
1246012483
new single-run ValueError boundary."""

0 commit comments

Comments
 (0)