From 1e138337cf2a731130f53027566797854b3b6239 Mon Sep 17 00:00:00 2001 From: Roman Kharitonov Date: Sat, 14 Feb 2026 12:07:14 +1000 Subject: [PATCH 1/3] Gracefully handle missing addon directories (closes #47) Check if addon directory exists before calling signature.validate to avoid ValueError crash when user renames addon folders. Co-Authored-By: Claude Opus 4.6 --- src/snapjaw.py | 5 ++++- tests/test_snapjaw_states.py | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/snapjaw.py b/src/snapjaw.py index 5c6823b..47b14dd 100644 --- a/src/snapjaw.py +++ b/src/snapjaw.py @@ -358,12 +358,15 @@ def get_addon_states(config: Config, addons_dir: str) -> list[AddonState]: processed += 1 print(f"{processed}/{total_addons}", end="\r") comment = None + addon_dir = os.path.join(addons_dir, addon.name) if state.error is not None: status = AddonStatus.Error comment = state.error + elif not os.path.isdir(addon_dir): + status = AddonStatus.Missing elif state.head_commit_hex is None: status = AddonStatus.Unknown - elif addon.checksum is None or not signature.validate(os.path.join(addons_dir, addon.name), addon.checksum): + elif addon.checksum is None or not signature.validate(addon_dir, addon.checksum): status = AddonStatus.Modified elif state.head_commit_hex == addon.commit: status = AddonStatus.UpToDate diff --git a/tests/test_snapjaw_states.py b/tests/test_snapjaw_states.py index 1a1996b..f8366b0 100644 --- a/tests/test_snapjaw_states.py +++ b/tests/test_snapjaw_states.py @@ -107,6 +107,9 @@ def test_error(self, tmp_path, monkeypatch, make_addon, capsys): def test_unknown(self, tmp_path, monkeypatch, make_addon, capsys): """No commit and no error sets Unknown status.""" + addon_dir = tmp_path / "TestAddon" + addon_dir.mkdir() + addon = make_addon() config = Config(addons_by_key={"testaddon": addon}) @@ -154,6 +157,29 @@ def test_missing(self, tmp_path, monkeypatch, make_addon, capsys): assert states[0].addon == "MyMissingAddon" capsys.readouterr() + def test_missing_directory_with_remote_state(self, tmp_path, monkeypatch, make_addon, capsys): + """Addon in config with valid remote state but missing directory should be Missing, not crash. + + Reproduces https://github.com/.../issues/47: when an addon directory is renamed + (e.g. adding '---' suffix), signature.validate raises ValueError instead of + gracefully reporting the addon as missing. + """ + # Addon is in config with a checksum, but its directory does NOT exist on disk + addon = make_addon(checksum="sig|2") + config = Config(addons_by_key={"testaddon": addon}) + + # Remote returns a valid state (so the code enters the signature.validate branch) + monkeypatch.setattr( + "snapjaw.mygit.fetch_states", + lambda reqs: iter([RemoteState("https://github.com/test/test.git", "master", "abc123", None)]), + ) + # Do NOT mock signature.validate — use the real one to reproduce the ValueError + + states = get_addon_states(config, str(tmp_path)) + assert len(states) == 1 + assert states[0].status == AddonStatus.Missing + capsys.readouterr() + def test_multiple_addons_different_statuses(self, tmp_path, monkeypatch, make_addon, capsys): """Multiple addons with different statuses are correctly detected.""" # Create addon directories with content From 19a626d29f8a66229226b5da1654a637b5c4a451 Mon Sep 17 00:00:00 2001 From: Roman Kharitonov Date: Sat, 14 Feb 2026 12:09:17 +1000 Subject: [PATCH 2/3] Replace deprecated codecov/test-results-action with codecov-action Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 272a951..5eaaadc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,9 +48,11 @@ jobs: - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + report_type: test_results + files: junit.xml build: needs: check From 58e9554fb94d3b63b15bc4a2605c1756ac879841 Mon Sep 17 00:00:00 2001 From: Roman Kharitonov Date: Sat, 14 Feb 2026 12:15:22 +1000 Subject: [PATCH 3/3] Treat warnings as errors, fix pygit2 ls_remotes deprecation Replace deprecated Remote.ls_remotes() with Remote.list_heads() and update ref access from dict subscript to attribute access (RemoteHead). Configure pytest filterwarnings = ["error"] to catch future deprecations. Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 1 + src/mygit.py | 10 +++++----- tests/conftest.py | 4 ++-- tests/test_mygit.py | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f2ba209..3a2de18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ markers = [ "slow: mark test as slow", ] addopts = "-m 'not integration'" +filterwarnings = ["error"] [dependency-groups] dev = [ diff --git a/src/mygit.py b/src/mygit.py index 3903713..3c2a782 100644 --- a/src/mygit.py +++ b/src/mygit.py @@ -111,7 +111,7 @@ class RemoteState: @dataclass class _RemoteLsResult: remote: pygit2.Remote - refs: list[dict] + refs: list error: str | None @@ -127,7 +127,7 @@ def fetch_states(requests: list[RemoteStateRequest]) -> Iterator[RemoteState]: def ls(remote: pygit2.Remote) -> _RemoteLsResult: try: - refs = remote.ls_remotes() + refs = remote.list_heads() error = None except pygit2.GitError as exception: refs = [] @@ -148,9 +148,9 @@ def ls(remote: pygit2.Remote) -> _RemoteLsResult: else: branch_ref = f"refs/heads/{branch}" for ref in ls_result.refs: - is_head = ref["name"] == "HEAD" and ref["symref_target"] == branch_ref - if is_head or ref["name"] == branch_ref: - yield RemoteState(url, branch, str(ref["oid"]), None) + is_head = ref.name == "HEAD" and ref.symref_target == branch_ref + if is_head or ref.name == branch_ref: + yield RemoteState(url, branch, str(ref.oid), None) break diff --git a/tests/conftest.py b/tests/conftest.py index d528f8b..94cf68f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,9 +128,9 @@ def _mock_remote(name, url, refs=None, error=None): remote.name = name remote.url = url if error: - remote.ls_remotes.side_effect = pygit2.GitError(error) + remote.list_heads.side_effect = pygit2.GitError(error) else: - remote.ls_remotes.return_value = refs or [] + remote.list_heads.return_value = refs or [] return remote return _mock_remote diff --git a/tests/test_mygit.py b/tests/test_mygit.py index 2b8802d..df3a2a2 100644 --- a/tests/test_mygit.py +++ b/tests/test_mygit.py @@ -75,7 +75,7 @@ def test_success_returns_commit_hash(self, mock_remote, mock_pygit2_repo, fetch_ remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "refs/heads/master", "symref_target": "", "oid": "deadbeef"}], + refs=[SimpleNamespace(name="refs/heads/master", symref_target="", oid="deadbeef")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote])) @@ -92,7 +92,7 @@ def test_head_symref_resolves_branch(self, mock_remote, mock_pygit2_repo, fetch_ remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "HEAD", "symref_target": "refs/heads/main", "oid": "cafebabe"}], + refs=[SimpleNamespace(name="HEAD", symref_target="refs/heads/main", oid="cafebabe")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote])) @@ -137,7 +137,7 @@ def test_branch_not_found_in_refs(self, mock_remote, mock_pygit2_repo, fetch_sta remote = mock_remote( "abc123", "https://github.com/test/repo.git", - refs=[{"name": "refs/heads/other-branch", "symref_target": "", "oid": "deadbeef"}], + refs=[SimpleNamespace(name="refs/heads/other-branch", symref_target="", oid="deadbeef")], ) mock_pygit2_repo.remotes.__iter__ = MagicMock(return_value=iter([remote]))