#!/usr/bin/env python3 """Reproduce stock inventory UI behavior in a diagnostic-only vanilla-dependent addon. Never loads successor addons or edits installed/original content. Native errors remain failed. """ import argparse import hashlib import json from pathlib import Path import re import shutil import subprocess import sys import tempfile from native_report import analyze ROOT = Path(__file__).resolve().parents[1] FIXTURE = ROOT/'tests/native/inventory_control' HELPERS = ('headless_workbench.py','headless_game.py','private_prefix_cleanup.py','native_provenance.py','source_manifest.py','native_report.py','archive_native.py','project_schema.py') EXPECTED_ERROR = 'OverlayWidget Frame0 has invalid slot GridWidgetSlot for slot manipulation function used, expected: LayoutSlot' def fixture_inventory(folder): if folder.is_symlink() or not folder.is_dir(): raise ValueError('Control fixture must be a regular directory') result = {} for path in sorted(folder.rglob('*')): if path.is_symlink(): raise ValueError('Control fixture must not contain symlinks') if path.is_file(): if path.suffix == '.gproj' and path != folder/'addon.gproj': raise ValueError('Control fixture must contain only its root addon project') if path.suffix not in {'.c','.gproj','.et','.ent','.layer','.meta','.conf','.md'}: raise ValueError('Unexpected control fixture input: '+str(path)) result[str(path.relative_to(folder))] = hashlib.sha256(path.read_bytes()).hexdigest() elif not path.is_dir(): raise ValueError('Control fixture must contain only regular source files/directories') if not result or 'addon.gproj' not in result: raise ValueError('Missing diagnostic fixture') project = (folder/'addon.gproj').read_text() guids = re.findall(r'\bGUID\s+"([A-F0-9]{16})"', project) dependencies = re.findall(r'\bDependencies\s*\{([^{}]*)\}', project) if (len(guids) != 1 or len(re.findall(r'\bGUID\b', project)) != 1 or len(dependencies) != 1 or len(re.findall(r'\bDependencies\b', project)) != 1 or not re.fullmatch(r'\s*"58D0FB3206B6F859"\s*', dependencies[0])): raise ValueError('Control must depend only on installed vanilla') guid = guids[0] if guid in {'5614BBCCBB55ED1C','58D0FB3206B6F859','2528466DE220D91C','5192138CB9D5B911','D31039C7C53E348A','FA804F411BDDF2C6','9AAEE9041D79809E'}: raise ValueError('Control must use its own addon identity') return result,guid def harness_source(source): replacements = { 'ERR_ValidateBaseWorldPlugin':'ERRIC_InventoryControlPlugin', 'Worlds/ERR_Everon.ent':'Worlds/ERRIC_Inventory.ent', "'-errPlay', '1'":"'-errPlay', '1', '-erricInventoryControl', '1'", } for old,new in replacements.items(): if old not in source: raise ValueError('Known hidden harness entry changed: '+old) source = source.replace(old,new) return source def control_evidence(content,guid): report = analyze(content, 'console.log') blocks = [] for section in content.split('Loaded addons:')[1:]: block = [] for line in section.splitlines()[1:]: match = re.search(r"ENGINE\s*:\s*gproj: .*? guid: '([A-F0-9]{16})'\s*$", line) if not match: break block.append(match[1]) blocks.append(block) guids = blocks[0] if blocks else [] isolation = bool(blocks) and all(len(block)==3 and set(block)=={'5614BBCCBB55ED1C','58D0FB3206B6F859',guid} for block in blocks) errors = report['engine_errors'] target_errors = [error for error in errors if re.search(r'\bGUI\s+\(E\)', error) and EXPECTED_ERROR in error] other_errors = [error for error in errors if error not in target_errors] target_pair = (isolation and report['smoke_completions'] == 1 and not report['script_or_runtime_failures'] and not report['missing_end_screen'] and len(target_errors) == 2) reproduced = (isolation and report['smoke_completions']==1 and not report['script_or_runtime_failures'] and not report['missing_end_screen'] and len(errors)==2 and all(EXPECTED_ERROR in error for error in errors)) return {'loaded_addon_guids':guids,'loaded_addon_blocks':blocks,'only_vanilla_and_control_loaded':isolation, 'target_gui_error_count':len(target_errors),'target_gui_pair_observed':target_pair, 'other_engine_errors':other_errors, 'stock_slot_error_reproduced':reproduced,'native_clean':not errors and not report['script_or_runtime_failures'], 'native_report':report,'successor_or_release_qualified':False} def main(): p=argparse.ArgumentParser(description=__doc__) p.add_argument('--compat',type=Path,default=Path('/tmp/erri-headless-compat')) p.add_argument('--profile',type=Path,default=Path('/tmp/erri-inventory-control-profile')) p.add_argument('--timeout',type=int,default=150) p.add_argument('--archive-name',required=True) a=p.parse_args() if a.compat != Path('/tmp/erri-headless-compat') or a.compat.resolve()!=a.compat: p.error('Only the established disposable test prefix is supported') if not 90<=a.timeout<=300 or not re.fullmatch(r'inventory-control-[a-z0-9-]+',a.archive_name): p.error('Use a90..300 second bound and a unique inventory-control-* archive name') dest=ROOT/'docs/evidence'/a.archive_name if dest.exists() or dest.is_symlink():p.error('Archive must not preexist') profile=a.profile if profile != profile.resolve() or not profile.is_relative_to('/tmp') or profile==Path('/tmp') or profile.is_relative_to(a.compat) or a.compat.is_relative_to(profile): p.error('Use a separate canonical disposable /tmp profile') marker=profile/'.erri-inventory-control-profile.json';marker_value={'kind':'erri-vanilla-inventory-control','version':1} if profile.exists() and (not marker.is_file() or marker.is_symlink() or json.loads(marker.read_text())!=marker_value): p.error('Existing profile is not owned by this control') original,guid=fixture_inventory(FIXTURE) bundle=Path(tempfile.mkdtemp(prefix='erri-inventory-control-run-')) (bundle/'addons').mkdir();shutil.copytree(FIXTURE,bundle/'addons/core');(bundle/'tools').mkdir() for name in HELPERS:shutil.copyfile(ROOT/'tools'/name,bundle/'tools'/name) runner=bundle/'tools/headless_workbench.py';runner.write_text(harness_source(runner.read_text())) profile.mkdir(parents=True,exist_ok=True);marker.write_text(json.dumps(marker_value)+'\n') tool_hashes={name:hashlib.sha256((bundle/'tools'/name).read_bytes()).hexdigest() for name in HELPERS} command=[sys.executable,str(runner),'--compat',str(a.compat),'--profile',str(profile),'--project','core','--smoke','victory','--rounds','1','--timeout',str(a.timeout)] before=set((profile/'logs').glob('logs_*')) if (profile/'logs').exists() else set() with (bundle/'control-launch.log').open('w') as output: result=subprocess.run(command,stdout=output,stderr=subprocess.STDOUT) fresh=set((profile/'logs').glob('logs_*'))-before if len(fresh)!=1: print((bundle/'control-launch.log').read_text(),end='');raise RuntimeError('Control did not produce exactly one session; retained bundle '+str(bundle)) session=next(iter(fresh));runner_report=json.loads((session/'runner.json').read_text()) if runner_report.get('cleanup_confirmed') is not True or runner_report.get('source_unchanged_after_run') is not True: raise RuntimeError('Control cleanup/source seal incomplete; retained bundle '+str(bundle)) subprocess.run([sys.executable,str(bundle/'tools/archive_native.py'),str(session),a.archive_name],check=False) staged=bundle/'docs/evidence'/a.archive_name if not staged.is_dir():raise RuntimeError('Native archive was not created; retained bundle '+str(bundle)) for capture in sorted([*session.glob('erric-*.png'), *session.glob('erric-*.bmp')]): if capture.is_file() and not capture.is_symlink(): shutil.copyfile(capture,staged/capture.name) after,_=fixture_inventory(FIXTURE) if original!=after:raise RuntimeError('Authored control fixture changed during run') review=control_evidence((session/'console.log').read_text(errors='replace'),guid) review.update(scope='Scripted stock-character inventory control with vanilla plus one diagnostic addon only; no normal-input, successor gameplay or release qualification.', native_runner_exit=result.returncode,session=str(session),bundle=str(bundle),command=command, fixture_sha256=original,harness_sha256=tool_hashes,cleanup_confirmed=True,source_unchanged_after_run=True) (staged/'control-review.json').write_text(json.dumps(review,indent=2)+'\n') shutil.copyfile(bundle/'control-launch.log',staged/'control-launch.log.txt') # Preserve the actual remapped harness which launched this isolated input. (staged/'harness.py.txt').write_bytes(runner.read_bytes()) (staged/'control-extra-sha256.json').write_text(json.dumps({name:hashlib.sha256((staged/name).read_bytes()).hexdigest() for name in ('control-review.json','control-launch.log.txt','harness.py.txt')},indent=2)+'\n') shutil.copytree(staged,dest) print(json.dumps({'archive':str(dest),**review},indent=2)) return 0 if review['stock_slot_error_reproduced'] else 1 if __name__=='__main__':raise SystemExit(main())