"""AC-74: standard folder round trips, unsafe paths and immutable file integrity.""" import base64 import io import tempfile import unittest import zipfile from pathlib import Path from services import skill_packages as packages, assistant_store as store, component_registry from services.project_assistant import ProjectAssistantService from services.lease_case_library import LibraryError class SkillPackageTests(unittest.TestCase): def setUp(self): self.temp=tempfile.TemporaryDirectory(ignore_cleanup_errors=True) self.root=Path(self.temp.name); self.folder=self.root/'packages' self.service=ProjectAssistantService(self.root/'assistant.sqlite3') text='---\nname: cashflow-check\ndescription: 核对对公承租人现金流来源\n---\n\n# 现金流核查\n缺证留空。\n' self.files=[{'path':'cashflow-check/SKILL.md','content_base64':base64.b64encode(text.encode()).decode()}, {'path':'cashflow-check/references/checklist.md','content_base64':base64.b64encode('银行流水核验清单'.encode()).decode()}, {'path':'cashflow-check/scripts/never-run.py','content_base64':base64.b64encode(b'raise Exception("MUST NOT EXECUTE")').decode()}] def tearDown(self): self.temp.cleanup() def test_import_reopen_download_preserves_bytes(self): with self.service.connect() as db: project=store.get(db,'project','DEMO-A') card=packages.import_package(db,project,'admin-a',self.files,'financial.operating_cashflow',self.folder) self.assertEqual(card['origin'],'manual'); self.assertEqual(card['status'],'draft'); self.assertEqual(card['allowed_tools'],[]) with self.service.connect() as db: current=store.get(db,'component_draft',card['id']) content=packages.content(db,current,self.folder); self.assertEqual(len(content['files']),3) name,binary=packages.archive(db,current,self.folder) with zipfile.ZipFile(io.BytesIO(binary)) as z: self.assertEqual(z.read(name+'/SKILL.md'),base64.b64decode(self.files[0]['content_base64'])) self.assertIn(name+'/scripts/never-run.py',z.namelist()) self.assertTrue((self.folder/content['id']/name/'SKILL.md').is_file()) with self.assertRaises(LibraryError): packages.import_package(db,project,'admin-a',self.files,None,self.folder) def test_unsafe_paths_and_yaml_rejected(self): for path in ('/cashflow-check/SKILL.md','cashflow-check/../SKILL.md','C:/test/SKILL.md','cashflow-check/CON','cashflow-check//SKILL.md','cashflow-check/SKILL.md.'): with self.subTest(path=path),self.assertRaises(LibraryError): packages.validate_files([{**self.files[0],'path':path}]) for text in ('plain text','---\nname: wrong\ndescription: test\n---\nbody','---\nname: cashflow-check\ndescription: 123\n---\nbody','---\nname: cashflow-check\ndescription: !!python/object/apply:os.system [bad]\n---\nbody'): with self.subTest(text=text),self.assertRaises(LibraryError): packages.validate_files([{**self.files[0],'content_base64':base64.b64encode(text.encode()).decode()}]) def test_limits_and_duplicates(self): with self.assertRaises(LibraryError): packages.validate_files(self.files+[self.files[0]]) with self.assertRaises(LibraryError): packages.validate_files([]) with self.assertRaises(LibraryError): packages.validate_files([{**self.files[0],'content_base64':base64.b64encode(b'x'*(2*1024*1024+1)).decode()}]) def test_file_change_rejected(self): with self.service.connect() as db: card=packages.import_package(db,store.get(db,'project','DEMO-A'),'admin-a',self.files,None,self.folder) meta=card['package']; (self.folder/meta['id']/meta['name']/'SKILL.md').write_text('changed',encoding='utf-8') with self.assertRaises(LibraryError): packages.content(db,card,self.folder) def test_system_skill_is_standard_and_idempotent(self): with self.service.connect() as db: skill=component_registry.resolve(db,'corporate.cashflow'); first=packages.content(db,skill,self.folder) self.assertIn('name: corporate-cashflow',first['files'][0]['text']) self.assertEqual(first['id'],packages.content(db,skill,self.folder)['id']) if __name__=='__main__': unittest.main()