Protect digest-pinned packages and refuse partial-inventory pruning
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
This commit is contained in:
codex 2026-09-27 16:00:51 +02:00
parent 3f834b0556
commit 743def17be
5 changed files with 174 additions and 11 deletions

View file

@ -0,0 +1,45 @@
from unittest.mock import patch
from scripts import forgejo_package_prune as p
IMAGE = 'forgejo.coulomb.social/coulomb/activity-core'
def test_digest_and_tag_digest_protect_whole_package(tmp_path):
for suffix in ['@sha256:' + 'a'*64, ':old@sha256:' + 'b'*64]:
f = tmp_path / 'images'; f.write_text(IMAGE + suffix + '\n')
protected, notes = p.collect_live_images_from_files([f])
assert notes == []
assert protected == {('container', 'activity-core', '*')}
versions = [{'name': name, 'version': version, 'created_at': date}
for name in ['activity-core', 'unrelated']
for version, date in [('new', '2026-09-27'), ('old', '2025-01-01')]]
with patch.object(p, 'list_packages', return_value=versions):
plans, errors = p.build_delete_plans(base_url='', token='', owner='coulomb',
package_types=['container'], max_versions=1, protected=protected)
assert not errors
assert [(x.name, x.protected, x.reason) for x in plans] == [
('activity-core', True, 'protected_digest_package'),
('unrelated', False, 'beyond_retention_depth')]
def test_bad_digest_refuses_apply_before_credentials(tmp_path):
f = tmp_path / 'images'; f.write_text(IMAGE + '@sha256:bad\n')
with patch.object(p, 'load_token') as auth, patch.object(p, 'delete_version') as delete:
assert p.main(['--apply', '--live-images-file', str(f)]) == 2
auth.assert_not_called(); delete.assert_not_called()
def test_incomplete_inventory_never_deletes():
for errors, notes in [(['list failed'], []), ([], ['cluster unavailable'])]:
with patch.object(p, 'load_token', return_value='fixture'), \
patch.object(p, 'collect_protected_versions', return_value=set()), \
patch.object(p, 'collect_live_cluster_versions', return_value=(set(), notes)), \
patch.object(p, 'build_delete_plans', return_value=([p.DeletePlan('container','x','old','',False,'old')], errors)), \
patch.object(p, 'delete_version') as delete:
assert p.main(['--apply']) == 2
delete.assert_not_called()
def test_cluster_digest_uses_same_protection():
import subprocess
with patch.object(p.shutil, 'which', return_value='/bin/kubectl'), \
patch.object(p.subprocess, 'run', return_value=subprocess.CompletedProcess([],0,IMAGE+'@sha256:'+'a'*64+'\n','')):
protected, notes = p.collect_live_cluster_versions()
assert not notes
assert ('container','activity-core','*') in protected