fix: escape Rich markup in catalog list output (#3738)

The `catalog list` subcommands for workflows, workflow steps, presets,
and integrations printed user-editable catalog fields (name/url/
description from the `*-catalogs.yml` files) through `console.print`
with Rich markup enabled. Any bracketed content such as a description
`Does [stuff] nicely` was parsed as a style tag and silently swallowed,
and a malformed tag could raise while rendering.

Route each untrusted field through the module's already-imported
`escape` helper, matching the pattern already used by
`extension catalog list`.

Adds regression tests for all four commands that inject bracketed
name/url/description and assert the brackets survive verbatim in the
output.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Noor ul ain
2026-07-27 20:15:37 +05:00
committed by GitHub
parent 103ad73775
commit 99dc915ae3
6 changed files with 124 additions and 13 deletions

View File

@@ -489,13 +489,14 @@ def integration_catalog_list():
display_name = str(raw_name).strip() if raw_name is not None else ""
if not display_name:
display_name = f"catalog-{i + 1}"
safe_name = _rich_escape(display_name)
if env_override or project_configs is None:
console.print(f" - [bold]{display_name}[/bold] — {install_status}")
console.print(f" - [bold]{safe_name}[/bold] — {install_status}")
else:
console.print(f" [{i}] [bold]{display_name}[/bold] — {install_status}")
console.print(f" {cfg.get('url', '')}")
console.print(f" [{i}] [bold]{safe_name}[/bold] — {install_status}")
console.print(f" {_rich_escape(str(cfg.get('url', '')))}")
if cfg.get("description"):
console.print(f" [dim]{cfg['description']}[/dim]")
console.print(f" [dim]{_rich_escape(str(cfg['description']))}[/dim]")
console.print()

View File

@@ -580,10 +580,10 @@ def preset_catalog_list():
if entry.install_allowed
else "[yellow]discovery only[/yellow]"
)
console.print(f" [bold]{entry.name}[/bold] (priority {entry.priority})")
console.print(f" [bold]{_escape_markup(str(entry.name))}[/bold] (priority {entry.priority})")
if entry.description:
console.print(f" {entry.description}")
console.print(f" URL: {entry.url}")
console.print(f" {_escape_markup(str(entry.description))}")
console.print(f" URL: {_escape_markup(str(entry.url))}")
console.print(f" Install: {install_str}")
console.print()

View File

@@ -2424,10 +2424,10 @@ def workflow_catalog_list():
console.print("\n[bold cyan]Workflow Catalog Sources:[/bold cyan]\n")
for i, cfg in enumerate(configs):
install_status = "[green]install allowed[/green]" if cfg["install_allowed"] else "[yellow]discovery only[/yellow]"
console.print(f" [{i}] [bold]{cfg['name']}[/bold] — {install_status}")
console.print(f" {cfg['url']}")
console.print(f" [{i}] [bold]{_escape_markup(str(cfg['name']))}[/bold] — {install_status}")
console.print(f" {_escape_markup(str(cfg['url']))}")
if cfg.get("description"):
console.print(f" [dim]{cfg['description']}[/dim]")
console.print(f" [dim]{_escape_markup(str(cfg['description']))}[/dim]")
console.print()
@@ -3067,10 +3067,10 @@ def workflow_step_catalog_list():
if cfg["install_allowed"]
else "[yellow]discovery only[/yellow]"
)
console.print(f" [{i}] [bold]{cfg['name']}[/bold] — {install_status}")
console.print(f" {cfg['url']}")
console.print(f" [{i}] [bold]{_escape_markup(str(cfg['name']))}[/bold] — {install_status}")
console.print(f" {_escape_markup(str(cfg['url']))}")
if cfg.get("description"):
console.print(f" [dim]{cfg['description']}[/dim]")
console.print(f" [dim]{_escape_markup(str(cfg['description']))}[/dim]")
console.print()

View File

