mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-18 12:59:12 +00:00
fix(store): do not pull what the gate cannot un-pull
Review of the gate found a data-loss path it had introduced, plus two smaller scope errors. All three from CodeRabbit on #508. **The stash failure was load-bearing and was not treated as one.** update_plugin stashes local changes before pulling; when that stash failed or timed out it logged a warning and pulled anyway. That was harmless while nothing ever undid a pull. It is not harmless now: the gate's rollback is `git reset --hard`, which discards uncommitted tracked edits -- exactly the edits the stash existed to protect. A pull does not refuse on a dirty tree as long as the incoming commit touches other files, so the sequence completed silently: pull succeeds, gate refuses, reset takes the user's work with it. update_plugin now returns before pulling unless the tree was already clean or was successfully stashed. Refusing costs an update in a case that had already gone wrong; the alternative costs data. That also makes `--hard` safe by construction in _gate_pulled_commit, and its comment now says so rather than observing it in passing. Pinned by test_a_failed_stash_stops_the_update_before_pulling, which writes a local edit, forces the stash to fail, and asserts both that HEAD did not move and that the edit is still on disk. Verified it bites: with the new guard removed the file comes back as `class P: pass`, the edit gone. **_HAS_GIT could take the module down instead of skipping it.** With no git on PATH, subprocess.run raises FileNotFoundError, and this runs at import time -- before skipif can act, so the whole file errors rather than skipping. Now catches OSError. **The doc overclaimed the gate's reach.** It said the floor is enforced on "every route that installs or updates" while the same passage notes install_from_url is ungated. Both spots now scope the claim to registry-managed installs and the two supported update paths, and name the sideload exception. Full suite 3720 passed, 6 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
This commit is contained in:
co-authored by
Claude Opus 5
parent
217aac780b
commit
45d912142c
@@ -221,8 +221,18 @@ class TestInstallGate:
|
||||
# The git-pull update path — the one route that does not re-download
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
_HAS_GIT = subprocess.run(
|
||||
['git', '--version'], capture_output=True).returncode == 0
|
||||
def _git_available() -> bool:
|
||||
# OSError, not just a non-zero exit: with no git on PATH subprocess raises
|
||||
# FileNotFoundError, and this runs at import time -- before skipif can act,
|
||||
# so the whole module would error out instead of skipping.
|
||||
try:
|
||||
return subprocess.run(
|
||||
['git', '--version'], capture_output=True).returncode == 0
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
_HAS_GIT = _git_available()
|
||||
|
||||
|
||||
def _git(*args, cwd):
|
||||
@@ -361,6 +371,42 @@ class TestGitPullGate:
|
||||
assert mgr.update_plugin('gitplug') is True
|
||||
assert self._head(work) != before
|
||||
|
||||
def test_a_failed_stash_stops_the_update_before_pulling(
|
||||
self, checkout, monkeypatch):
|
||||
"""Uncommitted work is not collateral for the gate.
|
||||
|
||||
The gate's only rollback is `git reset --hard`, which discards
|
||||
uncommitted tracked edits. update_plugin stashes them first -- but when
|
||||
that stash fails it used to pull anyway, and a pull touching different
|
||||
files succeeds on a dirty tree. Refuse, refuse the rollback's rollback,
|
||||
and the user's edits go with it.
|
||||
"""
|
||||
mgr, seed, work = checkout
|
||||
before = self._head(work)
|
||||
(work / 'manager.py').write_text(
|
||||
"class P:\n MINE = 'do not lose this'\n", encoding='utf-8')
|
||||
self._push(seed, json.dumps({
|
||||
"id": "gitplug", "name": "Git Plug", "class_name": "P",
|
||||
"display_modes": ["a"], "min_ledmatrix_version": "9.9.9",
|
||||
}))
|
||||
|
||||
real_run = subprocess.run
|
||||
|
||||
def fail_stash(cmd, *a, **k):
|
||||
if isinstance(cmd, list) and 'stash' in cmd:
|
||||
return subprocess.CompletedProcess(cmd, 1, '', 'stash boom')
|
||||
return real_run(cmd, *a, **k)
|
||||
|
||||
import src
|
||||
monkeypatch.setattr(src, '__version__', '3.2.0')
|
||||
monkeypatch.setattr(
|
||||
'src.plugin_system.store_manager.subprocess.run', fail_stash)
|
||||
assert mgr.update_plugin('gitplug') is False
|
||||
|
||||
assert self._head(work) == before, "must not pull what it cannot undo"
|
||||
assert 'do not lose this' in (work / 'manager.py').read_text(), (
|
||||
"the local edit the stash failed to save must still be there")
|
||||
|
||||
def test_rollback_reports_the_recovery_command_when_git_fails(
|
||||
self, checkout, monkeypatch):
|
||||
"""If the reset itself fails the user is left on a version that cannot
|
||||
|
||||
Reference in New Issue
Block a user