From 990a1513c2b0757239624aed57e161505067af89 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:04:55 -0500 Subject: [PATCH] fix: address PR review feedback for multi-catalog support - Rename 'org-approved' catalog to 'default' - Move 'catalogs' command to 'catalog list' for consistency - Add 'description' field to CatalogEntry dataclass - Add --description option to 'catalog add' CLI command - Align install_allowed default to False in _load_catalog_config - Add user-level config detection in catalog list footer - Fix _load_catalog_config docstring (document ValidationError) - Fix test isolation for test_search_by_tag, test_search_by_query, test_search_verified_only, test_get_extension_info - Update version to 0.1.14 and CHANGELOG - Update all docs (RFC, User Guide, API Reference) --- CHANGELOG.md | 14 +++++ extensions/EXTENSION-API-REFERENCE.md | 19 ++++--- extensions/EXTENSION-USER-GUIDE.md | 19 ++++--- extensions/RFC-EXTENSION-SYSTEM.md | 17 +++--- src/specify_cli/__init__.py | 30 +++++++--- src/specify_cli/extensions.py | 82 +++++++++++++++++++-------- tests/test_extensions.py | 82 +++++++++++++++++++++++++-- 7 files changed, 204 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08452ee6..4539e686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - feat: add Tabnine CLI agent support +- **Multi-Catalog Support (#1707)**: Extension catalog system now supports multiple active catalogs simultaneously via a catalog stack + - New `specify extension catalog list` command lists all active catalogs with name, URL, priority, and `install_allowed` status + - New `specify extension catalog add` and `specify extension catalog remove` commands for project-scoped catalog management + - Default built-in stack includes `catalog.json` (default, installable) and `catalog.community.json` (community, discovery only) — community extensions are now surfaced in search results out of the box + - `specify extension search` aggregates results across all active catalogs, annotating each result with source catalog + - `specify extension add` enforces `install_allowed` policy — extensions from discovery-only catalogs cannot be installed directly + - Project-level `.specify/extension-catalogs.yml` and user-level `~/.specify/extension-catalogs.yml` config files supported, with project-level taking precedence + - `SPECKIT_CATALOG_URL` environment variable still works for backward compatibility (replaces full stack with single catalog) + - All catalog URLs require HTTPS (HTTP allowed for localhost development) + - New `CatalogEntry` dataclass in `extensions.py` for catalog stack representation + - Per-URL hash-based caching for non-default catalogs; legacy cache preserved for default catalog + - Higher-priority catalogs win on merge conflicts (same extension id in multiple catalogs) + - 13 new tests covering catalog stack resolution, merge conflicts, URL validation, and `install_allowed` enforcement + - Updated RFC, Extension User Guide, and Extension API Reference documentation ## [0.1.13] - 2026-03-03 diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 4c2764c1..bd25d4bb 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -254,9 +254,10 @@ from specify_cli.extensions import CatalogEntry entry = CatalogEntry( url="https://example.com/catalog.json", - name="org-approved", + name="default", priority=1, install_allowed=True, + description="Built-in catalog of installable extensions", ) ``` @@ -268,6 +269,7 @@ entry = CatalogEntry( | `name` | `str` | Human-readable catalog name | | `priority` | `int` | Sort order (lower = higher priority, wins on conflicts) | | `install_allowed` | `bool` | Whether extensions from this catalog can be installed | +| `description` | `str` | Optional human-readable description of the catalog (default: empty) | ### ExtensionCatalog @@ -282,7 +284,7 @@ catalog = ExtensionCatalog(project_root) **Class attributes**: ```python -ExtensionCatalog.DEFAULT_CATALOG_URL # org-approved catalog URL +ExtensionCatalog.DEFAULT_CATALOG_URL # default catalog URL ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL ``` @@ -328,14 +330,16 @@ Each extension dict returned by `search()` and `get_extension_info()` includes: ```yaml catalogs: - - name: "org-approved" - url: "https://example.com/catalog.json" + - name: "default" + url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" priority: 1 install_allowed: true + description: "Built-in catalog of installable extensions" - name: "community" url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" priority: 2 install_allowed: false + description: "Community-contributed extensions (discovery only)" ``` ### HookExecutor @@ -604,11 +608,11 @@ EXECUTE_COMMAND: {command} **Output**: List of installed extensions with metadata -### extension catalogs +### extension catalog list -**Usage**: `specify extension catalogs` +**Usage**: `specify extension catalog list` -Lists all active catalogs in the current catalog stack, showing name, URL, priority, and `install_allowed` status. +Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and `install_allowed` status. ### extension catalog add @@ -619,6 +623,7 @@ Lists all active catalogs in the current catalog stack, showing name, URL, prior - `--name NAME` - Catalog name (required) - `--priority INT` - Priority (lower = higher priority, default: 10) - `--install-allowed / --no-install-allowed` - Allow installs from this catalog (default: false) +- `--description TEXT` - Optional description of the catalog **Arguments**: diff --git a/extensions/EXTENSION-USER-GUIDE.md b/extensions/EXTENSION-USER-GUIDE.md index e561cf2b..e551809e 100644 --- a/extensions/EXTENSION-USER-GUIDE.md +++ b/extensions/EXTENSION-USER-GUIDE.md @@ -84,7 +84,7 @@ vim .specify/extensions/jira/jira-config.yml specify extension search ``` -Shows all extensions across all active catalogs (org-approved and community by default). +Shows all extensions across all active catalogs (default and community by default). ### Search by Keyword @@ -423,13 +423,13 @@ Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simul | Priority | Catalog | Install Allowed | Purpose | |----------|---------|-----------------|---------| -| 1 | `catalog.json` (org-approved) | ✅ Yes | Extensions your org approves for installation | +| 1 | `catalog.json` (default) | ✅ Yes | Curated extensions available for installation | | 2 | `catalog.community.json` (community) | ❌ No (discovery only) | Browse community extensions | ### Listing Active Catalogs ```bash -specify extension catalogs +specify extension catalog list ``` ### Adding a Catalog (Project-scoped) @@ -463,23 +463,26 @@ You can also edit `.specify/extension-catalogs.yml` directly: ```yaml catalogs: - - name: "org-approved" + - name: "default" url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" priority: 1 install_allowed: true + description: "Built-in catalog of installable extensions" - name: "internal" url: "https://internal.company.com/spec-kit/catalog.json" priority: 2 install_allowed: true + description: "Internal company extensions" - name: "community" url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" priority: 3 install_allowed: false + description: "Community-contributed extensions (discovery only)" ``` -A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when present. +A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when it contains one or more catalog entries. An empty `catalogs: []` list falls back to built-in defaults. ## Organization Catalog Customization @@ -569,7 +572,7 @@ Add to `.specify/extension-catalogs.yml` in your project: ```yaml catalogs: - - name: "org-approved" + - name: "my-org" url: "https://your-org.com/spec-kit/catalog.json" priority: 1 install_allowed: true @@ -579,7 +582,7 @@ Or use the CLI: ```bash specify extension catalog add \ - --name "org-approved" \ + --name "my-org" \ --install-allowed \ https://your-org.com/spec-kit/catalog.json ``` @@ -595,7 +598,7 @@ export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" ```bash # List active catalogs -specify extension catalogs +specify extension catalog list # Search should now show your catalog's extensions specify extension search diff --git a/extensions/RFC-EXTENSION-SYSTEM.md b/extensions/RFC-EXTENSION-SYSTEM.md index 7df07e42..c6469c48 100644 --- a/extensions/RFC-EXTENSION-SYSTEM.md +++ b/extensions/RFC-EXTENSION-SYSTEM.md @@ -961,7 +961,7 @@ specify extension info jira ### Custom Catalogs -Spec Kit supports a **catalog stack** — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to benefit from org-approved extensions, an internal catalog, and community discovery all at once. +Spec Kit supports a **catalog stack** — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to maintain their own org-approved extensions alongside an internal catalog and community discovery, all at once. #### Catalog Stack Resolution @@ -978,38 +978,41 @@ When no config file exists, the CLI uses: | Priority | Catalog | install_allowed | Purpose | |----------|---------|-----------------|---------| -| 1 | `catalog.json` (org-approved) | `true` | Extensions your org approves for installation | +| 1 | `catalog.json` (default) | `true` | Curated extensions available for installation | | 2 | `catalog.community.json` (community) | `false` | Discovery only — browse but not install | -This means `specify extension search` surfaces community extensions out of the box, while `specify extension add` is still restricted to org-approved entries. +This means `specify extension search` surfaces community extensions out of the box, while `specify extension add` is still restricted to entries from catalogs with `install_allowed: true`. #### `.specify/extension-catalogs.yml` Config File ```yaml catalogs: - - name: "org-approved" + - name: "default" url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json" priority: 1 # Highest — only approved entries can be installed install_allowed: true + description: "Built-in catalog of installable extensions" - name: "internal" url: "https://internal.company.com/spec-kit/catalog.json" priority: 2 install_allowed: true + description: "Internal company extensions" - name: "community" url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json" priority: 3 # Lowest — discovery only, not installable install_allowed: false + description: "Community-contributed extensions (discovery only)" ``` -A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. When a project-level config is present, it takes full control and the built-in defaults are not applied. +A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. When a project-level config is present with one or more catalog entries, it takes full control and the built-in defaults are not applied. An empty `catalogs: []` list is treated the same as no config file, falling back to defaults. #### Catalog CLI Commands ```bash # List active catalogs with name, URL, priority, and install_allowed -specify extension catalogs +specify extension catalog list # Add a catalog (project-scoped) specify extension catalog add --name "internal" --install-allowed \ @@ -1024,7 +1027,7 @@ specify extension catalog remove internal # Show which catalog an extension came from specify extension info jira -# → Source catalog: org-approved +# → Source catalog: default ``` #### Merge Conflict Resolution diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 603fb06b..babd1b4f 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1844,8 +1844,8 @@ def extension_list( console.print(" [cyan]specify extension add [/cyan]") -@extension_app.command("catalogs") -def extension_catalogs(): +@catalog_app.command("list") +def catalog_list(): """List all active extension catalogs.""" from .extensions import ExtensionCatalog, ValidationError @@ -1873,15 +1873,20 @@ def extension_catalogs(): else "[yellow]discovery only[/yellow]" ) console.print(f" [bold]{entry.name}[/bold] (priority {entry.priority})") + if entry.description: + console.print(f" {entry.description}") console.print(f" URL: {entry.url}") console.print(f" Install: {install_str}") console.print() config_path = project_root / ".specify" / "extension-catalogs.yml" + user_config_path = Path.home() / ".specify" / "extension-catalogs.yml" if config_path.exists() and catalog._load_catalog_config(config_path) is not None: console.print(f"[dim]Config: {config_path.relative_to(project_root)}[/dim]") elif os.environ.get("SPECKIT_CATALOG_URL"): console.print("[dim]Catalog configured via SPECKIT_CATALOG_URL environment variable.[/dim]") + elif user_config_path.exists() and catalog._load_catalog_config(user_config_path) is not None: + console.print("[dim]Config: ~/.specify/extension-catalogs.yml[/dim]") else: console.print("[dim]Using built-in default catalog stack.[/dim]") console.print( @@ -1898,6 +1903,7 @@ def catalog_add( False, "--install-allowed/--no-install-allowed", help="Allow extensions from this catalog to be installed", ), + description: str = typer.Option("", "--description", help="Description of the catalog"), ): """Add a catalog to .specify/extension-catalogs.yml.""" from .extensions import ExtensionCatalog, ValidationError @@ -1924,16 +1930,20 @@ def catalog_add( if config_path.exists(): try: config = yaml.safe_load(config_path.read_text()) or {} - except Exception: - config = {} + except Exception as e: + console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}") + raise typer.Exit(1) else: config = {} catalogs = config.get("catalogs", []) + if not isinstance(catalogs, list): + console.print("[red]Error:[/red] Invalid catalog config: 'catalogs' must be a list.") + raise typer.Exit(1) # Check for duplicate name for existing in catalogs: - if existing.get("name") == name: + if isinstance(existing, dict) and existing.get("name") == name: console.print(f"[yellow]Warning:[/yellow] A catalog named '{name}' already exists.") console.print("Use 'specify extension catalog remove' first, or choose a different name.") raise typer.Exit(1) @@ -1943,6 +1953,7 @@ def catalog_add( "url": url, "priority": priority, "install_allowed": install_allowed, + "description": description, }) config["catalogs"] = catalogs @@ -1980,8 +1991,11 @@ def catalog_remove( raise typer.Exit(1) catalogs = config.get("catalogs", []) + if not isinstance(catalogs, list): + console.print("[red]Error:[/red] Invalid catalog config: 'catalogs' must be a list.") + raise typer.Exit(1) original_count = len(catalogs) - catalogs = [c for c in catalogs if c.get("name") != name] + catalogs = [c for c in catalogs if isinstance(c, dict) and c.get("name") != name] if len(catalogs) == original_count: console.print(f"[red]Error:[/red] Catalog '{name}' not found.") @@ -2268,8 +2282,8 @@ def extension_search( else: console.print(f"\n [yellow]⚠[/yellow] Not directly installable from '{catalog_name}'.") console.print( - " Add to an approved catalog with install_allowed: true, " - "or use: specify extension add --from " + f" Add to an approved catalog with install_allowed: true, " + f"or install from a ZIP URL: specify extension add {ext['id']} --from " ) console.print() diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index a02d10ec..b1045e3c 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -45,6 +45,7 @@ class CatalogEntry: name: str priority: int install_allowed: bool + description: str = "" class ExtensionManifest: @@ -1032,30 +1033,57 @@ class ExtensionCatalog: Returns: Ordered list of CatalogEntry objects, or None if file doesn't exist or contains no valid catalog entries. + + Raises: + ValidationError: If any catalog entry has an invalid URL, + the file cannot be parsed, or a priority value is invalid. """ if not config_path.exists(): return None try: data = yaml.safe_load(config_path.read_text()) or {} - catalogs_data = data.get("catalogs", []) - if not catalogs_data: - return None - entries: List[CatalogEntry] = [] - for idx, item in enumerate(catalogs_data): - url = str(item.get("url", "")).strip() - if not url: - continue - self._validate_catalog_url(url) - entries.append(CatalogEntry( - url=url, - name=str(item.get("name", f"catalog-{idx + 1}")), - priority=int(item.get("priority", idx + 1)), - install_allowed=bool(item.get("install_allowed", True)), - )) - entries.sort(key=lambda e: e.priority) - return entries if entries else None - except (yaml.YAMLError, OSError): + except (yaml.YAMLError, OSError) as e: + raise ValidationError( + f"Failed to read catalog config {config_path}: {e}" + ) + catalogs_data = data.get("catalogs", []) + if not catalogs_data: return None + if not isinstance(catalogs_data, list): + raise ValidationError( + f"Invalid catalog config: 'catalogs' must be a list, got {type(catalogs_data).__name__}" + ) + entries: List[CatalogEntry] = [] + for idx, item in enumerate(catalogs_data): + if not isinstance(item, dict): + raise ValidationError( + f"Invalid catalog entry at index {idx}: expected a mapping, got {type(item).__name__}" + ) + url = str(item.get("url", "")).strip() + if not url: + continue + self._validate_catalog_url(url) + try: + priority = int(item.get("priority", idx + 1)) + except (TypeError, ValueError): + raise ValidationError( + f"Invalid priority for catalog '{item.get('name', idx + 1)}': " + f"expected integer, got {item.get('priority')!r}" + ) + raw_install = item.get("install_allowed", False) + if isinstance(raw_install, str): + install_allowed = raw_install.strip().lower() in ("true", "yes", "1") + else: + install_allowed = bool(raw_install) + entries.append(CatalogEntry( + url=url, + name=str(item.get("name", f"catalog-{idx + 1}")), + priority=priority, + install_allowed=install_allowed, + description=str(item.get("description", "")), + )) + entries.sort(key=lambda e: e.priority) + return entries if entries else None def get_active_catalogs(self) -> List[CatalogEntry]: """Get the ordered list of active catalogs. @@ -1064,7 +1092,7 @@ class ExtensionCatalog: 1. SPECKIT_CATALOG_URL env var — single catalog replacing all defaults 2. Project-level .specify/extension-catalogs.yml 3. User-level ~/.specify/extension-catalogs.yml - 4. Built-in default stack (org-approved + community) + 4. Built-in default stack (default + community) Returns: List of CatalogEntry objects sorted by priority (ascending) @@ -1086,7 +1114,7 @@ class ExtensionCatalog: file=sys.stderr, ) self._non_default_catalog_warning_shown = True - return [CatalogEntry(url=catalog_url, name="custom", priority=1, install_allowed=True)] + return [CatalogEntry(url=catalog_url, name="custom", priority=1, install_allowed=True, description="Custom catalog via SPECKIT_CATALOG_URL")] # 2. Project-level config overrides all defaults project_config_path = self.project_root / ".specify" / "extension-catalogs.yml" @@ -1102,8 +1130,8 @@ class ExtensionCatalog: # 4. Built-in default stack return [ - CatalogEntry(url=self.DEFAULT_CATALOG_URL, name="org-approved", priority=1, install_allowed=True), - CatalogEntry(url=self.COMMUNITY_CATALOG_URL, name="community", priority=2, install_allowed=False), + CatalogEntry(url=self.DEFAULT_CATALOG_URL, name="default", priority=1, install_allowed=True, description="Built-in catalog of installable extensions"), + CatalogEntry(url=self.COMMUNITY_CATALOG_URL, name="community", priority=2, install_allowed=False, description="Community-contributed extensions (discovery only)"), ] def get_catalog_url(self) -> str: @@ -1155,9 +1183,11 @@ class ExtensionCatalog: try: metadata = json.loads(cache_meta_file.read_text()) cached_at = datetime.fromisoformat(metadata.get("cached_at", "")) + if cached_at.tzinfo is None: + cached_at = cached_at.replace(tzinfo=timezone.utc) age = (datetime.now(timezone.utc) - cached_at).total_seconds() is_valid = age < self.CACHE_DURATION - except (json.JSONDecodeError, ValueError, KeyError): + except (json.JSONDecodeError, ValueError, KeyError, TypeError): # If metadata is invalid or missing expected fields, treat cache as invalid pass @@ -1231,8 +1261,8 @@ class ExtensionCatalog: for ext_id, ext_data in catalog_data.get("extensions", {}).items(): if ext_id not in merged: # Higher-priority catalog wins merged[ext_id] = { - "id": ext_id, **ext_data, + "id": ext_id, "_catalog_name": catalog_entry.name, "_install_allowed": catalog_entry.install_allowed, } @@ -1254,9 +1284,11 @@ class ExtensionCatalog: try: metadata = json.loads(self.cache_metadata_file.read_text()) cached_at = datetime.fromisoformat(metadata.get("cached_at", "")) + if cached_at.tzinfo is None: + cached_at = cached_at.replace(tzinfo=timezone.utc) age_seconds = (datetime.now(timezone.utc) - cached_at).total_seconds() return age_seconds < self.CACHE_DURATION - except (json.JSONDecodeError, ValueError, KeyError): + except (json.JSONDecodeError, ValueError, KeyError, TypeError): return False def fetch_catalog(self, force_refresh: bool = False) -> Dict[str, Any]: diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 5f520154..9ef9cb73 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -950,10 +950,29 @@ class TestExtensionCatalog: def test_search_by_query(self, temp_dir): """Test searching by query text.""" + import yaml as yaml_module + project_dir = temp_dir / "project" project_dir.mkdir() (project_dir / ".specify").mkdir() + # Use a single-catalog config so community extensions don't interfere + config_path = project_dir / ".specify" / "extension-catalogs.yml" + with open(config_path, "w") as f: + yaml_module.dump( + { + "catalogs": [ + { + "name": "test-catalog", + "url": ExtensionCatalog.DEFAULT_CATALOG_URL, + "priority": 1, + "install_allowed": True, + } + ] + }, + f, + ) + catalog = ExtensionCatalog(project_dir) # Create mock catalog @@ -995,10 +1014,29 @@ class TestExtensionCatalog: def test_search_by_tag(self, temp_dir): """Test searching by tag.""" + import yaml as yaml_module + project_dir = temp_dir / "project" project_dir.mkdir() (project_dir / ".specify").mkdir() + # Use a single-catalog config so community extensions don't interfere + config_path = project_dir / ".specify" / "extension-catalogs.yml" + with open(config_path, "w") as f: + yaml_module.dump( + { + "catalogs": [ + { + "name": "test-catalog", + "url": ExtensionCatalog.DEFAULT_CATALOG_URL, + "priority": 1, + "install_allowed": True, + } + ] + }, + f, + ) + catalog = ExtensionCatalog(project_dir) # Create mock catalog @@ -1047,10 +1085,29 @@ class TestExtensionCatalog: def test_search_verified_only(self, temp_dir): """Test searching verified extensions only.""" + import yaml as yaml_module + project_dir = temp_dir / "project" project_dir.mkdir() (project_dir / ".specify").mkdir() + # Use a single-catalog config so community extensions don't interfere + config_path = project_dir / ".specify" / "extension-catalogs.yml" + with open(config_path, "w") as f: + yaml_module.dump( + { + "catalogs": [ + { + "name": "test-catalog", + "url": ExtensionCatalog.DEFAULT_CATALOG_URL, + "priority": 1, + "install_allowed": True, + } + ] + }, + f, + ) + catalog = ExtensionCatalog(project_dir) # Create mock catalog @@ -1092,10 +1149,29 @@ class TestExtensionCatalog: def test_get_extension_info(self, temp_dir): """Test getting specific extension info.""" + import yaml as yaml_module + project_dir = temp_dir / "project" project_dir.mkdir() (project_dir / ".specify").mkdir() + # Use a single-catalog config so community extensions don't interfere + config_path = project_dir / ".specify" / "extension-catalogs.yml" + with open(config_path, "w") as f: + yaml_module.dump( + { + "catalogs": [ + { + "name": "test-catalog", + "url": ExtensionCatalog.DEFAULT_CATALOG_URL, + "priority": 1, + "install_allowed": True, + } + ] + }, + f, + ) + catalog = ExtensionCatalog(project_dir) # Create mock catalog @@ -1214,7 +1290,7 @@ class TestCatalogStack: # --- get_active_catalogs --- def test_default_stack(self, temp_dir): - """Default stack includes org-approved and community catalogs.""" + """Default stack includes default and community catalogs.""" project_dir = self._make_project(temp_dir) catalog = ExtensionCatalog(project_dir) @@ -1222,7 +1298,7 @@ class TestCatalogStack: assert len(entries) == 2 assert entries[0].url == ExtensionCatalog.DEFAULT_CATALOG_URL - assert entries[0].name == "org-approved" + assert entries[0].name == "default" assert entries[0].priority == 1 assert entries[0].install_allowed is True assert entries[1].url == ExtensionCatalog.COMMUNITY_CATALOG_URL @@ -1372,8 +1448,6 @@ class TestCatalogStack: def test_merge_conflict_higher_priority_wins(self, temp_dir): """When same extension id is in two catalogs, higher priority wins.""" - import yaml as yaml_module - project_dir = self._make_project(temp_dir) # Write project config with two catalogs