"""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'') 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()