R-805: the persistence gate names an empty bind (reported, not judged)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
## 2026-10-06 (night, held) — the persistence gate names an empty bind (R-805)
|
||||
|
||||
**What runs on a box changed:** nothing. Test tools only. (Held with the night's other catalog commits on `night-held-2026-10-06`.)
|
||||
|
||||
- `scripts/check-volume-persistence.py` `classify`: an EMPTY bind after the exercise is now a NOTE in the reasons („bind … is EMPTY after the exercise — not judged … (R-805)"); the verdict does not change. **Decided by CC unattended — operator may reverse:** an empty bind is reported, not UNDETERMINED like an empty declared volume (R-788), because a Felhom bind is mostly the household's own folder (media, documents), empty on a fresh install — komga, paperless-ngx, radarr and sonarr would otherwise never pass. Grimmory's empty `/app/data` read CLEAN with no word; now it is named.
|
||||
- Tests: TestEmptyBind (3: named + CLEAN; a written bind carries no note; a non-directory source such as docker.sock, `files: -1`, is not called empty). Red-proved.
|
||||
|
||||
## 2026-10-06 (night, held) — homepage lists the box's own name; glance's public page measured (R-782)
|
||||
|
||||
**What runs on a box changed:** homepage's environment. **Held on branch `night-held-2026-10-06`**, not `main` tonight.
|
||||
|
||||
@@ -351,6 +351,16 @@ def classify(probe: dict):
|
||||
# test_empty_declared_volume_is_undetermined_even_when_another_mount_has_data.
|
||||
empty_vols.append(f"{nm}: declared volume {m['target']} is EMPTY after the exercise — the app was "
|
||||
f"not shown to write where the template preserves it")
|
||||
if m["class"] == "bind" and m.get("files", 0) == 0:
|
||||
# R-805 (2026-10-06): an EMPTY bind is REPORTED, never judged. Unlike a declared volume (R-788 above),
|
||||
# a Felhom bind is mostly the household's own folder on its drive — a media library, a documents
|
||||
# inbox — which is legitimately empty on a fresh install (komga, paperless-ngx, radarr, sonarr), so
|
||||
# calling it UNDETERMINED would withhold every such app for ever. But it must not be SILENT either:
|
||||
# Grimmory's /app/data bind held 0 files and the app read CLEAN with no word about it. The note rides
|
||||
# the verdict line and probe.json. Pinned by TestEmptyBind.
|
||||
notes.append(f"{nm}: bind {m['target']} is EMPTY after the exercise — not judged (a household "
|
||||
f"folder may be empty on a fresh install); if this app keeps STATE there, the gate "
|
||||
f"did not see it written (R-805)")
|
||||
for d in c.get("diff_data_dirs") or []:
|
||||
wrote_anything = True
|
||||
broken.append(f"{nm}: DATA in the writable layer at {d['dir']} "
|
||||
|
||||
@@ -567,5 +567,35 @@ class TestEmptyDeclaredVolume(unittest.TestCase):
|
||||
"diff_unresolved": []}]}
|
||||
self.assertEqual(cvp.classify(probe)[0], cvp.CLEAN)
|
||||
|
||||
class TestEmptyBind(unittest.TestCase):
|
||||
"""R-805: an EMPTY bind after the exercise is reported in the reasons, and does not change the verdict.
|
||||
|
||||
COMPANION RED-PROOF (observed 2026-10-06): delete the R-805 `notes.append` in classify() → this fails with
|
||||
"an empty bind must be named in the reasons". Restored."""
|
||||
|
||||
def _probe(self, bind_files):
|
||||
return {"containers": [{"name": "app", "status": "running", "health": "healthy", "uid": 1000, "gid": 1000,
|
||||
"mounts": [{"class": "bind", "target": "/app/data", "files": bind_files, "writable_by_app": "yes"},
|
||||
{"class": "named-declared", "target": "/state", "files": 3, "writable_by_app": "yes"}],
|
||||
"diff_data_dirs": [], "diff_token_dirs": [], "diff_other_dirs": [], "diff_benign_db_touches": [],
|
||||
"diff_unresolved": []}]}
|
||||
|
||||
def test_empty_bind_is_named_and_the_verdict_stays_clean(self):
|
||||
st, why = cvp.classify(self._probe(0))
|
||||
self.assertEqual(st, cvp.CLEAN, why)
|
||||
self.assertTrue(any("/app/data" in w and "EMPTY" in w and "R-805" in w for w in why),
|
||||
f"an empty bind must be named in the reasons; got {why}")
|
||||
|
||||
def test_written_bind_carries_no_note(self):
|
||||
st, why = cvp.classify(self._probe(2))
|
||||
self.assertEqual(st, cvp.CLEAN, why)
|
||||
self.assertFalse(any("R-805" in w for w in why), why)
|
||||
|
||||
def test_unreadable_bind_source_is_not_called_empty(self):
|
||||
# _walk returns -1 for a bind whose source is not a directory (homepage's docker.sock): not "empty".
|
||||
st, why = cvp.classify(self._probe(-1))
|
||||
self.assertFalse(any("R-805" in w for w in why), why)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
Reference in New Issue
Block a user