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:
2026-10-06 20:53:38 +02:00
parent 093e5ed231
commit 816c5577fb
3 changed files with 47 additions and 0 deletions
+30
View File
@@ -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)