Persistence gate: empty declared volume = UNDETERMINED (R-788), the app's own fixture seed as the exercise; re-sweep verdicts (CLEAN 40 / UNDETERMINED 18 / BROKEN 0); papra 256M -> 768M (R-803); records 2.1, EXISTING-APPS-GAPS regenerated
gates / gates (push) Successful in 2s

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-02 16:54:29 +02:00
parent febf58c7e6
commit 917a779cca
19 changed files with 224 additions and 77 deletions
+31 -2
View File
@@ -159,7 +159,12 @@ class TestClassify(unittest.TestCase):
c = _ctr("crafty", 0, 0, mounts=[_mount("/crafty/app/config", files=16),
_mount("/crafty/backups", files=0)])
c["diff_other_dirs"] = [{"dir": "/crafty/app/classes", "added": ["x"]}]
self.assertEqual(cvp.classify({"app": "crafty", "containers": [c]})[0], cvp.CLEAN)
status, why = cvp.classify({"app": "crafty", "containers": [c]})
# R-788 (2026-10-02): the empty /crafty/backups is no longer passed over — UNDETERMINED names it; the
# structural check still stays silent (that is what this test is about).
self.assertEqual(status, cvp.UNDETERMINED)
self.assertNotIn("NOTHING this app wrote landed", " ".join(why))
self.assertIn("/crafty/backups is EMPTY", " ".join(why))
def test_a_SIBLING_container_holding_the_state_silences_it(self):
"""The measured docmost / immich / claper shape, and the reason the question is asked per
@@ -171,7 +176,10 @@ class TestClassify(unittest.TestCase):
db = _ctr("docmost-postgres", 0, 0,
mounts=[_mount("/var/lib/postgresql/data", files=1540)])
status, why = cvp.classify({"app": "docmost", "containers": [app, db]})
self.assertEqual(status, cvp.CLEAN)
# R-788: the uploads volume was never shown to receive an upload — UNDETERMINED, named; the
# STRUCTURAL check (this test's subject) must still stay silent.
self.assertEqual(status, cvp.UNDETERMINED)
self.assertIn("/app/data/storage is EMPTY", " ".join(why))
self.assertNotIn("NOTHING this app wrote landed", " ".join(why))
self.assertIn("benign when a sibling container holds the state", " ".join(why),
"the per-container observation must still be reported, not dropped")
@@ -479,5 +487,26 @@ class TestRoutedPorts(unittest.TestCase):
def test_no_routed_port(self):
self.assertEqual(cvp.routed_ports({"services": {"db": {"labels": {"x": "y"}}}}), [])
class TestEmptyDeclaredVolume(unittest.TestCase):
def test_empty_declared_volume_is_undetermined_even_when_another_mount_has_data(self):
"""R-788: one mount holds data, the DECLARED volume is empty — the old rule called this CLEAN."""
probe = {"containers": [{"name": "app", "status": "running", "health": "healthy", "uid": 1000, "gid": 1000,
"mounts": [{"class": "bind", "target": "/downloads", "files": 1, "writable_by_app": "yes"},
{"class": "named-declared", "target": "/state", "files": 0, "writable_by_app": "yes"}],
"diff_data_dirs": [], "diff_token_dirs": [], "diff_other_dirs": [], "diff_benign_db_touches": [],
"diff_unresolved": []}]}
st, why = cvp.classify(probe)
self.assertEqual(st, cvp.UNDETERMINED, why)
self.assertTrue(any("/state" in w and "EMPTY" in w for w in why), why)
def test_both_written_is_clean(self):
probe = {"containers": [{"name": "app", "status": "running", "health": "healthy", "uid": 1000, "gid": 1000,
"mounts": [{"class": "bind", "target": "/downloads", "files": 1, "writable_by_app": "yes"},
{"class": "named-declared", "target": "/state", "files": 2, "writable_by_app": "yes"}],
"diff_data_dirs": [], "diff_token_dirs": [], "diff_other_dirs": [], "diff_benign_db_touches": [],
"diff_unresolved": []}]}
self.assertEqual(cvp.classify(probe)[0], cvp.CLEAN)
if __name__ == "__main__":
unittest.main(verbosity=2)