Integrate delivered HQ memory, finite physical convoy and RHS WCS dependencies
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""Exact synthetic external closure and packaging orchestration; never launch native executables."""
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import unittest
|
||||
import uuid
|
||||
from unittest.mock import Mock,patch
|
||||
import test_native_package as base
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import native_package as n
|
||||
import external_addons as ext
|
||||
|
||||
class ExternalPackageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.fixture=base.NativePackageTests();self.fixture.setUp();self.addCleanup(self.fixture.doCleanups)
|
||||
self.source=self.fixture.external_fixture()
|
||||
self.root=self.fixture.source;self.session=self.fixture.session
|
||||
self.steam=self.fixture.steam;self.compat=self.fixture.compat
|
||||
self.context=n.external_request('chat',self.source,self.root)
|
||||
def prepare(self):
|
||||
self.authored=n.copy_payload(self.root,self.session)
|
||||
n.capture_external(self.context,self.session)
|
||||
def profile(self,project):
|
||||
profile=self.session/'profiles'/project;(profile/'addons').mkdir(parents=True)
|
||||
(profile/'addons/ArmaReforger').symlink_to(self.steam/'steamapps/common/Arma Reforger/addons/data',target_is_directory=True)
|
||||
return profile
|
||||
def native_double(self,project='ai',mutation=None,error='',native_exit=0,cleanup=True,write_status=True):
|
||||
child=Mock();child.poll.return_value=0
|
||||
def spawn(command,**kwargs):
|
||||
self.assertNotIn('WINEPREFIX',kwargs['env'])
|
||||
logs=self.session/'profiles'/project/'logs/logs_synthetic';logs.mkdir(parents=True)
|
||||
(logs/'console.log').write_text('SYNTHETIC PACKAGE DOUBLE; NOT NATIVE EVIDENCE\n'+error)
|
||||
(self.session/'packages'/project/'data.pak').write_bytes(b'SYNTHETIC SUCCESSOR OUTPUT')
|
||||
native=n.command_for(self.steam,self.session,project);native=native[native.index('--')+1:]
|
||||
if write_status:n.save(self.session/'evidence'/project/'native-exit.json',{'command':native,'exit_code':native_exit})
|
||||
if mutation:mutation()
|
||||
return child
|
||||
with patch.object(n,'assert_no_game'),patch.object(n,'assert_prefix_idle'), \
|
||||
patch.object(n.subprocess,'Popen',side_effect=spawn) as launch, \
|
||||
patch.object(n.subprocess,'check_output',side_effect=lambda args,**kw:'[]' if 'sink-inputs' in args else '7'), \
|
||||
patch.object(n.subprocess,'run'),patch.object(n,'cleanup_owned_run',return_value={'cleanup_confirmed':cleanup,'errors':[]}) as cleaned:
|
||||
report=n.run_project(self.steam,self.compat,self.session,project,30,self.authored,
|
||||
external=self.context if project in {'ai','chat'} else None)
|
||||
launch.assert_called_once();cleaned.assert_called_once()
|
||||
return report
|
||||
def args(self,project='chat',external=True,continuation=True):
|
||||
session=Path('/tmp')/('erri-external-package-'+uuid.uuid4().hex)
|
||||
self.addCleanup(lambda:shutil.rmtree(session,ignore_errors=True))
|
||||
return argparse.Namespace(project=project,external_addons=self.source if external else None,
|
||||
session=session,steam=self.steam,compat=self.compat,timeout=30,continue_on_failure=continuation)
|
||||
def run_double(self,args,callback):
|
||||
with patch.object(n,'assert_no_game'),patch.object(n,'assert_prefix_idle'), \
|
||||
patch.object(n.shutil,'which',return_value='/mock'),patch.object(n,'run_project',side_effect=callback) as runs, \
|
||||
patch.object(n.subprocess,'Popen') as child,contextlib.redirect_stdout(io.StringIO()):
|
||||
code=n.run(args,self.root)
|
||||
child.assert_not_called()
|
||||
return code,json.loads((args.session/'package-report.json').read_text()),runs.call_args_list
|
||||
def test_option_is_required_only_for_external_projects_before_native_work(self):
|
||||
for project in ('ai','chat'):
|
||||
with self.subTest(project=project),patch.object(n,'assert_no_game') as game,patch.object(n,'copy_payload') as copy:
|
||||
with self.assertRaisesRegex(ValueError,'require --external-addons'):n.run(self.args(project,False),self.root)
|
||||
game.assert_not_called();copy.assert_not_called()
|
||||
for project in ('core','intel','map'):
|
||||
with self.subTest(project=project),patch.object(n,'assert_no_game') as game:
|
||||
with self.assertRaisesRegex(ValueError,'vanilla-only'):n.run(self.args(project),self.root)
|
||||
game.assert_not_called()
|
||||
self.assertIsNone(n.external_request(project,None,self.root))
|
||||
def test_cli_passes_explicit_installed_path_without_launch(self):
|
||||
with patch.object(n,'run',return_value=1) as run:
|
||||
self.assertEqual(n.main(['--project','ai','--session','/tmp/not-created','--compat',str(self.compat),
|
||||
'--external-addons',str(self.source)]),1)
|
||||
self.assertEqual(run.call_args.args[0].external_addons,self.source)
|
||||
def test_exact_profiles_expose_only_reviewed_six_not_workshop_parent(self):
|
||||
unrelated=self.source/'original';unrelated.mkdir();(unrelated/'addon.gproj').write_text('GameProject { GUID "5F16D7E4A1CBE075" }')
|
||||
self.prepare()
|
||||
for project in ('core','ai','intel','map','chat'):
|
||||
profile=self.profile(project);context=self.context if project in ('ai','chat') else None
|
||||
records=n.expose_external(project,profile,self.steam,context)
|
||||
expected={'ArmaReforger'}|({'ERRI_'+guid for guid in ext.ALLOWED} if context else set())
|
||||
self.assertEqual({p.name for p in (profile/'addons').iterdir()},expected)
|
||||
self.assertEqual(len(records),6 if context else 0)
|
||||
for link in (profile/'addons').iterdir():
|
||||
self.assertTrue(link.is_symlink());self.assertNotEqual(link.resolve(),self.source)
|
||||
self.assertNotEqual(link.resolve(),unrelated)
|
||||
command=n.command_for(self.steam,self.session,project)
|
||||
self.assertEqual(command[command.index('-addonsDir')+1],'Z:'+str(self.session/'source/addons'))
|
||||
self.assertNotIn(str(self.source),command)
|
||||
self.assertFalse((self.session/'source/addons'/ext.DIRECT[0]).exists())
|
||||
self.assertEqual(json.loads((self.session/'external-addons-before.json').read_text()),self.context['records'])
|
||||
def test_native_helper_rejects_missing_or_wrong_context_before_creating_profile(self):
|
||||
self.prepare()
|
||||
with patch.object(n.subprocess,'Popen') as child:
|
||||
with self.assertRaises(ValueError):n.run_project(self.steam,self.compat,self.session,'ai',30,self.authored)
|
||||
with self.assertRaises(ValueError):n.run_project(self.steam,self.compat,self.session,'core',30,self.authored,external=self.context)
|
||||
child.assert_not_called()
|
||||
self.assertFalse((self.session/'profiles').exists())
|
||||
def test_missing_manifest_changed_policy_or_foreign_dependency_refuse(self):
|
||||
manifest=self.source/ext.DIRECT[0]/'addon.gproj';manifest.write_text(manifest.read_text()+'// altered')
|
||||
self.session.mkdir()
|
||||
with self.assertRaisesRegex(ValueError,'manifest changed'):n.capture_external(self.context,self.session)
|
||||
self.assertFalse((self.session/'profiles').exists())
|
||||
def test_missing_closure_and_resource_identity_refuse_before_exposure(self):
|
||||
database=self.source/ext.DIRECT[1]/'resourceDatabase.rdb';database.write_bytes(b'wrong resource identity')
|
||||
self.session.mkdir()
|
||||
with self.assertRaisesRegex(ValueError,'resource identity'):n.capture_external(self.context,self.session)
|
||||
shutil.rmtree(self.source/ext.DIRECT[0])
|
||||
with self.assertRaisesRegex(ValueError,'Missing installed'):n.capture_external(self.context,self.session)
|
||||
def test_unrelated_addon_wrong_link_and_whole_tree_link_are_refused(self):
|
||||
self.prepare();profile=self.profile('ai');records=n.expose_external('ai',profile,self.steam,self.context)
|
||||
addon=profile/'addons';extra=addon/'Original';extra.symlink_to(self.source,target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError,'unrelated'):n.verify_package_profile(profile,self.steam,records)
|
||||
extra.unlink();link=addon/('ERRI_'+ext.DIRECT[0]);link.unlink();link.symlink_to(self.source,target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError,'link changed'):n.verify_package_profile(profile,self.steam,records)
|
||||
link.unlink();link.mkdir()
|
||||
with self.assertRaisesRegex(ValueError,'link changed'):n.verify_package_profile(profile,self.steam,records)
|
||||
def test_native_clean_exit_binds_before_after_external_fingerprints(self):
|
||||
self.prepare();report=self.native_double()
|
||||
self.assertTrue(report['candidate_created']);self.assertTrue(report['external_addons_unchanged_after_run'])
|
||||
self.assertTrue(report['profile_addons_unchanged']);self.assertEqual(report['external_addon_count'],6)
|
||||
evidence=self.session/'evidence/ai'
|
||||
before=json.loads((evidence/'external-addons-before.json').read_text())
|
||||
after=json.loads((evidence/'external-addons-after.json').read_text())
|
||||
self.assertTrue(after['unchanged']);self.assertEqual(after['records'],before)
|
||||
self.assertEqual(json.loads((evidence/'launch.json').read_text())['external_policy_sha256'],report['external_policy_sha256'])
|
||||
for key in ('release_qualified','package_contents_verified','native_consumption_verified','tool_completion_semantics_verified'):
|
||||
self.assertFalse(report[key])
|
||||
def test_external_payload_change_cannot_qualify_after_clean_native_exit(self):
|
||||
self.prepare();pak=self.source/ext.DIRECT[0]/'data.pak'
|
||||
report=self.native_double(mutation=lambda:pak.write_bytes(b'changed installed payload'))
|
||||
self.assertFalse(report['candidate_created']);self.assertFalse(report['external_addons_unchanged_after_run'])
|
||||
self.assertTrue(report['copied_authored_unchanged']);self.assertTrue(report['cleanup_confirmed'])
|
||||
self.assertTrue(any('External input validation failed' in e for e in report['errors']))
|
||||
after=json.loads((self.session/'evidence/ai/external-addons-after.json').read_text())
|
||||
self.assertFalse(after['unchanged']);self.assertNotEqual(after['records'],self.context['records'])
|
||||
def test_cache_tampering_cannot_relabel_changed_payload_as_unchanged(self):
|
||||
self.prepare()
|
||||
self.context['cache_path'].write_text('{}')
|
||||
with self.assertRaisesRegex(ValueError,'fingerprint cache changed'):n.verify_external(self.context)
|
||||
def test_policy_mutation_or_deleted_input_cannot_qualify(self):
|
||||
self.prepare();path=self.context['policy_path']
|
||||
report=self.native_double(mutation=lambda:path.write_text(path.read_text()+'\n'))
|
||||
self.assertFalse(report['candidate_created']);self.assertFalse(report['external_addons_unchanged_after_run'])
|
||||
after=json.loads((self.session/'evidence/ai/external-addons-after.json').read_text());self.assertIsNone(after['records'])
|
||||
def test_relinked_private_dependency_fails_without_altering_installed_inputs(self):
|
||||
self.prepare()
|
||||
def mutate():
|
||||
link=self.session/'profiles/ai/addons'/('ERRI_'+ext.DIRECT[0]);link.unlink();link.symlink_to(self.source,target_is_directory=True)
|
||||
report=self.native_double(mutation=mutate)
|
||||
self.assertFalse(report['candidate_created']);self.assertFalse(report['profile_addons_unchanged'])
|
||||
self.assertTrue(report['external_addons_unchanged_after_run'])
|
||||
def test_unchanged_external_inputs_do_not_suppress_native_errors(self):
|
||||
self.prepare();report=self.native_double(error='RESOURCES (E): retained upstream localization error')
|
||||
self.assertFalse(report['candidate_created']);self.assertTrue(report['external_addons_unchanged_after_run'])
|
||||
self.assertTrue(any('retained upstream localization error' in e for e in report['errors']))
|
||||
def test_missing_native_exit_or_failed_cleanup_remain_disqualifying(self):
|
||||
self.prepare();report=self.native_double(write_status=False,cleanup=False)
|
||||
self.assertFalse(report['candidate_created']);self.assertIsNone(report['native_exit_code'])
|
||||
self.assertFalse(report['cleanup_confirmed']);self.assertTrue(report['external_addons_unchanged_after_run'])
|
||||
def test_context_only_passed_to_ai_and_chat_in_full_build(self):
|
||||
args=self.args()
|
||||
def result(*pos,**kw):
|
||||
self.assertEqual(kw['external'] is not None,pos[3] in ('ai','chat'))
|
||||
return base.NativePackageTests.completed_report(True)
|
||||
code,summary,calls=self.run_double(args,result)
|
||||
self.assertEqual(code,0);self.assertEqual([c.args[3] for c in calls],['core','ai','intel','map','chat'])
|
||||
self.assertEqual(summary['external_addon_count'],6);self.assertTrue(summary['external_addons_unchanged_after_run'])
|
||||
self.assertFalse(summary['release_qualified']);self.assertFalse(summary['published'])
|
||||
self.assertTrue((args.session/'external-addons-after.json').is_file())
|
||||
def test_diagnostic_continuation_stops_on_mid_batch_external_mutation(self):
|
||||
args=self.args();seen=[]
|
||||
def result(*pos,**kw):
|
||||
seen.append(pos[3]);(self.source/ext.DIRECT[0]/'data.pak').write_bytes(b'changed between projects')
|
||||
return base.NativePackageTests.completed_report(True)
|
||||
code,summary,calls=self.run_double(args,result)
|
||||
self.assertEqual(code,1);self.assertEqual(seen,['core']);self.assertEqual(len(calls),1)
|
||||
self.assertFalse(summary['external_addons_unchanged_after_run']);self.assertFalse(summary['all_candidates_created'])
|
||||
self.assertIn('external dependency inputs changed',summary['stopped_reason'])
|
||||
self.assertEqual(summary['skipped_projects'],['ai','intel','map','chat'])
|
||||
def test_diagnostic_continuation_cannot_ignore_unverified_external_report(self):
|
||||
args=self.args();reports=[base.NativePackageTests.completed_report(True),
|
||||
base.NativePackageTests.completed_report(True,external_addons_unchanged_after_run=False)]
|
||||
code,summary,calls=self.run_double(args,lambda *a,**k:reports.pop(0))
|
||||
self.assertEqual(code,1);self.assertEqual(len(calls),2);self.assertIn('external dependency',summary['stopped_reason'])
|
||||
self.assertFalse(summary['all_candidates_created'])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user