#!/usr/bin/env python3 """Packed archival verification without native execution or production writes.""" import json import shutil from pathlib import Path import sys import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1]/'tools')) import archive_packed as archiver import headless_game as runner import test_packed_core_game as fixtures class PackedArchiveTests(unittest.TestCase): def setUp(self): self.fixture=fixtures.PackedCoreTests();self.fixture.setUp() self.session=self.fixture.session;self.root=self.fixture.root/'archive-repository';self.root.mkdir() runner.prepare_packed(self.fixture.packed) command=runner.command_for(Path('/unused'),self.fixture.steam,Path('/compat'),self.fixture.profile,'core',self.fixture.packed) self.raw=runner.packed_snapshot(self.fixture.packed,command,self.fixture.profile) self.meta=json.loads(self.raw) self.native=self.fixture.profile/'logs/logs_2026-09-19_11-02-00';self.native.mkdir(parents=True) (self.native/'console.log').write_text(self.fixture.content()) self.report=runner.packed_seal(self.fixture.packed,self.raw,self.native) self.report.update(started_unix=self.meta['captured_unix']+0.001,stopped_unix=self.meta['captured_unix']+30,result='packed-core-started',session=str(self.native),project='core',cleanup_confirmed=True,runner_error=None,failure_observed_during_polling=False,executable=command[16],dedicated_server=False,control_only=False,packed_consumption_verified=True) # The command executable follows Proton/run; avoid an assumed path in fixtures. self.report['executable']=command[command.index('--')+3] self.startup={'ready':True,'failed':False,'packed_only':True} self.write_reports() def tearDown(self):self.fixture.tearDown() def write_reports(self): raw=archiver.encoded(self.report) (self.session/'packed-runner.json').write_bytes(raw);(self.native/'runner.json').write_bytes(raw) startup=archiver.encoded(self.startup) (self.session/'packed-startup-report.json').write_bytes(startup);(self.native/'game-startup-report.json').write_bytes(startup) (self.native/runner.PACKED_MANIFEST).write_bytes(self.raw) def update_metadata(self): self.raw=archiver.encoded(self.meta);(self.session/runner.PACKED_MANIFEST).write_bytes(self.raw) self.report['packed_input_provenance_sha256']=archiver.digest(self.raw);self.write_reports() def reseal_console(self,text): raw=text.encode();(self.native/'console.log').write_bytes(raw);self.report['console_sha256']=archiver.digest(raw);self.write_reports() def test_exact_artifacts_reports_and_hashes_retained_without_source_claim(self): dest,status=archiver.archive(self.session,'packed-test',self.root);self.assertEqual(status,0) self.assertEqual(runner.packed_inventory(dest/'package'),self.meta['artifacts']) for name in runner.PACKED_FILES:self.assertEqual((dest/'package'/name).read_bytes(),(self.fixture.packed['input']/name).read_bytes()) self.assertEqual((dest/runner.PACKED_MANIFEST).read_bytes(),self.raw) hashes=json.loads((dest/'archive-sha256.json').read_text()) for name,digest in hashes.items():self.assertEqual(archiver.digest((dest/name).read_bytes()),digest) self.assertFalse((dest/'source-sha256.json').exists());self.assertFalse((dest/'source-provenance.json').exists()) report=json.loads((dest/'report.json').read_text());self.assertTrue(report['packed_consumption_verified']);self.assertFalse(report['build_qualified']);self.assertFalse(report['release_qualified']) def test_valid_failed_run_archives_nonzero_and_retains_raw_failure(self): self.report.update(result='game-failed',packed_consumption_verified=False,failure_observed_during_polling=True) self.startup={'ready':False,'packed_input_violations':['historical alias parser refusal']};self.write_reports() dest,status=archiver.archive(self.session,'failed-run',self.root) self.assertEqual(status,1);self.assertIn('historical alias parser refusal',(dest/'raw-startup-report.json').read_text()) self.assertTrue(json.loads((dest/'report.json').read_text())['runner_failure_latched']) def test_corrected_ready_parser_cannot_erase_latched_run_failure(self): self.report.update(result='game-failed',packed_consumption_verified=False,failure_observed_during_polling=True);self.write_reports() files,report=archiver.inspect(self.session) self.assertFalse(report['packed_consumption_verified']);self.assertTrue(report['packed_loaded_paths_verified']) def test_native_raw_error_overrides_claimed_success_and_is_retained(self): self.reseal_console(self.fixture.content()+'00 SCRIPT (E): actual failure\n') dest,status=archiver.archive(self.session,'error-run',self.root);self.assertEqual(status,1) self.assertIn('actual failure',(dest/'console.log.txt').read_text()) def test_auxiliary_errors_fail_but_warnings_do_not(self): (self.native/'error.log').write_text('00 SCRIPT (W): warning\n') self.assertTrue(archiver.inspect(self.session)[1]['packed_consumption_verified']) (self.native/'error.log').write_text('00 SCRIPT (E): failure absent from console\n') files,report=archiver.inspect(self.session);self.assertFalse(report['packed_consumption_verified']);self.assertTrue(report['auxiliary_native_failure']);self.assertIn('error.log.txt',files) def test_forged_saved_ready_report_cannot_substitute_native_readiness(self): self.reseal_console(self.fixture.content().replace('local participant verified','not ready')) self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) def test_unchanged_flag_and_minimum_duration_required(self): self.report['packed_inputs_unchanged_after_run']=False;self.write_reports() self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) self.report['packed_inputs_unchanged_after_run']=True;self.report['stopped_unix']=self.report['started_unix']+14.9;self.write_reports() self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) def test_tampered_payload_console_or_copy_is_refused_before_destination(self): payload=self.fixture.packed['input']/'data.pak';original=payload.read_bytes();payload.write_bytes(b'changed') with self.assertRaisesRegex(ValueError,'package bytes differ'):archiver.archive(self.session,'tampered',self.root) self.assertFalse((self.root/'docs/evidence/tampered').exists());payload.write_bytes(original) (self.native/'console.log').write_text('changed') with self.assertRaisesRegex(ValueError,'console differs'):archiver.inspect(self.session) def test_root_native_report_disagreement_refused(self): (self.native/'runner.json').write_text('{}') with self.assertRaisesRegex(ValueError,'copies disagree'):archiver.inspect(self.session) def test_manifest_seal_mismatch_and_duplicate_keys_refused(self): (self.session/runner.PACKED_MANIFEST).write_bytes(self.raw+b' ') with self.assertRaisesRegex(ValueError,'binding differs'):archiver.inspect(self.session) (self.session/runner.PACKED_MANIFEST).write_text('{"schema_version":1,"schema_version":1}') with self.assertRaisesRegex(ValueError,'duplicate JSON key'):archiver.inspect(self.session) def test_unexpected_command_loose_search_or_server_flag_refused(self): self.meta['command'].extend(['-addonsDir','Z:/loose/successor/addons']);self.update_metadata() with self.assertRaisesRegex(ValueError,'Recorded command differs'):archiver.inspect(self.session) def test_recorded_native_session_escape_refused(self): self.report['session']='/tmp/foreign/logs_foo';self.write_reports() with self.assertRaisesRegex(ValueError,'escapes'):archiver.inspect(self.session) def test_runtime_or_profile_redirection_refused(self): self.meta['runtime_directory']='/tmp/foreign/core';self.update_metadata() with self.assertRaisesRegex(ValueError,'do not belong'):archiver.inspect(self.session) def test_cleanup_unknown_result_and_source_relabel_refused(self): original=dict(self.report) for key,value in [('cleanup_confirmed',False),('result','all good'),('source_provenance','launch_snapshot'),('build_qualified',True),('packed_only',False),('packed_inputs_unchanged_after_run',1)]: self.report=dict(original);self.report[key]=value;self.write_reports() with self.subTest(key=key),self.assertRaises(ValueError):archiver.inspect(self.session) def test_canonical_names_no_overwrite_and_symlink_destination(self): for name in ('../outside','nested/name','.','..','/absolute','bad name','_hidden'): with self.subTest(name=name),self.assertRaises(ValueError):archiver.archive(self.session,name,self.root) dest,status=archiver.archive(self.session,'once',self.root) with self.assertRaisesRegex(ValueError,'overwrite'):archiver.archive(self.session,'once',self.root) (self.root/'docs/evidence/redirect').symlink_to(dest,target_is_directory=True) with self.assertRaises(ValueError):archiver.archive(self.session,'redirect',self.root) def test_symlinked_evidence_refused(self): path=self.native/'error.log';path.symlink_to(self.native/'console.log') with self.assertRaisesRegex(ValueError,'symlinked'):archiver.inspect(self.session) def test_timestamp_and_missing_hash_validation(self): self.report['started_unix']=self.meta['captured_unix']-1;self.write_reports() with self.assertRaisesRegex(ValueError,'chronology'):archiver.inspect(self.session) class PackedChatArchiveTests(unittest.TestCase): # Reuse the existing Core fixture construction without inheriting/rerunning its # tests. Convert the private payload to the runner's actual schema-2 closure. write_reports=PackedArchiveTests.write_reports update_metadata=PackedArchiveTests.update_metadata reseal_console=PackedArchiveTests.reseal_console def setUp(self): base=PackedArchiveTests();base.setUp();self.__dict__.update(base.__dict__) runtime=self.session/'input/addons';candidate=self.fixture.root/'chat-candidate';candidate.mkdir() for project in archiver.ADDON_ORDER: folder=runtime/project;folder.mkdir(exist_ok=True) (folder/'addon.gproj').write_bytes((runner.ROOT/'addons'/project/'addon.gproj').read_bytes()) (folder/'data.pak').write_bytes(('fixture-pak-'+project).encode()) (folder/'resourceDatabase.rdb').write_bytes(('fixture-db-'+project).encode()) shutil.copytree(folder,candidate/project) self.packed={'project':'chat','input':runtime,'candidate':candidate,'session':self.session,'manifest':runner.packed_payload_inventory(runtime,'chat')} command=runner.command_for(Path('/unused'),self.fixture.steam,Path('/compat'),self.fixture.profile,'chat',self.packed) self.raw=runner.packed_snapshot(self.packed,command,self.fixture.profile);self.meta=json.loads(self.raw) (self.native/'console.log').write_text(self.content()) self.report=runner.packed_seal(self.packed,self.raw,self.native) self.report.update(started_unix=self.meta['captured_unix']+0.001,stopped_unix=self.meta['captured_unix']+30,result='packed-chat-started',session=str(self.native),project='chat',cleanup_confirmed=True,runner_error=None,failure_observed_during_polling=False,executable=command[command.index('--')+3],dedicated_server=False,control_only=False,packed_consumption_verified=True) self.startup={'ready':True,'failed':False,'packed_only':True};self.write_reports() def tearDown(self):self.fixture.tearDown() def content(self): folder=self.session/'input/addons' lines=[f"00 ENGINE : dir: '{folder}'", "00 ENGINE : dir: './addons'", "00 ENGINE : dir: '$profile:../addons/'", '00 ENGINE : Loaded addons:', "00 ENGINE : gproj: './addons/core/core.gproj' guid: '5614BBCCBB55ED1C'", "00 ENGINE : gproj: './addons/data/ArmaReforger.gproj' guid: '58D0FB3206B6F859'"] for project in archiver.ADDON_ORDER: name,guid,deps=runner.PACKED_PROJECTS[project] lines.append(f"00 ENGINE : gproj: '{folder/project}/addon.gproj' guid: '{guid}'") lines.extend(["00 ENGINE : FileSystem: Adding package './addons/core/' (pak count: 2) to filesystem under name core", "00 ENGINE : FileSystem: Adding package './addons/data/' (pak count: 14) to filesystem under name ArmaReforger"]) for i,project in enumerate(archiver.ADDON_ORDER): name,guid,deps=runner.PACKED_PROJECTS[project] lines.extend([f"00 ENGINE : FileSystem: Adding package '{folder/project}/' (pak count: 1) to filesystem under name {name}", f"00 RESOURCES : ResourceDB: loading cache (id={i} name={name} path={folder/project}/resourceDatabase.rdb)"]) lines.extend(['00 SCRIPT : Module: GameLib; defines: "PLATFORM_WINDOWS"','00 SCRIPT : Module: Game; defines: "PLATFORM_WINDOWS"','00 SCRIPT : ERR ORDINARY: header verified project=chat systems=MissionSystems','00 SCRIPT : ERR ORDINARY: local participant verified project=chat phase=prison','00 SCRIPT : Rereintroduced [mission]: phase 0 -> 1']) return '\n'.join(lines)+'\n' def test_chat_preserves_all_fifteen_artifacts_and_schema(self): destination,status=archiver.archive(self.session,'packed-chat',self.root);self.assertEqual(status,0) self.assertEqual(runner.packed_payload_inventory(destination/'package','chat'),self.meta['artifacts']) self.assertEqual(len(self.meta['artifacts']),15) for name,info in self.meta['artifacts'].items():self.assertEqual(archiver.digest((destination/'package'/name).read_bytes()),info['sha256']) kind=json.loads((destination/'archive-kind.json').read_text());self.assertEqual(kind['schema_version'],2);self.assertEqual(kind['kind'],'packed-chat-consumption');self.assertEqual(kind['addon_order'],archiver.ADDON_ORDER) report=json.loads((destination/'report.json').read_text());self.assertTrue(report['packed_consumption_verified']);self.assertFalse(report['release_qualified']) self.assertFalse((destination/'source-sha256.json').exists()) def test_missing_extra_or_traversal_manifest_key_refused(self): original=dict(self.meta['artifacts']) for operation in ('missing','extra','traversal'): self.meta['artifacts']=dict(original) if operation=='missing':del self.meta['artifacts']['map/data.pak'] elif operation=='extra':self.meta['artifacts']['foreign/data.pak']=original['map/data.pak'] else:self.meta['artifacts']['../data.pak']=self.meta['artifacts'].pop('map/data.pak') self.update_metadata() with self.subTest(operation=operation),self.assertRaisesRegex(ValueError,'artifact set'):archiver.inspect(self.session) def test_schema_kind_project_order_and_core_result_cannot_mix(self): original=dict(self.meta) for key,value in [('schema_version',1),('kind','packed-core-runtime-input'),('project','core'),('addon_order',['core','chat','ai','intel','map'])]: self.meta=dict(original);self.meta[key]=value;self.update_metadata() with self.subTest(key=key),self.assertRaises(ValueError):archiver.inspect(self.session) self.meta=original;self.update_metadata();self.report['result']='packed-core-started';self.write_reports() with self.assertRaisesRegex(ValueError,'result'):archiver.inspect(self.session) def test_wrong_companion_identity_is_not_accepted_as_hash_manifest(self): path=self.packed['input']/'ai/addon.gproj';path.write_bytes((self.packed['input']/'core/addon.gproj').read_bytes()) data=path.read_bytes();self.meta['artifacts']['ai/addon.gproj']={'bytes':len(data),'sha256':archiver.digest(data)};self.update_metadata() with self.assertRaises(ValueError):archiver.inspect(self.session) def test_each_companion_loaded_mount_and_database_evidence_required(self): for fragment in ("under name ERR_Ai", "name=ERR_Intel path=", "guid: '9AAEE9041D79809E'", 'header verified project=chat', 'local participant verified project=chat'): self.reseal_console(self.content().replace(fragment,'missing')) with self.subTest(fragment=fragment):self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) def test_redirected_companion_and_loose_extra_search_fail_native_review(self): text=self.content().replace(str(self.packed['input']/'ai'),'Z:/loose/ai') self.reseal_console(text) self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) self.reseal_console(self.content()+"00 ENGINE : dir: '/loose/source/addons'\n") self.assertFalse(archiver.inspect(self.session)[1]['packed_consumption_verified']) def test_valid_failed_chat_run_archives_nonzero(self): self.report.update(result='timeout',packed_consumption_verified=False);self.write_reports() destination,status=archiver.archive(self.session,'failed-chat',self.root);self.assertEqual(status,1) self.assertTrue((destination/'package/ai/data.pak').is_file()) def test_recorded_command_cannot_select_core_or_an_external_addon_parent(self): original=list(self.meta['command']) for flag,value in [('-addons','2528466DE220D91C'),('-addonsDir','Z:/loose/addons')]: self.meta['command']=list(original);self.meta['command'][self.meta['command'].index(flag)+1]=value;self.update_metadata() with self.subTest(flag=flag),self.assertRaisesRegex(ValueError,'Recorded command differs'):archiver.inspect(self.session) if __name__=='__main__':unittest.main()