From ccbe14f814e1e787135b8580ae3f546760d737e3 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Fri, 31 Jul 2026 11:06:14 +0500 Subject: [PATCH] fix(workflows): escape the literal bracket in workflow resolve output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `workflow_resolve` printed each layer as f" • [{layer.tier}] {layer.source} (priority={priority})" Rich parses `[base]` and `[project-overlay]` as style tags, so the tier column was silently swallowed on every run: BEFORE: '* .specify/workflows/wf/workflow.yml (priority=n/a)' AFTER : '* [base] .specify/workflows/wf/workflow.yml (priority=n/a)' The tier is one of the two things the command exists to report, and the JSON payload still carried it correctly — only the human-readable output lost it. The existing test asserts `"base" in result.output`, which passes anyway because the step-attribution section prints the same word as a source, so the missing column was masked. Escape the literal bracket with `\[`, matching presets/_commands.py:378, and escape the user-controlled layer sources and step ids while there. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/overlays/_commands.py | 14 +++++- tests/workflows/test_overlay_commands.py | 45 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/workflows/overlays/_commands.py b/src/specify_cli/workflows/overlays/_commands.py index cec7d8534a..423cf3457c 100644 --- a/src/specify_cli/workflows/overlays/_commands.py +++ b/src/specify_cli/workflows/overlays/_commands.py @@ -7,6 +7,7 @@ import typer import yaml +from rich.markup import escape as _escape_markup from ..._console import console, err_console from ...extensions import normalize_priority @@ -412,14 +413,23 @@ def workflow_resolve(project_root: Path, workflow_id: str) -> dict[str, Any] | N priority = ( "n/a" if layer.tier == "base" else str(normalize_priority(layer.priority)) ) + # Escape the literal bracket (\[) so Rich renders `[]` instead of + # parsing it as a style tag: `[base]` and `[project-overlay]` were + # silently swallowed, so the tier column vanished from the output + # entirely. Layer sources and step ids are user-controlled paths/ids, + # so escape those values too. console.print( - f" \u2022 [{layer.tier}] {layer.source} " + f" \u2022 \\[{_escape_markup(str(layer.tier))}] " + f"{_escape_markup(str(layer.source))} " f"(priority={priority})" ) console.print("Step attribution:") for composed in attribution: - console.print(f" \u2022 {composed.step_id}: {composed.source}") + console.print( + f" \u2022 {_escape_markup(str(composed.step_id))}: " + f"{_escape_markup(str(composed.source))}" + ) return { "workflow_id": workflow_id, diff --git a/tests/workflows/test_overlay_commands.py b/tests/workflows/test_overlay_commands.py index e068738005..4451dd057d 100644 --- a/tests/workflows/test_overlay_commands.py +++ b/tests/workflows/test_overlay_commands.py @@ -509,6 +509,51 @@ def test_workflow_resolve(self, project_dir, monkeypatch): assert payload["layers"][-1]["tier"] == "base" assert payload["layers"][-1]["priority"] is None + def test_workflow_resolve_prints_the_tier_column(self, project_dir, monkeypatch): + """The `[]` column must survive Rich rendering. + + The layer line interpolated a literal `[{layer.tier}]`, so Rich parsed + `[base]` / `[project-overlay]` as style tags and swallowed them — the + tier column disappeared from every `workflow resolve` run. A bare + `"base" in result.output` does not catch this, because the + step-attribution section prints the same word as a source; the tier has + to be asserted in bracket form on the layers line itself. + """ + monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir) + _write_workflow( + project_dir, + "wf", + { + "schema_version": "1.0", + "workflow": {"id": "wf", "name": "WF", "version": "1.0.0"}, + "steps": [{"id": "a", "type": "command", "command": "echo"}], + }, + ) + _write_overlay( + project_dir, + "wf", + "ov1", + { + "id": "ov1", + "extends": "wf", + "priority": 10, + "edits": [{"remove": "a"}], + }, + ) + + result = runner.invoke(app, ["workflow", "resolve", "wf"]) + assert result.exit_code == 0, result.output + + layer_lines = [ + line + for line in result.output.splitlines() + if "priority=" in line + ] + assert layer_lines, result.output + joined = "\n".join(layer_lines) + assert "[base]" in joined, joined + assert "[project-overlay]" in joined, joined + def test_workflow_resolve_equal_priority_layers_sort_by_source(self, project_dir, monkeypatch): """Equal-priority overlays are listed alphabetically by source.""" monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir)