Protect digest-pinned packages and refuse partial-inventory pruning
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e241-8285-7a63-8c0c-51c9cb824dc3
This commit is contained in:
parent
3f834b0556
commit
743def17be
5 changed files with 174 additions and 11 deletions
45
tests/test_digest_retention.py
Normal file
45
tests/test_digest_retention.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue