Files
EscapistsReintroduced/tests/test_native_local_handoff_probe.py
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

83 lines
8.8 KiB
Python

"""Actual staged fixture transitions, immutable comparison and cleanup; no native proof."""
import os,re,sys
from pathlib import Path
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'));sys.path.insert(0,str(REPO/'tests'))
from policy_harness import body,run
P=Path(os.environ.get('ERR_LOCAL_PROBE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeLocalHandoffProbe.c'))
s='\n'.join(body(P,signature) for signature in ('protected bool SameSnapshot(','int Tick(ERR_HQ hq, float now)','void Stop()'))
s=s.replace('protected ','').replace('null','nullptr').replace('bool restore;', 'bool restore = false;')
s=s.replace('ERR_GameMode.Get()', 'Mode::Get()')
for a,b in [('ERR_HQReport report','Report* report'),('ERR_HQ hq','HQ* hq'),('ERR_HQBelief before','Belief* before'),('ERR_HQBelief belief','Belief* belief'),('IEntity caller','Actor* caller')]:s=s.replace(a,b)
for v in ('report','m_Payload','m_Source','m_Recipient','m_Mode','m_HQ','m_Route','hq','before','belief','m_Witness'):s=s.replace(v+'.',v+'->')
s=s.replace('Memory().','Memory()->').replace('Nodes[0].','Nodes[0]->').replace('PendingReport.','PendingReport->')
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct Group{};
struct Actor{int moved=0,last=0;void SetOrigin(int value){moved++;last=value;}};
struct Mode{static inline Mode* current=nullptr;bool active=true;bool Active(){return active;}static Mode* Get(){return current;}};
struct Report{int MessageId=1,Position=20,ObservedAt=0;Group* Source=nullptr;Mode* Target=nullptr;};
struct Node{struct Group* Group;Actor* Carrier;};
struct Nodes:std::vector<Node*>{int Count(){return size();}};
struct Route{Group* LocalSource=nullptr;Actor* LocalWitness=nullptr;struct Nodes Nodes;bool Originated=false,Sending=false,Failed=false;int Cursor=0,result=0;int Advance(float,int){return result;}};
struct Unit{struct Group* Group;Route* Uplink=nullptr;Report* PendingReport=nullptr;};
struct Belief{int MessageId=0;struct Report* Report=nullptr;};
struct Memory{Belief* belief=nullptr;Belief* Get(Mode*){return belief;}};
struct HQ{struct Memory memory;int sourceToken=220001,recipientToken=220001;Group* source;int releases=0;int Journal(){return 0;}struct Memory* Memory(){return &memory;}int LogisticsToken(Group* g){return g==source?sourceToken:recipientToken;}void ReleaseLogistics(Group*g,int token){releases++;int& held=g==source?sourceToken:recipientToken;if(held==token)held=0;}bool ERR_ProbeSquadsReady(){return true;}};
struct Probe{static constexpr int TOKEN=220001;Mode* m_Mode;HQ* m_HQ;Unit* m_Source;Unit* m_Recipient;Actor* m_Witness;Actor* m_Radio;Route* m_Route;Report* m_Payload;int m_OriginalPosition=7,m_DeliveredId=0,m_State=1;float m_Wait=0,m_Deadline=0;bool m_Stopped=false,m_Failed=false,m_Positioned=true,owned=true,canQueue=true;int removals=0;bool Enabled(){return true;}bool Original(Unit*,Actor*){return owned&&m_Mode&&m_Mode==Mode::Get()&&m_Mode->active;}bool Select(){return true;}bool Arrange(){return true;}bool Queue(float){if(canQueue){m_Source->Uplink=m_Route;m_Source->PendingReport=m_Payload;}return canQueue;}bool Check(bool value,std::string){if(!value){m_Failed=true;Stop();}return value;}int RemoveRadios(Unit*,Actor*){removals++;return 1;}
METHODS
};
struct F{Mode mode;Group source,recipient;Actor witness,radio;Node node{&recipient,&radio};Route route;Report report;Unit src{&source},dst{&recipient};HQ hq;Probe p;Belief belief;F():p{&mode,&hq,&src,&dst,&witness,&radio,&route,&report}{Mode::current=&mode;hq.source=&source;route.LocalSource=&source;route.LocalWitness=&witness;route.Nodes.push_back(&node);report.Source=&source;report.Target=&mode;belief.MessageId=1;belief.Report=&report;}};
int main(){
{F f;assert(f.p.Tick(&f.hq,0)==0&&f.p.m_State==2&&!f.p.m_Failed);assert(f.p.Tick(&f.hq,1000)==0&&f.p.m_State==2);f.route.Originated=true;f.route.Cursor=1;f.hq.memory.belief=&f.belief;assert(f.p.Tick(&f.hq,6000)==0&&f.p.m_State==3&&f.p.m_DeliveredId==1);Report second=f.report;second.MessageId=2;second.ObservedAt=7000;Route retry=f.route;retry.Originated=false;retry.Cursor=0;f.p.m_Payload=&second;f.p.m_Route=&retry;assert(f.p.Tick(&f.hq,7000)==0&&f.p.m_State==4&&f.p.removals==1);assert(f.p.Tick(&f.hq,8000)==0);retry.Failed=true;assert(f.p.Tick(&f.hq,9000)==1&&f.p.m_Stopped&&f.hq.sourceToken==0&&f.hq.recipientToken==0&&f.witness.moved==1);}
// An HQ belief appearing before the actual radio chain finishes cannot pass.
{F f;f.p.Tick(&f.hq,0);f.hq.memory.belief=&f.belief;assert(f.p.Tick(&f.hq,1000)==-1&&f.p.m_Failed);}
{F f;f.p.Tick(&f.hq,0);Report altered=f.report;altered.Position++;f.src.PendingReport=&altered;assert(f.p.Tick(&f.hq,1000)==-1);}
{F f;Report other=f.report;other.Source=&f.recipient;assert(!f.p.SameSnapshot(&other));other=f.report;other.ObservedAt++;assert(!f.p.SameSnapshot(&other));}
// A newer second belief invalidates interruption proof even if route failed.
{F f;f.p.m_State=4;f.p.m_DeliveredId=1;f.belief.MessageId=2;f.hq.memory.belief=&f.belief;f.route.Failed=true;assert(f.p.Tick(&f.hq,0)==-1);}
{F f;f.src.Uplink=&f.route;f.src.PendingReport=&f.report;f.p.Stop();assert(!f.src.Uplink&&!f.src.PendingReport&&f.witness.moved==1);int calls=f.hq.releases;f.p.Stop();assert(f.hq.releases==calls&&f.witness.moved==1);}
{F f;Route other;f.src.Uplink=&other;f.src.PendingReport=&f.report;f.hq.sourceToken=99;f.hq.recipientToken=98;f.p.Stop();assert(f.src.Uplink==&other&&f.src.PendingReport==&f.report&&f.hq.sourceToken==99&&f.hq.recipientToken==98&&f.witness.moved==0);}
{F f;Mode::current=nullptr;f.p.Stop();assert(f.witness.moved==0);}
{F f;f.p.owned=false;f.p.Stop();assert(f.witness.moved==0);}
}
'''.replace('METHODS',s))
assert 'ObservationRoute(unit)' in body(P,'ERR_HQRoute ERR_LocalFixtureQueue(')
assert 'arranged.Snapshot()' in body(P,'ERR_HQRoute ERR_LocalFixtureQueue(')
assert 'm_Wait = now + 1500' in body(P,'int Tick(ERR_HQ hq, float now)')
print('PASS: actual fixture local/radio gating, immutable source/time/position, interrupted-report exclusion and exact-owner cleanup')
# Invalid fixture launch requests stop during argument validation, before any runtime work.
import subprocess
runner=Path(os.environ.get('ERR_LOCAL_RUNNER_SOURCE',REPO/'tools/headless_workbench.py'))
code="import sys,runpy;sys.path.insert(0,"+repr(str(REPO/'tools'))+");p=sys.argv.pop(1);runpy.run_path(p,run_name='__main__')"
for project,rounds,timeout in [('core','1','360'),('chat','2','360'),('chat','1','359')]:
result=subprocess.run([sys.executable,'-c',code,str(runner),'--compat','/tmp/unused-local-handoff','--profile','/tmp/unused-local-handoff-profile','--smoke','hq-local-handoff','--project',project,'--rounds',rounds,'--timeout',timeout],capture_output=True,text=True)
assert result.returncode==2 and 'hq-local-handoff requires Chat, one round and timeout at least360 seconds' in result.stderr,result.stderr
print('PASS: local-handoff runner refuses wrong project/round count/insufficient deadline before runtime work')
# Fixture radio deletion must wait for allocator ownership to end, not merely
# for a radio to appear in inventory or a late callback to claim completion.
settled=body(P,'bool ERR_LocalFixtureSettled()')
lookup=body(P,'bool ERR_LocalFixtureRadiosSettled(SCR_AIGroup group)')
lookup=lookup.replace('SCR_AIGroup group','Group* group').replace('ERR_NativeLocalHandoffProbe.Enabled()','Enabled()')
lookup=lookup.replace('foreach (ERR_EnemyEquipmentGroup state : m_Groups)','for (State* state : m_Groups)').replace('state.','state->').replace('Radio.','Radio->')
run(r'''
#include <cassert>
#include <vector>
struct Radio{bool m_Stopped=false,m_Submitted=false;
SETTLED
};
struct Group{};
struct State{struct Group* Group;struct Radio* Radio;};
struct Equipment{bool enabled=true;std::vector<State*> m_Groups;bool Enabled(){return enabled;}
LOOKUP
};
int main(){Radio radio;assert(!radio.ERR_LocalFixtureSettled());radio.m_Submitted=true;assert(!radio.ERR_LocalFixtureSettled());radio.m_Stopped=true;assert(!radio.ERR_LocalFixtureSettled());radio.m_Submitted=false;assert(radio.ERR_LocalFixtureSettled());Group group,unknown;State state{&group,&radio};Equipment e;e.m_Groups.push_back(&state);assert(e.ERR_LocalFixtureRadiosSettled(&group));assert(!e.ERR_LocalFixtureRadiosSettled(&unknown));assert(!e.ERR_LocalFixtureRadiosSettled(nullptr));radio.m_Submitted=true;assert(!e.ERR_LocalFixtureRadiosSettled(&group));state.Radio=nullptr;assert(e.ERR_LocalFixtureRadiosSettled(&group));e.enabled=false;assert(!e.ERR_LocalFixtureRadiosSettled(&group));}
'''.replace('SETTLED',settled).replace('LOOKUP',lookup))
selection=body(P,'protected bool Select()')
assert selection.index('ERR_LocalFixtureRadiosSettled(recipient)')<selection.index('ReserveLogistics(recipient.Group')
assert selection.index('ERR_LocalFixtureRadiosSettled(source)')<selection.index('ReserveLogistics(source.Group')
print('PASS: actual fixture allocator terminal/unsubmitted guard, original-group lookup and both candidate gates')