Count occupied restarts as failed missions; sync published Workshop version
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-07 01:05:43 +01:00
parent 161100cf08
commit e421dcfc15
14 changed files with 640 additions and 50 deletions
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Run isolated native occupancy, JSON persistence, recovery and shutdown checks."""
import argparse
import json
from pathlib import Path
import shutil
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[3]
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--work', type=Path, required=True, help='New disposable directory; must not exist')
p.add_argument('--compat', type=Path, required=True, help='Existing private test Proton prefix')
p.add_argument('--external-addons', type=Path, required=True)
a = p.parse_args()
work = a.work.resolve()
if work.exists():
p.error('--work must not exist; preserve evidence from earlier runs')
work.mkdir(parents=True)
source, profile = work/'source', work/'profile'
for name in ['addons', 'tools']:
shutil.copytree(ROOT/name, source/name)
(source/'docs').mkdir()
shutil.copy2(ROOT/'docs/external-addons.json', source/'docs/external-addons.json')
def replace(path, before, after):
text = path.read_text()
if text.count(before) != 1:
raise ValueError(f'Fixture insertion point changed in {path.name}')
path.write_text(text.replace(before, after))
scripts = source/'addons/core/Scripts/Game/ERR/00_Core'
shutil.copy2(Path(__file__).with_name('ERR_TelemetryCheckpointTest.c'), scripts/'ERR_TelemetryCheckpointTest.c')
# Modded classes can only access fields/methods of earlier layers. Test accessors
# follow telemetry; the existing native-end fixture is moved after that layer.
replace(scripts/'ERR_TelemetryLifecycle.c', ' void ERR_RefreshTelemetry()',
' bool ERR_TestTelemetryBefore() { return m_ERRTelemetry && m_ERRTelemetry.ERR_TestOccupiedRecovery(); }\n'
' bool ERR_TestTelemetryAfter() { return m_ERRTelemetry && m_ERRTelemetry.ERR_TestFinished(); }\n'
' void ERR_RefreshTelemetry()')
probe = scripts/'ERR_NativeEndProbe.c'
replace(probe, 'm_ERREndData = SCR_GameModeEndData.CreateSimple',
'if (!ERR_EndCheck(ERR_TestTelemetryBefore(), "occupied telemetry snapshot and recovery")) return;\n'
' m_ERREndData = SCR_GameModeEndData.CreateSimple')
replace(probe, 'EndGameMode(m_ERREndData);',
'EndGameMode(m_ERREndData);\n'
' if (!ERR_EndCheck(ERR_TestTelemetryAfter(), "native shutdown records one defeat")) return;')
probe.rename(scripts/'ERR_ZTelemetryNativeEndProbe.c')
runner = source/'tools/headless_workbench.py'
replace(runner, 'if a.roadblock_probe_count is not None: command',
"command += ['-errTelemetryTest', '1']\nif a.roadblock_probe_count is not None: command")
# Use two explicit process launches: Workbench need not auto-reload a hosted game.
replace(runner, "a.project != 'core' or a.rounds != 2 or a.timeout < 120", "a.project != 'core' or a.rounds != 1 or a.timeout < 120")
replace(runner, "if any((profile/name).exists() or (profile/name).is_symlink() for name in fixture_names): p.error('native-end fixture paths must not preexist; preserve prior evidence before retrying')",
'# This isolated fixture intentionally preserves the first instance marker.')
(profile/'profile').mkdir(parents=True)
(profile/'profile/EscapistsTelemetry.json').write_text(
'{"endpoint":"https://telemetry.invalid/api/mission-events",'
'"token":"local-test-only","serverName":"Isolated restart test"}\n')
command = [sys.executable, str(runner), '--compat', str(a.compat.resolve()),
'--profile', str(profile), '--external-addons', str(a.external_addons.resolve()),
'--smoke', 'native-end-preparing', '--project', 'core', '--rounds', '1', '--timeout', '120']
sys.path.insert(0, str(ROOT/'tools'))
from native_report import analyze
for _ in range(2):
old_logs = set((profile/'logs').glob('logs_*'))
result = subprocess.run(command, check=False)
new_logs = set((profile/'logs').glob('logs_*')) - old_logs
assert len(new_logs) == 1, 'Expected exactly one fresh native session'
session = new_logs.pop()
evidence = json.loads((session/'runner.json').read_text())
report = analyze((session/'console.log').read_text(errors='replace'), session/'console.log')
assert result.returncode in (0, 1)
assert evidence['result'] == 'smoke-completed' and evidence['cleanup_confirmed']
assert evidence['source_unchanged_after_run'] and evidence['external_addons_unchanged_after_run']
assert report['smoke_completions'] == 1 and not report['script_or_runtime_failures']
assert not report['missing_end_screen']
# The generic runner also grades all optional asset/backend errors. Preserve
# its complete report; this fixture grades telemetry, not DLC/asset integrity.
report['telemetry_assertions_passed'] = True
(session/'telemetry-check.json').write_text(json.dumps(report, indent=2)+'\n')
if report['engine_errors']:
print(f"Telemetry checks passed; {len(report['engine_errors'])} asset/backend errors retained in {session/'telemetry-check.json'}", flush=True)
state_path = profile/'profile/EscapistsTelemetryState.json'
events = [json.loads(payload) for payload in json.loads(state_path.read_text())['state']['pending']]
finished = [event for event in events if event['type'] == 'mission_finished']
assert len(finished) == 2 and len({event['missionId'] for event in finished}) == 2
assert all(event['outcome'] == 'defeat' and event['phase'] == 6 and event['connectedPlayers'] > 0 for event in finished)
restarts = [event for event in events if event['type'] == 'mission_restarted']
assert len(restarts) == 1 and restarts[0]['previousOutcome'] == 'defeat'
assert restarts[0]['previousMissionId'] in {event['missionId'] for event in finished}
subprocess.run(['node', '--input-type=module', '-e',
"import {readFileSync} from 'node:fs'; import {validateEvent} from "
+ json.dumps((ROOT/'services/telemetry-worker/src/validation.js').as_uri()) + ";"
+ "const {state}=JSON.parse(readFileSync(process.argv[1],'utf8'));"
+ "for(const payload of state.pending) validateEvent(JSON.parse(payload));", str(state_path)], check=True)
print('PASS: two native process instances, real occupied shutdowns, durable recovery, no duplicate results and receiver-valid native payloads')