Skip to content

Commit 0eea07d

Browse files
author
Markus
committed
fix(bundler): fall back to packaged catalog snapshot
Assisted-by: GitHub Copilot (model: gpt-5.6-luna, autonomous)
1 parent e76c02c commit 0eea07d

3 files changed

Lines changed: 71 additions & 3 deletions

File tree

‎src/specify_cli/bundler/services/adapters.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,16 @@ def fetch(source: CatalogSource) -> dict:
143143
if repository_url is None:
144144
raise BundlerError(f"Unknown built-in catalog '{url}'.")
145145
if allow_network:
146-
return _http_get_json(source.id, repository_url)
146+
try:
147+
return _http_get_json(source.id, repository_url)
148+
except BundlerError as exc:
149+
# Built-in catalogs remain usable when the repository is
150+
# temporarily unavailable; the packaged snapshot is the
151+
# authoritative offline fallback.
152+
try:
153+
return _load_packaged_catalog(_BUILTIN_PACKAGED_SNAPSHOTS[url])
154+
except BundlerError as snapshot_exc:
155+
raise snapshot_exc from exc
147156
return _load_packaged_catalog(_BUILTIN_PACKAGED_SNAPSHOTS[url])
148157

149158
if scheme == "file":
@@ -204,7 +213,7 @@ def _validate_redirect(_old_url: str, new_url: str) -> None:
204213
).decode("utf-8")
205214
except BundlerError:
206215
raise
207-
except Exception as exc: # noqa: BLE001
216+
except Exception as exc:
208217
raise BundlerError(f"Failed to fetch catalog from {url}: {exc}") from exc
209218
return loads_json(raw, origin=final_url)
210219

‎tests/integration/test_bundler_offline.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,36 @@ def test_builtin_default_catalog_resolves_first_party_bundles_offline():
4141
assert resolved.install_allowed is True
4242

4343

44+
def test_builtin_catalog_failure_does_not_block_lower_priority_source(monkeypatch):
45+
def fail_http_get_json(source_id, url):
46+
raise BundlerError("repository unavailable")
47+
48+
monkeypatch.setattr(
49+
"specify_cli.bundler.services.adapters._http_get_json", fail_http_get_json
50+
)
51+
monkeypatch.setattr(
52+
"specify_cli.bundler.services.adapters._load_packaged_catalog",
53+
lambda filename: {"schema_version": "1.0", "bundles": {}},
54+
)
55+
56+
project = _src("project", "https://example.com/catalog.json", priority=10)
57+
fetcher = make_catalog_fetcher(allow_network=True)
58+
59+
def fetch_project(source):
60+
if source.id == "project":
61+
return {
62+
"schema_version": "1.0",
63+
"bundles": {"company": catalog_entry_dict("company")},
64+
}
65+
return fetcher(source)
66+
67+
stack = CatalogStack(
68+
[_src("default", "builtin://default"), project], fetch_project
69+
)
70+
71+
assert stack.resolve("company").source.id == "project"
72+
73+
4474
def test_builtin_community_catalog_resolves_from_packaged_snapshot_offline():
4575
fetcher = make_catalog_fetcher(allow_network=False)
4676
source = _src(

‎tests/unit/test_bundler_adapters.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Unit tests for catalog-fetch adapters (auth + redirect safety)."""
22
from __future__ import annotations
33

4+
from typing import Self
5+
46
import pytest
57

68
from specify_cli.bundler import BundlerError
@@ -23,7 +25,7 @@ def __init__(self, body: bytes, final_url: str) -> None:
2325
self._offset = 0
2426
self._final_url = final_url
2527

26-
def __enter__(self) -> "_FakeResponse":
28+
def __enter__(self) -> Self:
2729
return self
2830

2931
def __exit__(self, *exc) -> bool:
@@ -136,6 +138,33 @@ def fake_http_get_json(source_id, url):
136138
}
137139

138140

141+
def test_builtin_default_catalog_falls_back_to_core_pack_on_fetch_error(
142+
monkeypatch, tmp_path
143+
):
144+
catalog_path = tmp_path / "bundles" / "catalog.json"
145+
catalog_path.parent.mkdir()
146+
catalog_path.write_text(
147+
'{"schema_version":"1.0","bundles":{"packaged":{'
148+
'"id":"packaged","name":"Packaged","version":"1.0.0",'
149+
'"role":"developer","description":"Packaged catalog entry.",'
150+
'"author":"Spec Kit","license":"MIT","download_url":"",'
151+
'"requires":{"speckit_version":">=0.1.0"},'
152+
'"provides":{},"verified":false}}}',
153+
encoding="utf-8",
154+
)
155+
monkeypatch.setattr(adapters, "_locate_core_pack", lambda: tmp_path)
156+
157+
def fail_http_get_json(source_id, url):
158+
raise BundlerError("repository unavailable")
159+
160+
monkeypatch.setattr(adapters, "_http_get_json", fail_http_get_json)
161+
162+
fetcher = adapters.make_catalog_fetcher(allow_network=True)
163+
result = fetcher(_source("builtin://default"))
164+
165+
assert "packaged" in result["bundles"]
166+
167+
139168
def test_builtin_default_catalog_uses_core_pack_snapshot_offline(monkeypatch, tmp_path):
140169
catalog_path = tmp_path / "bundles" / "catalog.json"
141170
catalog_path.parent.mkdir()

0 commit comments

Comments
 (0)