From 3fe1c24bde593760afdba4fc814f14e4fd4fdeb8 Mon Sep 17 00:00:00 2001 From: Chaoqi Date: Sat, 10 Oct 2026 11:58:58 +0800 Subject: [PATCH 1/2] test(extensions): cover removing the last project catalog Assisted-by: Claude Code (model: Claude Opus 5, autonomous) --- .../extensions/catalog/test_command_remove.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/specify_cli/extensions/catalog/test_command_remove.py b/tests/specify_cli/extensions/catalog/test_command_remove.py index a66891a121..c4e7f93342 100644 --- a/tests/specify_cli/extensions/catalog/test_command_remove.py +++ b/tests/specify_cli/extensions/catalog/test_command_remove.py @@ -73,3 +73,77 @@ def test_extension_catalog_remove_escapes_catalog_name_markup(self, tmp_path): assert result.exit_code == 0, result.output output = _normalize_cli_output(result.output) assert f"Removed catalog '{catalog_name}'" in output + + def test_extension_catalog_remove_final_entry_restores_defaults( + self, tmp_path, monkeypatch + ): + """The loader rejects an empty ``catalogs`` list, so removing the last + entry must not leave one behind for follow-up commands to trip on.""" + project = self._make_project(tmp_path) + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("USERPROFILE", str(tmp_path)) + monkeypatch.delenv("SPECKIT_CATALOG_URL", raising=False) + + add = self._invoke( + [ + "extension", + "catalog", + "add", + "https://only.example.com/catalog.json", + "--name", + "only", + ], + project, + ) + assert add.exit_code == 0, add.output + + remove = self._invoke(["extension", "catalog", "remove", "only"], project) + assert remove.exit_code == 0, remove.output + assert "Built-in defaults will be used" in _normalize_cli_output(remove.output) + + listing = self._invoke(["extension", "catalog", "list"], project) + assert listing.exit_code == 0, listing.output + output = _normalize_cli_output(listing.output) + assert "default (priority 1)" in output + assert "community (priority 2)" in output + assert "Using built-in default catalog stack" in output + assert not (project / ".specify" / "extension-catalogs.yml").exists() + + def test_extension_catalog_remove_keeps_config_with_remaining_entries( + self, tmp_path + ): + project = self._make_project(tmp_path) + keep = { + "name": "keep", + "url": "https://keep.example.com/catalog.json", + "priority": 20, + "install_allowed": False, + "description": "", + } + cfg_path = project / ".specify" / "extension-catalogs.yml" + cfg_path.write_text( + yaml.safe_dump( + { + "catalogs": [ + { + "name": "drop", + "url": "https://drop.example.com/catalog.json", + "priority": 10, + "install_allowed": False, + "description": "", + }, + keep, + ] + }, + sort_keys=False, + ), + encoding="utf-8", + ) + + result = self._invoke(["extension", "catalog", "remove", "drop"], project) + + assert result.exit_code == 0, result.output + assert "No catalogs remain" not in _normalize_cli_output(result.output) + assert yaml.safe_load(cfg_path.read_text(encoding="utf-8")) == { + "catalogs": [keep] + } From d48f19fe6ccb6b4d07d8b4e7518d169e0a45cf19 Mon Sep 17 00:00:00 2001 From: Chaoqi Date: Sat, 10 Oct 2026 12:00:23 +0800 Subject: [PATCH 2/2] fix(extensions): delete the catalog config when the last entry is removed `specify extension catalog remove` wrote `catalogs: []` after dropping the last entry and printed that built-in defaults would be used. The catalog loader rejects an existing config with no entries, so every later catalog-backed command (`catalog list`, `search`, `add`, `update`, `info`) failed until the file was deleted by hand. Delete the file instead, as `specify integration catalog remove` already does. Assisted-by: Claude Code (model: Claude Opus 5, autonomous) --- .../extensions/catalog/command_remove.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/specify_cli/extensions/catalog/command_remove.py b/src/specify_cli/extensions/catalog/command_remove.py index e99bc7faa9..4d42de937b 100644 --- a/src/specify_cli/extensions/catalog/command_remove.py +++ b/src/specify_cli/extensions/catalog/command_remove.py @@ -48,16 +48,21 @@ def catalog_remove( _commands.console.print(f"[red]Error:[/red] Catalog '{safe_name}' not found.") raise typer.Exit(1) - config["catalogs"] = catalogs - config_path.write_text( - _commands.yaml.safe_dump( - config, - default_flow_style=False, - sort_keys=False, - allow_unicode=True, - ), - encoding="utf-8", - ) + if catalogs: + config["catalogs"] = catalogs + config_path.write_text( + _commands.yaml.safe_dump( + config, + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + ), + encoding="utf-8", + ) + else: + # The catalog loader rejects an existing config with no entries, so + # delete the file instead of writing ``catalogs: []``. + config_path.unlink(missing_ok=True) _commands.console.print(f"[green]✓[/green] Removed catalog '{safe_name}'") if not catalogs: