From 4f5e7121163ef8d8d546f0153f831f402caf8cfd Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:52:04 +0200 Subject: [PATCH] fix(presets): escape catalog metadata in discovery output Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/presets/_commands.py | 40 ++++++++++++----- tests/test_presets.py | 66 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/specify_cli/presets/_commands.py b/src/specify_cli/presets/_commands.py index 154346209f..a8076018ac 100644 --- a/src/specify_cli/presets/_commands.py +++ b/src/specify_cli/presets/_commands.py @@ -285,8 +285,13 @@ def preset_search( console.print(f"\n[bold cyan]Presets ({len(results)} found):[/bold cyan]\n") for pack in results: - console.print(f" [bold]{pack.get('name', pack['id'])}[/bold] ({pack['id']}) v{pack.get('version', '?')}") - console.print(f" {pack.get('description', '')}") + name = _escape_markup(str(pack.get("name", pack["id"]))) + pack_id = _escape_markup(str(pack["id"])) + version = _escape_markup(str(pack.get("version", "?"))) + console.print(f" [bold]{name}[/bold] ({pack_id}) v{version}") + console.print( + f" {_escape_markup(str(pack.get('description', '')))}" + ) if pack.get("tags"): tags_str = ", ".join(str(t) for t in pack["tags"]) console.print(f" [dim]Tags: {tags_str}[/dim]") @@ -367,6 +372,7 @@ def preset_info( from . import PresetCatalog, PresetManager, PresetError project_root = _require_specify_project() + safe_preset_id = _escape_markup(str(preset_id)) # Check if installed locally first manager = PresetManager(project_root) local_pack = manager.get_pack(preset_id) @@ -408,20 +414,32 @@ def preset_info( console.print(f"[red]Error:[/red] Preset '{preset_id}' not found (not installed and not in catalog)") raise typer.Exit(1) - console.print(f"\n[bold cyan]Preset: {pack_info.get('name', preset_id)}[/bold cyan]\n") - console.print(f" ID: {pack_info['id']}") - console.print(f" Version: {pack_info.get('version', '?')}") - console.print(f" Description: {pack_info.get('description', '')}") + name = _escape_markup(str(pack_info.get("name", preset_id))) + console.print(f"\n[bold cyan]Preset: {name}[/bold cyan]\n") + console.print(f" ID: {_escape_markup(str(pack_info['id']))}") + console.print( + f" Version: {_escape_markup(str(pack_info.get('version', '?')))}" + ) + console.print( + f" Description: {_escape_markup(str(pack_info.get('description', '')))}" + ) if pack_info.get("author"): - console.print(f" Author: {pack_info['author']}") + console.print( + f" Author: {_escape_markup(str(pack_info['author']))}" + ) if pack_info.get("tags"): - console.print(f" Tags: {', '.join(str(t) for t in pack_info['tags'])}") + tags_str = ", ".join(str(t) for t in pack_info["tags"]) + console.print(f" Tags: {tags_str}") if pack_info.get("repository"): - console.print(f" Repository: {pack_info['repository']}") + console.print( + f" Repository: {_escape_markup(str(pack_info['repository']))}" + ) if pack_info.get("license"): - console.print(f" License: {pack_info['license']}") + console.print( + f" License: {_escape_markup(str(pack_info['license']))}" + ) console.print("\n [yellow]Status: not installed[/yellow]") - console.print(f" Install with: [cyan]specify preset add {preset_id}[/cyan]") + console.print(f" Install with: [cyan]specify preset add {safe_preset_id}[/cyan]") console.print() diff --git a/tests/test_presets.py b/tests/test_presets.py index adf6484583..18e5c64439 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -11772,3 +11772,69 @@ def test_info_renders_non_string_tags(self, project_dir): assert result.exit_code == 0, result.output plain = strip_ansi(result.output) assert "Tags: 1, 2" in plain + + +class TestPresetCatalogRichMarkup: + """Catalog metadata must render as literal text in Rich output.""" + + MARKUP_PRESET = { + "id": "[red]markup-id[/red]", + "name": "[green]Markup Name[/green]", + "version": "[blue]1.0.0[/blue]", + "description": "[yellow]Markup Description[/yellow]", + "author": "[magenta]Markup Author[/magenta]", + "tags": ["[italic]markup-tag[/italic]"], + "repository": "[bold]Markup Repository[/bold]", + "license": "[cyan]Markup License[/cyan]", + } + + def test_search_escapes_catalog_markup(self, project_dir): + from typer.testing import CliRunner + from unittest.mock import patch + from specify_cli import app + + with patch.object(Path, "cwd", return_value=project_dir), patch.object( + PresetCatalog, + "search", + return_value=[self.MARKUP_PRESET], + ): + result = CliRunner().invoke(app, ["preset", "search"]) + + assert result.exit_code == 0, result.output + output = " ".join(strip_ansi(result.output).split()) + for value in ( + self.MARKUP_PRESET["id"], + self.MARKUP_PRESET["name"], + self.MARKUP_PRESET["version"], + self.MARKUP_PRESET["description"], + ): + assert value in output + + def test_info_escapes_catalog_markup(self, project_dir): + from typer.testing import CliRunner + from unittest.mock import patch + from specify_cli import app + + with patch.object(Path, "cwd", return_value=project_dir), patch.object( + PresetCatalog, + "get_pack_info", + return_value=self.MARKUP_PRESET, + ): + result = CliRunner().invoke( + app, + ["preset", "info", self.MARKUP_PRESET["id"]], + ) + + assert result.exit_code == 0, result.output + output = " ".join(strip_ansi(result.output).split()) + for field in ( + "id", + "name", + "version", + "description", + "author", + "repository", + "license", + ): + value = self.MARKUP_PRESET[field] + assert value in output