@@ -633,6 +633,40 @@ class TestIntegrationListCatalog:
assert "copilot" in result.output
assert "installed" in result.output
def test_catalog_list_escapes_rich_markup(self, tmp_path, monkeypatch):
"""User-editable catalog name/url/description must not be parsed as Rich markup."""
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.integrations.catalog import IntegrationCatalog
runner = CliRunner()
project = self._init_project(tmp_path)
configs = [
{
"name": "Bracket [Catalog]",
"url": "https://example.com/[cat].json",
"description": "desc [with] brackets",
"install_allowed": True,
},
]
monkeypatch.setattr(
IntegrationCatalog,
"get_project_catalog_configs",
lambda self: [dict(c) for c in configs],
)
old = os.getcwd()
try:
os.chdir(project)
result = runner.invoke(app, ["integration", "catalog", "list"])
finally:
os.chdir(old)
assert result.exit_code == 0, result.output
assert "Bracket [Catalog]" in result.output
assert "https://example.com/[cat].json" in result.output
assert "desc [with] brackets" in result.output
# ---------------------------------------------------------------------------
# CLI: integration upgrade

View File

@@ -2637,6 +2637,28 @@ class TestPresetCatalogMultiCatalog:
assert active[1].priority == 2
assert active[1].install_allowed is False
def test_catalog_list_escapes_rich_markup(self, project_dir):
"""User-editable catalog name/url/description must not be parsed as Rich markup."""
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app
entry = PresetCatalogEntry(
url="https://example.com/[cat].json",
name="Bracket [Catalog]",
priority=1,
install_allowed=True,
description="desc [with] brackets",
)
runner = CliRunner()
with patch.object(Path, "cwd", return_value=project_dir), \
patch.object(PresetCatalog, "get_active_catalogs", return_value=[entry]):
result = runner.invoke(app, ["preset", "catalog", "list"])
assert result.exit_code == 0, result.output
assert "Bracket [Catalog]" in result.output
assert "https://example.com/[cat].json" in result.output
assert "desc [with] brackets" in result.output
def test_env_var_overrides_catalogs(self, project_dir, monkeypatch):
"""Test that SPECKIT_PRESET_CATALOG_URL env var overrides defaults."""
monkeypatch.setenv(

View File

@@ -10077,6 +10077,60 @@ steps:
assert "desc [with] brackets" in result.output
assert "tag[1]" in result.output
def test_catalog_list_escapes_rich_markup(self, project_dir, monkeypatch):
"""User-editable catalog name/url/description must not be parsed as Rich markup."""
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import WorkflowCatalog
monkeypatch.chdir(project_dir)
configs = [
{
"name": "Bracket [Catalog]",
"url": "https://example.com/[cat].json",
"description": "desc [with] brackets",
"install_allowed": True,
},
]
monkeypatch.setattr(
WorkflowCatalog,
"get_catalog_configs",
lambda self: [dict(c) for c in configs],
)
runner = CliRunner()
result = runner.invoke(app, ["workflow", "catalog", "list"])
assert result.exit_code == 0, result.output
assert "Bracket [Catalog]" in result.output
assert "https://example.com/[cat].json" in result.output
assert "desc [with] brackets" in result.output
def test_step_catalog_list_escapes_rich_markup(self, project_dir, monkeypatch):
"""User-editable step-catalog name/url/description must not be parsed as Rich markup."""
from typer.testing import CliRunner
from specify_cli import app
from specify_cli.workflows.catalog import StepCatalog
monkeypatch.chdir(project_dir)
configs = [
{
"name": "Bracket [Step]",
"url": "https://example.com/[step].json",
"description": "step [with] brackets",
"install_allowed": True,
},
]
monkeypatch.setattr(
StepCatalog,
"get_catalog_configs",
lambda self: [dict(c) for c in configs],
)
runner = CliRunner()
result = runner.invoke(app, ["workflow", "step", "catalog", "list"])
assert result.exit_code == 0, result.output
assert "Bracket [Step]" in result.output
assert "https://example.com/[step].json" in result.output
assert "step [with] brackets" in result.output
# -- update ----------------------------------------------------------
def test_update_no_workflows_installed(self, project_dir, monkeypatch):