Skip to content

fix(presets): escape installed preset metadata in Rich output - #3826

Open
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/preset-installed-metadata-markup
Open

fix(presets): escape installed preset metadata in Rich output#3826
Noor-ul-ain001 wants to merge 1 commit into
github:mainfrom
Noor-ul-ain001:fix/preset-installed-metadata-markup

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

preset.yml is user-editable, but the installed-preset display paths interpolate its fields straight into console.print, where Rich parses [...] as a style tag.

#3773 escaped the catalog branch of these commands; the local branch was left behind — so the same field renders correctly from a catalog and incorrectly once installed.

Silent data loss. Description Does [stuff] nicely:

  Description: Does  nicely

Hard crash. An unbalanced tag like Broken [/red] tag raises rich.errors.MarkupError, aborting with a traceback and exit 1 — the preset can't be inspected at all:

$ specify preset info markup-pack
  ID:          markup-pack
  Version:     1.0.0
MarkupError: closing tag '[/red]' at position 22 doesn't match any open tag

preset resolve was unescaped throughout, and it echoes its own argument, so this crashes on user input alone with no preset installed:

$ specify preset resolve 'no[/red]such'
MarkupError: closing tag '[/red]' at position 12 doesn't match any open tag

Separately, the composition chain's literal [{strategy_label}] was itself consumed as a style tag, so every chain line printed a blank label:

  Composition chain:
    1.  base-pack v1.0.0 → ...      # should read "1. [base] ..."
    2.  app-pack v1.0.0 → ...       # should read "2. [append] ..."

Fix

Route the untrusted fields through the already-imported _escape_markup:

  • preset list (installed branch) — name, id, version, description
  • preset info (installed branch) — name, id, version, description, author, tags, repository, license, and the per-template description
  • preset info (catalog branch) — the tags join the earlier sweep missed
  • preset resolve — the template_name argument, resolved paths, layer sources, composition-error message
  • composition chain — escaped the literal bracket as \[, matching the step-graph line in workflow info

No behaviour change beyond rendering: fields now display verbatim instead of being swallowed or crashing.

Tests

TestInstalledPresetRichMarkup in tests/test_presets.py — 5 tests covering markup fields in list/info, the template description line, the unbalanced-tag crash, the preset resolve argument crash, and the composition strategy labels.

Test-the-test: with the source fix stashed, all 5 fail (4 on markup assertions, 1 on the swallowed template description); with it applied, all 5 pass.

pytest tests/test_presets.py475 passed, 7 failed, 1 error, matching the pre-existing baseline on clean main for this Windows box (symlink tests requiring elevation + one unrelated GHES-asset error). None are in the touched paths.

🤖 Generated with Claude Code

`preset.yml` is user-editable, but the installed-preset display paths
interpolated its fields straight into `console.print`, where Rich parses
`[...]` as a style tag. PR github#3773 escaped the *catalog* branch of these
commands; the local branch was left behind, so the same field rendered
correctly from a catalog and incorrectly once installed.

Two failure modes:

- Silent data loss: a description `Does [stuff] nicely` renders as
  `Does  nicely`.
- Hard crash: an unbalanced tag such as `Broken [/red] tag` raises
  `rich.errors.MarkupError`, aborting `preset list`/`preset info` with a
  traceback and exit code 1 — the preset cannot be inspected at all.

Escaped the installed branch of `preset list` (name/id/version/
description) and `preset info` (name/id/version/description/author/tags/
repository/license plus the per-template description), and the catalog
branch's tags join that the earlier sweep missed.

`preset resolve` was unescaped throughout: it echoes its own
`template_name` argument, so `preset resolve 'no[/red]such'` crashed on
user input alone. Also escaped the resolved paths, layer sources, and
composition-error message.

Separately, the composition chain's `[{strategy_label}]` was consumed as
a style tag, so every chain line printed a blank label instead of
`[base]`/`[append]`. Escaped the literal bracket as `\[`, matching the
step-graph line in `workflow info`.

Regression tests in `TestInstalledPresetRichMarkup` cover all five
behaviours; each fails before this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Escapes user-controlled preset metadata so Rich renders it literally instead of swallowing tags or crashing.

Changes:

  • Escapes installed preset and catalog metadata.
  • Safely renders resolve paths, sources, errors, and strategy labels.
  • Adds Rich-markup regression tests.
Show a summary per file
File Description
src/specify_cli/presets/_commands.py Escapes untrusted Rich output.
tests/test_presets.py Adds installed-preset markup tests.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +474 to +475
catalog_tags_str = _escape_markup(", ".join(str(t) for t in catalog_tags))
console.print(f" Tags: {catalog_tags_str}")
Comment thread tests/test_presets.py
Comment on lines +12284 to +12288
def test_resolve_escapes_template_name(self, project_dir):
"""``preset resolve`` echoes its argument; an unbalanced tag must not crash."""
result = self._invoke(project_dir, ["preset", "resolve", "no[/red]such"])
assert result.exit_code == 0, (result.output, result.exception)
assert "no[/red]such" in strip_ansi(result.output)

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Copilot feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants