Files
mini-openclaw-v2/backend/test_skill_packages.py
T

57 lines
4.3 KiB
Python

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