Files
Sanctification/tools/card-production/tests/test_card_workspace.py

121 lines
9.0 KiB
Python

"""Filesystem lifecycle tests use an isolated miniature card workspace."""
from pathlib import Path
import hashlib, json, os, sys, tempfile, unittest
from unittest.mock import patch
from PIL import Image
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
import card_workspace as workspace
class CardWorkspaceTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.repo=Path(self.temp.name)
self.repo_patch=patch.object(workspace,'REPO',self.repo);self.repo_patch.start()
self.size_patch=patch.object(workspace,'RESOLUTIONS',{'low':(5,7),'med':(10,14),'high':(20,28)});self.size_patch.start()
self.revision=workspace.scaffold('BP-999','Test card','Common','v01')
def tearDown(self):
self.size_patch.stop();self.repo_patch.stop();self.temp.cleanup()
def assemble(self):
for label,size in workspace.RESOLUTIONS.items():
for printing in workspace.PRINTINGS:
directory=self.revision/label/printing
Image.new('RGBA',size,(40,60,80,255)).save(directory/'card.png')
Image.new('RGBA',size,(127,127,127,255)).save(directory/'finish-mask.png')
if printing in ('normal','borderless'):Image.new('RGBA',size,(0,0,0,255)).save(directory/'text-mask.png')
source=self.repo/'fitted-art.png';Image.new('RGB',(20,28),'blue').save(source)
link=self.revision/'source/art-master.png';link.symlink_to(os.path.relpath(source,link.parent))
editable=self.revision/'high/normal/card.svg'
editable.write_text(f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="{os.path.relpath(source,editable.parent)}"/></svg>')
alias=self.repo/'spikes/proof/card.png';alias.parent.mkdir(parents=True,exist_ok=True);alias.symlink_to(os.path.relpath(self.revision/'med/normal/card.png',alias.parent))
manifest=workspace.read(self.revision/'manifest.json');manifest['stage']='assembly-review';manifest['compatibilityAliases']=[{'alias':'spikes/proof/card.png','target':'med/normal/card.png'}];workspace.write(self.revision/'manifest.json',manifest)
workspace.refresh(self.revision)
return alias
def test_scaffold_tracks_art_review_without_derived_exports(self):
self.assertEqual(workspace.validate(self.revision)['stage'],'art-review')
self.assertEqual(workspace.resolve_revision('BP-999','v01'),self.revision)
self.assertEqual(len(list(self.revision.glob('*/normal'))),3)
def test_approval_preserves_exports_sources_and_aliases_then_freezes(self):
alias=self.assemble();before={str(p.relative_to(self.revision)):workspace.digest(p) for p in self.revision.rglob('*.png')}
target=workspace.approve('BP-999','v01','Test fixture approval only')
self.assertFalse(self.revision.exists())
self.assertEqual(target,self.repo/'artifacts/cards/BP-999-test-card')
self.assertEqual(workspace.validate(target)['stage'],'approved')
self.assertEqual(alias.resolve(),target/'med/normal/card.png')
for relative,digest in before.items():self.assertEqual(workspace.digest(target/relative),digest)
with self.assertRaisesRegex(ValueError,'frozen'):workspace.refresh(target)
def test_flat_acceptance_keeps_revision_identity_and_content(self):
self.assemble()
content=workspace.read(self.revision/'card.json');content['verse']='A retained verse';workspace.write(self.revision/'card.json',content);workspace.refresh(self.revision)
target=workspace.approve('BP-999','v01','Test fixture approval only')
self.assertEqual(workspace.resolve_revision('BP-999','v01'),target)
self.assertEqual(workspace.read(target/'card.json')['verse'],'A retained verse')
self.assertEqual(workspace.read(target/'manifest.json')['revision'],'v01')
with self.assertRaises(ValueError):workspace.resolve_revision('BP-999','v02')
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
def test_new_acceptance_compacts_previous_and_internalizes_linked_art(self):
alias=self.assemble()
accepted=workspace.approve('BP-999','v01','First fixture approval')
low_before=workspace.digest(accepted/'low/normal/card.png')
linked_before=workspace.digest(accepted/'high/boundless/card.png')
self.revision=workspace.scaffold('BP-999','Test card','Common','v02')
alias.unlink();self.assemble()
# This revision deliberately depends on the earlier accepted full-art export.
link=self.revision/'source/art-master.png';link.unlink();link.symlink_to(os.path.relpath(accepted/'high/boundless/card.png',link.parent))
content=workspace.read(self.revision/'card.json');content['art']=str((accepted/'high/boundless/card.png').relative_to(self.repo));workspace.write(self.revision/'card.json',content);workspace.refresh(self.revision)
target=workspace.approve('BP-999','v02','Second fixture approval')
archive=self.repo/'in-progress/cards/BP-999-test-card/history/accepted-v01'
with self.assertRaises(ValueError):workspace.resolve_revision('BP-999','v01')
self.assertEqual(workspace.resolve_revision('BP-999','v02'),target)
self.assertEqual(workspace.digest(archive/'low/normal/card.png'),low_before)
self.assertFalse((archive/'med').exists());self.assertFalse((archive/'high').exists())
self.assertTrue((archive/'full-manifest.json').is_file());self.assertTrue((archive/'record.json').is_file())
self.assertFalse((target/'source/art-master.png').is_symlink())
self.assertEqual(workspace.digest(target/'source/art-master.png'),linked_before)
self.assertEqual(workspace.read(target/'card.json')['art'],'artifacts/cards/BP-999-test-card/source/art-master.png')
self.assertEqual(workspace.validate(target)['stage'],'approved')
self.assertEqual(alias.resolve(),target/'med/normal/card.png')
self.assertEqual(workspace.update_index()[0]['selectedRevision'],'v02')
def test_approval_compacts_superseded_working_revisions(self):
old=self.revision
Image.new('RGB',(20,28),'red').save(old/'source/art-master.png')
(old/'source/generation-prompt.txt').write_text('Retained prompt')
workspace.refresh(old)
self.revision=workspace.scaffold('BP-999','Test card','Common','v02');self.assemble()
workspace.approve('BP-999','v02','Second fixture approval')
history=self.repo/'in-progress/cards/BP-999-test-card/history/revisions/v01'
self.assertFalse(old.exists());self.assertTrue((history/'review/art-reference.png').is_file())
with Image.open(history/'review/art-reference.png') as reference:self.assertEqual(reference.size,(5,7))
self.assertFalse((history/'source/art-master.png').exists())
self.assertEqual((history/'source/generation-prompt.txt').read_text(),'Retained prompt')
self.assertTrue((history/'full-manifest.json').is_file())
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
def test_changed_or_invalid_exports_block_promotion(self):
self.assemble();Image.new('RGBA',(9,14),(40,60,80,255)).save(self.revision/'med/normal/card.png')
with self.assertRaises(ValueError):workspace.approve('BP-999','v01','Test fixture approval only')
self.assertTrue(self.revision.exists());self.assertFalse((self.repo/'artifacts/cards/BP-999-test-card').exists())
def test_catalogue_identity_survives_folder_title_rename(self):
base=self.revision.parents[1]
renamed=base.with_name('BP-999-renamed-title')
base.rename(renamed)
profile=workspace.read(renamed/'card.json')
profile.update({'title':'Renamed title','folderName':renamed.name})
workspace.write(renamed/'card.json',profile)
# The old index is deliberately stale; resolution must recover from profiles.
self.assertEqual(workspace.resolve_revision('BP-999','v01'),renamed/'revisions/v01')
self.assertEqual(workspace.update_index()[0]['cardId'],'BP-999')
self.revision=renamed/'revisions/v01'
def test_catalogue_id_is_required_and_duplicate_profiles_are_rejected(self):
with self.assertRaises(ValueError):workspace.scaffold('test-card','Test card','Common','v02')
base=self.repo/'in-progress/cards/BP-999-duplicate'
base.mkdir()
profile=workspace.read(self.revision.parents[1]/'card.json');profile['folderName']=base.name
workspace.write(base/'card.json',profile)
with self.assertRaisesRegex(ValueError,'Duplicate'):workspace.update_index()
def test_art_review_cannot_be_promoted_and_duplicate_revision_is_rejected(self):
with self.assertRaisesRegex(ValueError,'assembly-review'):workspace.approve('BP-999','v01','Test fixture approval only')
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
if __name__=='__main__':unittest.main()