Skip to content

fix(extensions): reject malformed catalog entries before removal - #5033

Merged
mnriem merged 1 commit into
github:mainfrom
kartsan03:fix/extension-catalog-remove-malformed-entries
Oct 20, 2026
Merged

mnriem merged 1 commit into
github:mainfrom
kartsan03:fix/extension-catalog-remove-malformed-entries

Conversation

@kartsan03

Copy link
Copy Markdown
Contributor

Description

specify extension catalog remove <name> drops every entry of .specify/extension-catalogs.yml that isn't a mapping while it removes <name> (the list comprehension in src/specify_cli/extensions/catalog/command_remove.py). One bare URL line, such as - https://example.com/private.json instead of a name/url mapping, is enough:

  • Removing a name that isn't in the file still prints ✓ Removed catalog '<name>', exits 0, and rewrites the file without the malformed entry. The comment and the CRLF line endings go with it.
  • Removing the last valid catalog leaves an empty list, so the command deletes the file, including the user's other top-level keys.

#4992 fixed the same thing in specify preset catalog remove. This PR does the same for extensions: every entry is checked before anything is removed, and a non-mapping entry exits 1 with Invalid catalog entry at index N: expected a mapping, got <type>, the message #4992 uses. The file is left as it was. Valid configs behave as before.

specify extension catalog list already refuses such a file: on the config below it exits 1 with catalog entry at index 1: expected a mapping, got str from the shared catalog loader. remove rewrote the same file instead.

I haven't seen a user report of this. It came up while checking the other catalog commands after #4992.

Repro: specify init --here --integration codex --ignore-agent-tools, then this .specify/extension-catalogs.yml (CRLF):

# Team config: café
catalogs:
  - name: keep
    url: https://example.com/team.json
  - https://example.com/private.json
metadata: keep this too
Command main (f68ffca) This PR
specify extension catalog remove does-not-exist exit 0, ✓ Removed catalog 'does-not-exist'; file rewritten without the comment, the CRLF endings and the URL entry exit 1, Invalid catalog entry at index 1: expected a mapping, got str; file unchanged
specify extension catalog remove keep exit 0, ✓ Removed catalog 'keep' and Project catalog override removed; file deleted, metadata and the URL entry with it exit 1, same error; file unchanged

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

New tests in tests/specify_cli/extensions/catalog/test_command_remove.py, in the shape of #4992's:

  • test_malformed_entries_preserve_config: a string, number, boolean, null or list entry, first or last in the list, while removing an existing or a missing name. The command exits 1 with the index and type in the message, doesn't print "Removed catalog", and the file bytes (comment, café, CRLF, metadata) are unchanged.
  • test_remove_malformed_config_in_initialized_project: the two repro commands through the real CLI in a project made by specify init.

With only src/ reverted to main, the 22 new cases fail; with the fix all 29 tests in the file pass.

Full suite on Linux, Python 3.13.15: 12447 passed, 377 skipped, 11 failed. The 11 are template parity tests that run the bash scripts with the host python3, which has no PyYAML here (PyYAML is required to resolve preset template composition), the same 11 #4992 reports. With the checkout's .venv/bin first on PATH, those five test files pass (257 passed, 112 skipped).

Ruff 0.15.0 passes on the changed files.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (fill in the disclosure below)

AI disclosure: Two agents, both autonomous. Codex CLI (model: gpt-6.1-sol, reasoning effort xhigh, autonomous, sandboxed and offline) found this while comparing the catalog commands after #4992, wrote the reproduction, and generated the fix and tests. Claude Code (model: claude-opus-5-5, autonomous) reviewed the change, reran the tests against main and the fix, reran the repro with the real specify CLI, ran the full suite and lint, and drafted this description. Commits carry Assisted-by: trailers.

`specify extension catalog remove` filtered out every non-mapping entry
in `.specify/extension-catalogs.yml` while removing the named catalog.
Removing a name that isn't there then reported success and rewrote the
file without the malformed entry, and removing the last valid catalog
deleted the file with the user's other keys. Validate the entries first
and leave the file untouched, as github#4992 does for preset catalogs.

Assisted-by: Codex CLI (model: gpt-6.1-sol, autonomous)
Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)

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.

🟢 Approval recommended

Validation precedes all file mutations, and targeted regression coverage addresses the reported failure paths.

No open issues 🎉

What changed in this PR

Aligns extension catalog removal with #4992’s preset fix, preventing malformed configuration from being silently discarded.

Changes:

  • Validate every catalog entry before removal or file changes.
  • Add regression coverage for malformed entries, diagnostics, and byte-for-byte file preservation.

Tests were not rerun; regression results are author-reported.

File Description
tests/​specify_cli/​extensions/​catalog/​test_command_remove.py Adds malformed-entry and initialized-project regression tests.
src/​specify_cli/​extensions/​catalog/​command_remove.py Rejects non-mapping entries before filtering or modifying configuration.

Review effort: Max

@mnriem
mnriem merged commit 6ad2082 into github:main Oct 20, 2026
16 checks passed
@mnriem

mnriem commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Thank you!

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