Prepare independent successor playtest with spawn recovery and casualty feedback
This commit is contained in:
@@ -11,7 +11,7 @@ def cpp(s):
|
||||
for name in ('corridor','low'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace(', null)', ', nullptr)')
|
||||
return re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])', r'\1f', s)
|
||||
run(r'''
|
||||
survey_code = (r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
@@ -75,6 +75,24 @@ int main(){
|
||||
reset();assert(Survey(a,{100.05f,10,0}));assert(SCR_TerrainHelper::samples==3772);
|
||||
}
|
||||
''')
|
||||
run(survey_code)
|
||||
# Execute the same actual Survey with WORKBENCH enabled. Native logging and
|
||||
# entity metadata are boundary doubles; every terrain/trace call remains real
|
||||
# to this actual-method harness. Refusal must log once without querying again.
|
||||
wb = '#define WORKBENCH\n' + survey_code
|
||||
wb = wb.replace('void reset(){world=World();', 'void reset(){diagnosticCount=0;world=World();')
|
||||
logging = r'''
|
||||
static bool diagnosticEnabled=true;static int diagnosticCount=0;
|
||||
static bool SurveyDiagnostics(){return diagnosticEnabled;}
|
||||
template<typename...T>void PrintFormat(const char*,T...){diagnosticCount++;}
|
||||
static void SurveyTraceFailure(const char*,float fraction,TraceBox*){assert(fraction==.5f);if(diagnosticEnabled)diagnosticCount++;}
|
||||
'''
|
||||
wb = wb.replace('static const int MAX_TERRAIN_NODES=4096;', logging+'\nstatic const int MAX_TERRAIN_NODES=4096;')
|
||||
wb = wb.replace('assert(Survey(a,b,&excluded));', 'assert(Survey(a,b,&excluded));assert(diagnosticCount==0);')
|
||||
wb = wb.replace('assert(!Survey(a,b));', 'assert(!Survey(a,b));assert(diagnosticCount==1);')
|
||||
wb = wb.replace('assert(!Survey(a,end));', 'assert(!Survey(a,end));assert(diagnosticCount==1);')
|
||||
wb = wb.replace('reset();assert(Survey(a,{100.05f,10,0}));', 'reset();diagnosticEnabled=false;SCR_TerrainHelper::kind=1;assert(!Survey(a,b)&&diagnosticCount==0&&SCR_TerrainHelper::samples==1);reset();diagnosticEnabled=true;assert(Survey(a,{100.05f,10,0}));')
|
||||
run(wb)
|
||||
source=P.read_text();qualify=body(P,'static ERR_AircraftFieldEnvelope QualifyField(')
|
||||
assert 'bool Claim(' not in source and 'bool ClaimedBy(' not in source
|
||||
assert 'ERR_AircraftFlightEnvelope.Qualify(mode, aircraft, pilot)' in qualify
|
||||
@@ -144,3 +162,17 @@ int main(){
|
||||
}
|
||||
''')
|
||||
print('PASS: actual QualifyField preserves original identity/footprint/time, orders complete surveys before issuance, and refuses failed stages/moved body/expired or changed world; native boundaries faked')
|
||||
|
||||
diag=body(P,'protected static void SurveyTraceFailure(')
|
||||
assert 'trace.TraceEnt' in diag and 'hit.GetPrefabData().GetPrefabName()' in diag
|
||||
for mutation in ('TraceMove(', 'Set', 'Spawn(', 'Delete(', 'GetTerrainY('):assert mutation not in diag
|
||||
# Exact optional scope executes separately; absent/wrong flags never log.
|
||||
scope=body(P,'protected static bool SurveyDiagnostics(').replace('protected ','').replace('System.','System::').replace('string enabled','std::string enabled')
|
||||
run(r'''#include <cassert>
|
||||
#include <string>
|
||||
struct System {static std::string smoke,outcome;static bool GetCLIParam(std::string key,std::string& out){out=key=="errSmoke"?smoke:outcome;return !out.empty();}};
|
||||
std::string System::smoke="",System::outcome="";
|
||||
'''+scope+r'''
|
||||
int main(){assert(!SurveyDiagnostics());System::smoke="1";assert(!SurveyDiagnostics());System::outcome="aircraft-route";assert(!SurveyDiagnostics());System::outcome="aircraft-field";assert(SurveyDiagnostics());System::smoke="0";assert(!SurveyDiagnostics());}
|
||||
''')
|
||||
print('PASS: WORKBENCH first-refusal diagnostics preserve ordinary predicates, query order/count/budget and opt-in scope; no native logging run')
|
||||
|
||||
@@ -93,18 +93,58 @@ int main(){
|
||||
f.CheckTranslation(61000,{-0.23f,8,0},8,0,1);assert(f.m_FieldPhase==1); // actual post-hover distance can exceed100m
|
||||
vector demand=f.FieldDemand({0,8,0},{},.1f);assert(f.m_FieldReference[0]>0&&f.m_FieldReference.Length()<=.020001f&&demand.Length()<=.080001f);
|
||||
f.m_FieldReference={2,0,0};f.m_FieldIntegral={1,0,0};f.m_Aircraft.origin={80,8,0};f.m_Simulation.velocity={2,0,0};
|
||||
f.CheckTranslation(89000,f.m_Aircraft.origin,8,2,1);assert(f.m_FieldPhase==1);
|
||||
f.m_Aircraft.origin={82,8,0};
|
||||
f.CheckTranslation(90000,f.m_Aircraft.origin,8,2,1);assert(f.m_FieldPhase==2&&f.m_FieldStopFrozen&&f.m_FieldLandingPoint[0]==100&&f.m_State==State::TRANSLATING&&f.landings==0&&f.m_StageDeadline==135000);
|
||||
float brakeDeadline=f.m_StageDeadline;float brakeCap=f.m_FieldBrakeSpeed;assert(near(brakeCap,1.8f));
|
||||
float brakeDeadline=f.m_StageDeadline;assert(f.m_FieldIntegral.Length()==0&&f.m_FieldReference.Length()==2);
|
||||
auto firstBrake=f.FieldDemand(f.m_Aircraft.origin,f.m_Simulation.velocity,.1f);assert(f.m_FieldReference.Length()<2&&f.m_FieldReference.Length()>=1.9799f&&firstBrake.Length()<=.080001f);
|
||||
float previous=f.m_FieldReference.Length();for(int i=0;i<20;i++){f.FieldDemand({85,8,0},{1.9f,0,0},.1f);assert(f.m_FieldReference.Length()<=previous+.000001f);previous=f.m_FieldReference.Length();}
|
||||
f.clock=91000;f.Cancel("stop during brake");assert(f.m_Cancelled&&f.m_FieldLandingPoint[0]==100&&f.m_StageDeadline==brakeDeadline&&f.landings==0);
|
||||
f.clock=92000;f.Cancel("duplicate");assert(f.m_Reason=="stop during brake"&&f.m_StageDeadline==brakeDeadline&&f.m_Token==77&&f.m_FieldBrakeSpeed==brakeCap);
|
||||
auto savedIntegral=f.m_FieldIntegral;auto savedReference=f.m_FieldReference;
|
||||
f.clock=91000;f.Cancel("stop during brake");assert(f.m_Cancelled&&f.m_FieldLandingPoint[0]==100&&f.m_StageDeadline==brakeDeadline&&f.landings==0);
|
||||
f.clock=92000;f.Cancel("duplicate");assert(f.m_Reason=="stop during brake"&&f.m_StageDeadline==brakeDeadline&&f.m_Token==77&&(f.m_FieldReference-savedReference).Length()==0&&(f.m_FieldIntegral-savedIntegral).Length()==0);
|
||||
for(int i=0;i<100;i++)f.FieldDemand(f.m_Aircraft.origin,f.m_Simulation.velocity,.1f);
|
||||
assert(f.m_FieldReference.Length()<=.00001f);
|
||||
f.m_Simulation.velocity={.1f,0,0};f.CheckTranslation(92000,{99.6f,8,0},8,.1f,1);f.CheckTranslation(93999,{99.6f,8,0},8,.1f,1);assert(f.m_FieldPhase==2);
|
||||
f.m_Simulation.velocity={.3f,0,0};f.CheckTranslation(94000,{99.6f,8,0},8,.3f,1);assert(f.m_FieldLowSince<0);
|
||||
f.m_Simulation.velocity={.1f,0,0};f.CheckTranslation(95000,{99.6f,8,0},8,.1f,1);f.CheckTranslation(97000,{99.6f,8,0},8,.1f,1);
|
||||
assert(f.m_FieldPhase==3&&f.m_FieldIntegral.Length()==0&&f.m_FieldReference.Length()==0&&f.m_StageDeadline==brakeDeadline&&f.landings==0);
|
||||
f.CheckTranslation(101999,{99.6f,8,0},8,.1f,1);assert(f.landings==0);
|
||||
f.CheckTranslation(102000,{99.6f,8,0},8,.1f,1);assert(f.landings==1&&f.m_State==State::LANDING&&!f.m_FieldArrived&&!f.m_TranslationProven&&f.m_StageDeadline==127000&&f.m_Deadline==240000);
|
||||
// A slow/unresponsive body cannot settle while a large slewed reference remains.
|
||||
Flight lag;started(lag,q);lag.m_Aircraft.origin={50,8,0};lag.m_Simulation.velocity={1,0,0};lag.m_FieldReference={2,0,0};lag.m_FieldIntegral={4,0,0};lag.clock=70000;lag.Cancel("lag stop");
|
||||
assert(lag.m_FieldIntegral.Length()==0&&lag.m_FieldReference[0]==2);
|
||||
auto lagPoint=lag.m_FieldLandingPoint;auto lagDeadline=lag.m_StageDeadline;
|
||||
vector lagDemand=lag.FieldDemand(lag.m_Aircraft.origin,lag.m_Simulation.velocity,.1f);
|
||||
assert(near(lag.m_FieldReference[0],1.98f)&&lagDemand[0]>0); // preserve, disclose native lag risk
|
||||
lag.m_Simulation.velocity={.1f,0,0};lag.CheckTranslation(70000,lag.m_Aircraft.origin,8,.1f,1);lag.CheckTranslation(72000,lag.m_Aircraft.origin,8,.1f,1);assert(lag.m_FieldPhase==2);
|
||||
for(int i=0;i<110;i++)lag.FieldDemand(lag.m_Aircraft.origin,lag.m_Simulation.velocity,.1f);
|
||||
lag.CheckTranslation(73000,lag.m_Aircraft.origin,8,.1f,1);assert(lag.m_FieldPhase==3&&lag.m_FieldLandingPoint[0]==lagPoint[0]&&lag.m_StageDeadline==lagDeadline);
|
||||
// Reversed native velocity does not reverse the braking reference toward the target.
|
||||
Flight opposite;started(opposite,q);opposite.m_Aircraft.origin={50,8,0};opposite.m_Simulation.velocity={-1,0,0};opposite.m_FieldReference={-2,0,0};opposite.m_FieldIntegral={-3,0,0};opposite.clock=70000;opposite.Cancel("opposing stop");
|
||||
assert(opposite.m_FieldIntegral.Length()==0&&opposite.m_FieldLandingPoint[0]<50);
|
||||
for(int i=0;i<110;i++){
|
||||
vector before=opposite.m_FieldReference;opposite.FieldDemand({60,8,0},opposite.m_Simulation.velocity,.1f);
|
||||
assert(opposite.m_FieldReference[0]<=.000001f&&opposite.m_FieldReference.Length()<=before.Length()+.000001f&&(opposite.m_FieldReference-before).Length()<=.020001f);
|
||||
}
|
||||
assert(opposite.m_FieldReference.Length()<=.00001f);
|
||||
// Below-threshold jitter resets the observed hold only when speed reaches .25.
|
||||
Flight jitter;started(jitter,q);jitter.m_Aircraft.origin={99.8f,8,0};assert(jitter.BeginFieldBrake(80000));
|
||||
jitter.m_Simulation.velocity={.249f,0,0};jitter.CheckTranslation(80000,jitter.m_Aircraft.origin,8,.249f,1);
|
||||
jitter.m_Simulation.velocity={.25f,0,0};jitter.CheckTranslation(81999,jitter.m_Aircraft.origin,8,.25f,1);assert(jitter.m_FieldLowSince<0);
|
||||
jitter.m_Simulation.velocity={0,0,0};jitter.CheckTranslation(82000,jitter.m_Aircraft.origin,8,0,1);jitter.CheckTranslation(83999,jitter.m_Aircraft.origin,8,0,1);assert(jitter.m_FieldPhase==2);jitter.CheckTranslation(84000,jitter.m_Aircraft.origin,8,0,1);assert(jitter.m_FieldPhase==3);
|
||||
// A ground-assisted stop cannot qualify or resume the airborne braking profile.
|
||||
Flight contactAdmission;started(contactAdmission,q);contactAdmission.m_Simulation.contact=true;
|
||||
assert(!contactAdmission.BeginFieldBrake(70000)&&contactAdmission.m_State==State::FAILED&&!contactAdmission.m_FieldStopFrozen&&contactAdmission.m_Controller.hoverWrites==0);
|
||||
Flight contactWrite;started(contactWrite,q);assert(contactWrite.BeginFieldBrake(70000));contactWrite.m_Simulation.contact=true;
|
||||
contactWrite.PrepareActions(contactWrite.m_Character,contactWrite.m_Pilot,contactWrite.m_Actions,.1f);
|
||||
assert(contactWrite.m_State==State::FAILED&&contactWrite.pairs==0&&!contactWrite.m_FieldArrived);
|
||||
contactWrite.m_Simulation.contact=false;contactWrite.Tick();assert(contactWrite.m_State==State::FAILED&&contactWrite.landings==0);
|
||||
Flight contactStop;started(contactStop,q);assert(contactStop.BeginFieldBrake(70000));contactStop.m_FieldLowSince=70000;contactStop.m_Simulation.contact=true;
|
||||
contactStop.CheckTranslation(73000,{99.8f,0,0},0,0,1);
|
||||
assert(contactStop.m_State==State::FAILED&&contactStop.m_FieldPhase==2&&!contactStop.m_FieldArrived&&contactStop.landings==0);
|
||||
// Real contact remains valid only after the existing airborne hold admitted landing.
|
||||
Flight permittedLanding;started(permittedLanding,q);permittedLanding.m_FieldPhase=3;permittedLanding.m_State=State::LANDING;permittedLanding.m_Simulation.contact=true;
|
||||
assert(permittedLanding.ControlLease()&&permittedLanding.failures==0);
|
||||
// Grounded completion requires exact frozen centre, stable native contact and shutdown.
|
||||
f.grounded=true;f.m_Aircraft.origin={98,0,0};assert(!f.Grounded());f.m_Aircraft.origin={99.6f,0,0};assert(f.Grounded());
|
||||
f.clock=103000;f.m_LastControl=f.clock;f.Tick();f.clock=104000;f.m_LastControl=f.clock;f.Tick();f.clock=104999;f.m_LastControl=f.clock;f.Tick();assert(f.shutdowns==0);
|
||||
|
||||
@@ -7,7 +7,7 @@ def cpp(x):
|
||||
x=x.replace('ERR_AircraftFlightState.','State::').replace('ERR_NativeAircraftFieldRules.','Rules::').replace('ERR_AircraftFieldRules.','FieldRules::').replace('vector.','vector::').replace('ERR_AircraftFieldFlight flight','Flight* flight').replace('flight.','flight->')
|
||||
return re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])',r'\1f',x)
|
||||
rules=s[s.index('class ERR_NativeAircraftFieldRules'):s.index('class ERR_NativeAircraftFieldProbe')].replace('class ERR_NativeAircraftFieldRules','struct Rules').rstrip()+';'
|
||||
observed='\n'.join(cpp(body(P,k)) for k in ('void BrakeBegan(', 'void BrakeStopped('))
|
||||
observed='\n'.join(cpp(body(P,k)) for k in ('void ObserveBrakingContact(', 'void BrakeBegan(', 'void BrakeStopped('))
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
@@ -19,7 +19,7 @@ struct FieldRules {static float HorizontalSpeed(vector v){return std::hypot(v.x,
|
||||
struct Flight {float ERR_NativeFieldStageDeadline(){return 70000;}vector LandingPoint(){return {100,0,0};}};
|
||||
template<typename...T>void PrintFormat(T...args){}
|
||||
struct Probe {
|
||||
bool m_Stopped=false,m_BrakeObserved=false,m_BrakeStopped=false,m_BrakePassed=false;
|
||||
bool m_Stopped=false,m_BrakeObserved=false,m_BrakeStopped=false,m_BrakePassed=false,m_BrakeContactObserved=false;
|
||||
string m_ObservationFailure;
|
||||
Flight* m_Flight=nullptr;float m_BrakeAt=0,m_BrakeReserve=0,m_BrakeStageDeadline=0,m_ControlDeadline=240000,m_BrakeSampledTravel=0,m_BrakeLowSince=-1;
|
||||
vector m_BrakeOrigin,m_BrakePrevious,m_BrakeLandingPoint;
|
||||
@@ -46,31 +46,45 @@ int main(){
|
||||
assert(!Rules::PhysicalFinish(true,true,true,1,2,1,1,101,101));
|
||||
for(int i=0;i<5;i++)assert(!Rules::CanDispose(i!=0,i!=1,i!=2,i!=3,i!=4));
|
||||
Flight flight,foreign;Probe p;p.m_Flight=&flight;
|
||||
p.BrakeBegan(&foreign,1000,{0,0,0},{2,0,0});assert(!p.m_BrakeObserved);
|
||||
p.BrakeBegan(&flight,1000,{0,0,0},{2,0,0});assert(p.m_BrakeObserved&&p.m_BrakeReserve>18&&p.m_BrakeReserve<19);
|
||||
p.BrakeBegan(&foreign,1000,{0,0,0},{2,0,0},false);assert(!p.m_BrakeObserved);
|
||||
p.BrakeBegan(&flight,1000,{0,0,0},{2,0,0},false);assert(p.m_BrakeObserved&&p.m_BrakeReserve>18&&p.m_BrakeReserve<19);
|
||||
p.m_BrakeSampledTravel=15;p.m_BrakePrevious={15,0,0};p.m_BrakeLowSince=2000;
|
||||
p.BrakeStopped(&flight,4000,{16,0,0},{.1f,0,0});assert(p.m_BrakePassed&&p.m_BrakeStopped&&p.m_ObservationFailure.empty());
|
||||
p.BrakeBegan(&flight,5000,{50,0,0},{2,0,0});assert(!p.m_ObservationFailure.empty()&&p.m_BrakeAt==1000); // no renewal
|
||||
p.BrakeStopped(&flight,4000,{16,0,0},{.1f,0,0},false);assert(p.m_BrakePassed&&p.m_BrakeStopped&&p.m_ObservationFailure.empty());
|
||||
p.BrakeBegan(&flight,5000,{50,0,0},{2,0,0},false);assert(!p.m_ObservationFailure.empty()&&p.m_BrakeAt==1000); // no renewal
|
||||
for(int fault=0;fault<4;fault++){
|
||||
Probe bad;bad.m_Flight=&flight;bad.BrakeBegan(&flight,1000,{0,0,0},{2,0,0});bad.m_BrakeLowSince=2000;
|
||||
Probe bad;bad.m_Flight=&flight;bad.BrakeBegan(&flight,1000,{0,0,0},{2,0,0},false);bad.m_BrakeLowSince=2000;
|
||||
float stopped=4000;vector at={16,0,0},vel={.1f,0,0};
|
||||
if(fault==0){at={19,0,0};} if(fault==1){stopped=3999;} if(fault==2){vel={.25f,0,0};} if(fault==3){bad.m_BrakeSampledTravel=20;}
|
||||
bad.BrakeStopped(&flight,stopped,at,vel);assert(!bad.m_BrakePassed&&!bad.m_ObservationFailure.empty()&&!bad.m_Stopped); // failure deferred outside callback
|
||||
bad.BrakeStopped(&flight,stopped,at,vel,false);assert(!bad.m_BrakePassed&&!bad.m_ObservationFailure.empty()&&!bad.m_Stopped); // failure deferred outside callback
|
||||
}
|
||||
assert(!Rules::Braking(NAN,1,1,0,3000,0)&&!Rules::Braking(10,NAN,1,0,3000,0));
|
||||
// Ground friction at entry, an intermediate callback, or the stop sample
|
||||
// permanently invalidates an otherwise plausible measured reserve/low-speed pass.
|
||||
for(int contactAt=0;contactAt<3;contactAt++){
|
||||
Probe grounded;grounded.m_Flight=&flight;
|
||||
grounded.BrakeBegan(&flight,1000,{0,0,0},{2,0,0},contactAt==0);
|
||||
grounded.m_BrakeLowSince=2000;
|
||||
grounded.ObserveBrakingContact(contactAt==1);
|
||||
grounded.ObserveBrakingContact(false); // later loss of contact cannot clear evidence
|
||||
grounded.BrakeStopped(&flight,4000,{16,0,0},{.1f,0,0},contactAt==2);
|
||||
assert(grounded.m_BrakeContactObserved&&!grounded.m_BrakePassed&&!grounded.m_ObservationFailure.empty()&&!grounded.m_Stopped);
|
||||
}
|
||||
Probe before;before.ObserveBrakingContact(true);assert(!before.m_BrakeContactObserved);
|
||||
Probe after;after.m_BrakeObserved=true;after.m_BrakeStopped=true;after.ObserveBrakingContact(true);assert(!after.m_BrakeContactObserved); // actual landing is separate
|
||||
assert(!Rules::Braking(NAN,1,1,0,3000,0,false)&&!Rules::Braking(10,NAN,1,0,3000,0,false));
|
||||
}
|
||||
''')
|
||||
assert s.startswith('#ifdef WORKBENCH') and s.rstrip().endswith('#endif')
|
||||
assert 'outcome == "aircraft-field"' in body(P,'static bool Enabled(')
|
||||
find=body(P,'protected bool FindPad(')
|
||||
assert '"4850.953 0 11787.412"' in find and '"100 0 0"' in find
|
||||
assert '"4890 0 11730"' in find and '"0 0 100"' in find
|
||||
assert find.count('.Survey(')==1 and 'for (' not in find and 'AirportOffset' not in s
|
||||
assert s.count('ERR_Actors.Spawn(AIRCRAFT,')==1 and s.count('SpawnCharacterInCompartment(')==1
|
||||
assert s.count('physics.SetActive(ActiveState.ACTIVE)')==1
|
||||
for prohibited in ('SetWorldTransform(', 'SetOrigin(', 'SetVelocity(', '.StartEngine(', '.StopEngine(', 'm_Mode.Own(', 'm_Flight.Tick(', 'm_Flight.Cancel(', 'm_Flight.StartField(', 'FieldDemand('):assert prohibited not in s
|
||||
for sig in ('void PrepareActions(', 'void PrepareHelicopter('):
|
||||
assert '++' in body(P,sig) and '.Prepare' not in body(P,sig)
|
||||
for sig in ('void BrakeBegan(', 'void BrakeStopped('):assert 'Check(' not in body(P,sig) and 'Stop(' not in body(P,sig)
|
||||
for sig in ('void ObserveBrakingContact(', 'void BrakeBegan(', 'void BrakeStopped('):assert 'Check(' not in body(P,sig) and 'Stop(' not in body(P,sig)
|
||||
assert 'ObserveBrakingContact' in body(P,'void PrepareHelicopter(') and 'ObserveBrakingContact(contact)' in body(P,'int Tick(')
|
||||
assert 'super.BeginFieldBrake(now)' in body(P,'protected override bool BeginFieldBrake(')
|
||||
assert 'super.CheckTranslation(now, position, height, speed, upright)' in body(P,'protected override void CheckTranslation(')
|
||||
start=body(P,'protected bool StartRetainedField(');assert '.StartField(m_Aircraft, m_Envelope, ORDER_TOKEN, m_ControlDeadline)' in start
|
||||
@@ -101,3 +115,78 @@ assert 'm_ERRTestAircraftControl || m_ERRTestAircraftRoute || m_ERRTestAircraftF
|
||||
assert 'm_ERRAircraftField.Stop()' in body(chat,'protected void ERR_StopHQPatrol(')
|
||||
assert 'errSmokeFieldServiceCancel' in cs and 'current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 241' in cs
|
||||
print('PASS: actual field result/braking observer and Chat dispatch rules; one finite fixed candidate, deferred callback failure and guarded cleanup; no native measurement')
|
||||
|
||||
# Execute the real optional grid/FindPad flow, faking only CLI/world/terrain/logging.
|
||||
def audit_cpp(text):
|
||||
text=text.replace('protected ','').replace('out vector result','vector& result')
|
||||
for before,after in (('System.','System::'),('Replication.','Replication::'),('SCR_TerrainHelper.','Terrain::'),('ERR_AircraftFieldEnvelope.','Envelope::'),('ERR_Actors.','Actors::'),('Math.','Math::'),('GetGame().GetWorld()','GetGame()->GetWorld()')):text=text.replace(before,after)
|
||||
for before,after in (('"0 0 0"','vector{}'),('"0 0 100"','vector{0,0,100}'),('"4890 0 11730"','vector{4890,0,11730}')):text=text.replace(before,after)
|
||||
return text
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
using string=std::string;
|
||||
struct vector {float v[3]={0,0,0};vector()=default;vector(float x,float y,float z):v{x,y,z}{}float& operator[](int i){return v[i];}float operator[](int i)const{return v[i];}vector operator+(vector b)const{return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}};
|
||||
std::ostream& operator<<(std::ostream& out,vector v){return out<<v[0]<<','<<v[1]<<','<<v[2];}
|
||||
std::vector<string> logs;
|
||||
void Print(const char* value){logs.push_back(value);}
|
||||
template<class...A>void PrintFormat(const char* format,A...args){std::ostringstream out;((out<<args<<' '),...);logs.push_back(string(format)+"|"+out.str());}
|
||||
struct System {static std::map<string,string> args;static bool GetCLIParam(string name,string& value){auto i=args.find(name);if(i==args.end())return false;value=i->second;return true;}};std::map<string,string> System::args;
|
||||
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
|
||||
struct Game {bool world=true;bool GetWorld(){return world;}} game;bool gamePresent=true;Game* GetGame(){return gamePresent?&game:nullptr;}
|
||||
struct Math {static float AbsFloat(float value){return std::abs(value);}};
|
||||
struct Terrain {static std::vector<vector> points;static int invalidAt;static float invalidValue;static float GetTerrainY(vector point){points.push_back(point);if(int(points.size())==invalidAt)return invalidValue;return point[0]*.001f+point[2]*.002f;}};std::vector<vector> Terrain::points;int Terrain::invalidAt=0;float Terrain::invalidValue=NAN;
|
||||
struct Actors {static int groundCalls;static vector Ground(vector p){groundCalls++;p[1]=28.5f;return p;}};int Actors::groundCalls=0;
|
||||
struct Envelope {static bool permitted;static int calls;static bool Survey(vector from,vector to){calls++;assert(std::abs(from[0]-4890)<.01f&&std::abs(from[2]-11730)<.01f&&to[2]-from[2]==100);return permitted;}};bool Envelope::permitted=false;int Envelope::calls=0;
|
||||
struct Probe {bool m_TerrainAudited=false,m_Stopped=false,m_AllocationRequested=false,m_Aircraft=false,m_Group=false,m_Pilot=false;vector m_Destination;
|
||||
'''+ '\n'.join(audit_cpp(body(P,key)) for key in ('static bool Enabled(', 'static bool TerrainAuditEnabled(', 'protected void AuditTerrain(', 'protected bool FindPad('))+r'''
|
||||
};
|
||||
void reset(){System::args={{"errSmoke","1"},{"errSmokeOutcome","aircraft-field"},{"errSmokeFieldTerrainAudit","1"}};logs.clear();Terrain::points.clear();Terrain::invalidAt=0;Terrain::invalidValue=NAN;Actors::groundCalls=0;Envelope::calls=0;Envelope::permitted=false;Replication::server=true;gamePresent=true;game.world=true;}
|
||||
int main(){
|
||||
reset();Probe p;vector unchanged={99,88,77};assert(!p.FindPad(unchanged));assert(unchanged[0]==99&&unchanged[1]==88&&unchanged[2]==77&&p.m_Destination[0]==0);
|
||||
assert(Envelope::calls==1&&Actors::groundCalls==2&&Terrain::points.size()==1681&&logs.size()==1683&&p.m_TerrainAudited&&!p.m_AllocationRequested&&!p.m_Aircraft&&!p.m_Group&&!p.m_Pilot);
|
||||
assert(logs.front().find("minX=4750 maxX=5150 minZ=11600 maxZ=12000 spacing=10 columns=41 rows=41 nodes=1681")!=string::npos);
|
||||
assert(logs.back().find("original survey refusal unchanged; no candidate qualified")!=string::npos);
|
||||
for(int i=0;i<1681;i++){
|
||||
auto q=Terrain::points[i];assert(q[0]==4750+(i/41)*10&&q[1]==0&&q[2]==11600+(i%41)*10);
|
||||
std::ostringstream expected;expected<<'|'<<i<<' '<<q[0]<<' '<<q[2]<<' '<<(q[0]*.001f+q[2]*.002f)<<' ';
|
||||
assert(logs[i+1].find("ERR AIRCRAFT FIELD TERRAIN AUDIT POINT:")==0&&logs[i+1].find(expected.str())!=string::npos);
|
||||
}
|
||||
p.AuditTerrain();assert(Terrain::points.size()==1681&&logs.size()==1683); // no retry, even if directly reentered
|
||||
// Neither CLI omission nor malformed/conflicting scope permits collection.
|
||||
for(int fault=0;fault<13;fault++){
|
||||
reset();Probe refused;
|
||||
if(fault==0)System::args.erase("errSmokeFieldTerrainAudit");
|
||||
if(fault==1)System::args["errSmokeFieldTerrainAudit"]="0";
|
||||
if(fault==2)System::args["errSmoke"]="0";
|
||||
if(fault==3)System::args["errSmokeOutcome"]="aircraft-route";
|
||||
if(fault==4)Replication::server=false;
|
||||
if(fault==5)System::args["errSmokeFieldServiceCancel"]="1";
|
||||
if(fault==6)refused.m_Stopped=true;
|
||||
if(fault==7)refused.m_AllocationRequested=true;
|
||||
if(fault==8)refused.m_Aircraft=true;
|
||||
if(fault==9)refused.m_Group=true;
|
||||
if(fault==10)refused.m_Pilot=true;
|
||||
if(fault==11)gamePresent=false;
|
||||
if(fault==12)game.world=false;
|
||||
assert(!refused.FindPad(unchanged));assert(Terrain::points.empty()&&logs.empty()&&!refused.m_TerrainAudited&&Envelope::calls==1);
|
||||
}
|
||||
// Successful original survey never runs the optional audit or changes its result.
|
||||
reset();Envelope::permitted=true;Probe success;assert(success.FindPad(unchanged));assert(Terrain::points.empty()&&!success.m_TerrainAudited&&Envelope::calls==1&&success.m_Destination[2]-unchanged[2]==100);
|
||||
// Malformed heights stop without retry, completion, candidate selection or allocation.
|
||||
for(float invalid:{float(NAN),float(INFINITY),float(-INFINITY),1000001.0f}){
|
||||
reset();Terrain::invalidAt=17;Terrain::invalidValue=invalid;Probe malformed;assert(!malformed.FindPad(unchanged));
|
||||
assert(Terrain::points.size()==17&&logs.size()==18&&logs.back().find("AUDIT ABORT:")!=string::npos&&malformed.m_TerrainAudited&&!malformed.m_AllocationRequested);
|
||||
malformed.AuditTerrain();assert(Terrain::points.size()==17);
|
||||
}
|
||||
}
|
||||
''')
|
||||
audit=body(P,'protected void AuditTerrain(')
|
||||
for forbidden in ('Spawn(', 'SetOrigin(', 'SetActive(', 'TraceMove(', '.Survey(', '.Qualify', 'new ', 'GetEntities', 'm_Destination ='):
|
||||
assert forbidden not in audit
|
||||
assert audit.count('GetTerrainY(')==1
|
||||
assert find.index('.Survey(')<find.index('AuditTerrain();')<find.index('return false;')<find.index('result = candidate')
|
||||
print('PASS: actual opt-in terrain audit records one fixed1681-point grid, preserves original survey refusal/success and exact no-allocation scope; malformed heights stop without retry; no native data acquired')
|
||||
|
||||
@@ -9,6 +9,7 @@ cpp = cpp.rstrip() + ';'
|
||||
run(r'''#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
struct vector {
|
||||
float v[3]={0,0,0};
|
||||
vector()=default; vector(float x,float y,float z):v{x,y,z}{}
|
||||
@@ -46,17 +47,6 @@ int main(){
|
||||
vector sideways=Rules::RampReference({2,0,0},{0,0,2},.1f);
|
||||
assert((sideways-vector{2,0,0}).Length()<=.020001f);
|
||||
assert(close(Rules::RampReference({1,4,0},{-2,0,0},0)[0],1));
|
||||
assert(close(Rules::ApproachSpeed(100,2),2)&&close(Rules::ApproachSpeed(.75f,2),0));
|
||||
assert(close(Rules::ApproachSpeed(3,2),0));
|
||||
assert(close(Rules::ApproachSpeed(Rules::BrakingReserve(1.5f),2),1.5f));
|
||||
assert(Rules::ApproachSpeed(18,2)<2); // before the2m/s reserve, unlike old sqrt(distance-.75)
|
||||
for(int i=3;i<=120;i++){
|
||||
float admitted=Rules::ApproachSpeed(float(i),2);
|
||||
assert(admitted<=2&&Rules::BrakingReserve(admitted)<=i+.00002f);
|
||||
}
|
||||
assert(Rules::ApproachSpeed(15,1.2f)<=1.2f);
|
||||
assert(Rules::ApproachSpeed(10,.1f)<=.1f&&Rules::ApproachSpeed(NAN,1)==0);
|
||||
assert(close(Rules::ApproachSpeed(108,2),2)&&Rules::ApproachSpeed(120.01f,2)==0);
|
||||
assert(!Rules::ShouldBrake(100.23f,1)&&!Rules::ShouldBrake(108,2)&&!Rules::ShouldBrake(120,2));
|
||||
assert(Rules::ShouldBrake(120.01f,1)&&!Rules::Length(108));
|
||||
vector integral, next;
|
||||
@@ -78,7 +68,21 @@ int main(){
|
||||
demand=Rules::VelocityDemand({NAN,0,0},{},{1,0,0},.1f,next);assert(demand.Length()==0&&next.Length()==0);
|
||||
demand=Rules::VelocityDemand({1,0,0},{},{1,0,0},0,next);assert(demand.Length()==0&&next.Length()==0);
|
||||
assert(close(Rules::BrakingReserve(0),3)&&close(Rules::BrakingReserve(2.5f),26.333334f));
|
||||
assert(Rules::ShouldBrake(25,0)&&!Rules::ShouldBrake(25.01f,0));
|
||||
assert(Rules::ShouldBrake(3,0)&&!Rules::ShouldBrake(3.001f,0)&&!Rules::ShouldBrake(25,0));
|
||||
for(float actualSpeed:{0.0f,.1f,.25f,1.0f,1.5f,2.0f,2.5f}){
|
||||
float reserve=Rules::BrakingReserve(actualSpeed);
|
||||
assert(Rules::ShouldBrake(reserve,actualSpeed)&&!Rules::ShouldBrake(reserve+.001f,actualSpeed));
|
||||
}
|
||||
// Stop reference never chases the destination or reverses direction.
|
||||
for(vector initial:{vector{2,0,0},vector{-1.2f,0,1.6f},vector{0,0,0}}){
|
||||
vector v=initial;
|
||||
for(int i=0;i<110;i++){
|
||||
vector nextV=Rules::RampReference(v,{},.1f);
|
||||
assert(nextV.Length()<=v.Length()+.000001f&&(nextV-v).Length()<=.020001f);
|
||||
assert(vector::Dot(nextV,initial)>=-.000001f);v=nextV;
|
||||
}
|
||||
assert(v.Length()<=.00001f);
|
||||
}
|
||||
assert(Rules::ShouldBrake(26,2.5f)&&!Rules::ShouldBrake(27,2.5f));
|
||||
assert(Rules::ShouldBrake(50,NAN)&&Rules::ShouldBrake(NAN,1));
|
||||
assert(Rules::LeaseAdmitted(1000,241000)&&Rules::LeaseAdmitted(1000,240000));
|
||||
|
||||
@@ -11,6 +11,9 @@ RUNNER=ROOT/'tools/headless_workbench.py'
|
||||
class FieldRunnerTests(unittest.TestCase):
|
||||
def test_early_refusal_before_any_profile_or_lock(self):
|
||||
cases=[
|
||||
(['--project','chat','--rounds','1','--timeout','420','--smoke','aircraft-route','--field-terrain-audit'],'--field-terrain-audit requires'),
|
||||
(['--project','chat','--rounds','1','--timeout','420','--smoke','aircraft-field','--field-terrain-audit','--field-service-cancel'],'mutually exclusive'),
|
||||
(['--project','chat','--rounds','1','--timeout','420','--smoke','aircraft-field','--field-terrain-audit'],'must be a disposable cloned prefix'),
|
||||
(['--project','core','--rounds','1','--timeout','420','--smoke','aircraft-field'],'requires Chat, one round'),
|
||||
(['--project','chat','--rounds','2','--timeout','420','--smoke','aircraft-field'],'requires Chat, one round'),
|
||||
(['--project','chat','--rounds','1','--timeout','419','--smoke','aircraft-field'],'requires Chat, one round'),
|
||||
@@ -24,13 +27,13 @@ class FieldRunnerTests(unittest.TestCase):
|
||||
root=Path(folder)
|
||||
result=subprocess.run([sys.executable,str(RUNNER),'--compat',str(root/'missing'),'--profile',str(root/'profile'),*arguments],capture_output=True,text=True,timeout=10)
|
||||
self.assertEqual(result.returncode,2);self.assertIn(error,result.stderr);self.assertEqual(list(root.iterdir()),[])
|
||||
def command(self,smoke,cancel=False):
|
||||
def command(self,smoke,cancel=False,audit=False):
|
||||
tree=ast.parse(RUNNER.read_text());nodes=[]
|
||||
for node in tree.body:
|
||||
if isinstance(node,ast.Assign) and any(isinstance(t,ast.Name) and t.id=='command' for t in node.targets):nodes.append(node)
|
||||
if isinstance(node,ast.If) and isinstance(node.test,ast.Attribute) and node.test.attr in ('field_service_cancel','route_service_cancel'):nodes.append(node)
|
||||
self.assertEqual(len(nodes),3)
|
||||
namespace={'a':SimpleNamespace(project='chat',smoke=smoke,field_service_cancel=cancel,route_service_cancel=False),'ROOT':Path('/private/source'),'proton':Path('/private/proton'),'wb':Path('/private/workbench'),'profile_arg':'Z:/private/profile'}
|
||||
if isinstance(node,ast.If) and isinstance(node.test,ast.Attribute) and node.test.attr in ('field_service_cancel','route_service_cancel','field_terrain_audit'):nodes.append(node)
|
||||
self.assertEqual(len(nodes),4)
|
||||
namespace={'a':SimpleNamespace(project='chat',smoke=smoke,field_service_cancel=cancel,route_service_cancel=False,field_terrain_audit=audit),'ROOT':Path('/private/source'),'proton':Path('/private/proton'),'wb':Path('/private/workbench'),'profile_arg':'Z:/private/profile'}
|
||||
exec(compile(ast.Module(body=nodes,type_ignores=[]),str(RUNNER),'exec'),namespace)
|
||||
return namespace['command']
|
||||
def test_exact_field_command_and_cancel_optin(self):
|
||||
@@ -41,6 +44,11 @@ class FieldRunnerTests(unittest.TestCase):
|
||||
self.assertEqual(normal[normal.index('--backend')+1],'headless')
|
||||
self.assertIn('-disableCrashReporter',normal)
|
||||
self.assertNotIn('-errSmokeFieldServiceCancel',self.command('aircraft-route'))
|
||||
def test_terrain_audit_adds_only_explicit_flag(self):
|
||||
normal=self.command('aircraft-field')
|
||||
self.assertEqual(self.command('aircraft-field',audit=True),normal+['-errSmokeFieldTerrainAudit','1'])
|
||||
self.assertNotIn('-errSmokeFieldTerrainAudit',normal)
|
||||
self.assertNotIn('-errSmokeFieldTerrainAudit',self.command('aircraft-route'))
|
||||
def test_cleanup_provenance_not_bypassed(self):
|
||||
source=RUNNER.read_text()
|
||||
self.assertIn('assert_prefix_idle(compat)',source)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Execute production feedback control flow; no native UI or network claim."""
|
||||
from policy_harness import ROOT, body, run
|
||||
service = ROOT/'addons/core/Scripts/Game/ERR/ERR_CasualtyLoadingService.c'
|
||||
action = ROOT/'addons/core/Scripts/Game/ERR/ERR_LoadCasualtyAction.c'
|
||||
feedback = ROOT/'addons/core/Scripts/Game/ERR/ERR_CasualtyLoadingFeedback.c'
|
||||
def convert(s):
|
||||
s=s.replace('protected ', '').replace('override ', '').replace('string.Empty','string{}').replace('.IsEmpty()', '.empty()')
|
||||
for ns in ['Replication','System','ERR_CasualtyFeedback','ERR_Phase','ERR_CasualtyLoadingRules']:
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
s=s.replace('ERR_GameMode.Get()', 'Mode::Get()').replace('ERR_GameMode mode', 'Mode* mode').replace('ERR_GameMode.Cast','Mode::Cast')
|
||||
s=s.replace('ERR_PlayerLife life','Life* life').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId));','Controller* controller = GetGame()->GetPlayerManager()->GetPlayerController(life->ConnectionId);')
|
||||
s=s.replace('ERR_CasualtyLoadTicket ticket','Ticket* ticket')
|
||||
for v in ['mode','m_Mode','ticket','life','controller']:
|
||||
s=s.replace(v+'.',v+'->')
|
||||
s=s.replace('mode->CasualtyLoading().','mode->CasualtyLoading()->').replace('null','nullptr')
|
||||
return s
|
||||
methods='\n'.join(convert(body(service, sig)) for sig in ['string RejectionReason(', 'protected void NotifyCaller(', 'void Request(', 'protected void NotifyResolution('])
|
||||
permission=convert(body(action,'override bool CanBePerformedScript('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct Actor{int identity=1;};using IEntity=Actor*;
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace System{int GetTickCount(){return 10000;}}
|
||||
namespace ERR_Phase{enum{EXTRACTION=4};}
|
||||
struct Mode;struct Service;
|
||||
struct Life{int ConnectionId=1;};
|
||||
struct Controller{IEntity actor=nullptr;int messages=0;string last;IEntity GetControlledEntity(){return actor;}void ERR_CasualtyFeedback(Mode*,IEntity a,string s){assert(a==actor);messages++;last=s;}};
|
||||
struct Manager{Controller* current=nullptr;Controller* GetPlayerController(int){return current;}};
|
||||
struct Game{Manager manager;Manager* GetPlayerManager(){return &manager;}} game;Game* GetGame(){return &game;}
|
||||
struct Mode{bool active=true;int phase=4;Service* service=nullptr;static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}Service* CasualtyLoading(){return service;}int ExtractionVehicle(){return 1;}};Mode* Mode::current=nullptr;
|
||||
namespace ERR_CasualtyFeedback{string physical;string PhysicalReason(Mode*,IEntity,IEntity){return physical;}}
|
||||
struct Ticket{bool FeedbackSubmitted=false;IEntity Caller=nullptr;int CallerAccount=1,CallerConnection=1;};
|
||||
struct Service{
|
||||
Mode* m_Mode;bool m_Stopped=false,settled=true,pending=false,seat=true,beginOK=true,same=true;int begins=0;Life life;IEntity admittedCaller=nullptr,admittedCasualty=nullptr;
|
||||
Life* CurrentLife(IEntity a){return a&&(a==admittedCaller||a==admittedCasualty)?&life:nullptr;}
|
||||
bool Settled(IEntity,int){return settled;}bool PendingActor(IEntity,IEntity){return pending;}bool AvailableSeat(IEntity,int){return seat;}
|
||||
bool Begin(IEntity,IEntity){begins++;return beginOK;}bool SameLife(IEntity a,int account,int connection){return same&&CurrentLife(a)&&account==1&&connection==1;}
|
||||
''' + methods + r'''
|
||||
};
|
||||
struct Action{string reason="old";IEntity target=nullptr;IEntity GetOwner(){return target;}void SetCannotPerformReason(string s){reason=s;}
|
||||
''' + permission + r'''
|
||||
};
|
||||
int main(){
|
||||
Mode mode;Service svc{&mode};mode.service=&svc;Mode::current=&mode;Actor caller,patient,stranger;svc.admittedCaller=&caller;svc.admittedCasualty=&patient;Controller controller;controller.actor=&caller;game.manager.current=&controller;Action action;action.target=&patient;
|
||||
assert(action.CanBePerformedScript(&caller)&&action.reason.empty());
|
||||
ERR_CasualtyFeedback::physical="No accessible transport seat is available";
|
||||
assert(!action.CanBePerformedScript(&caller)&&action.reason==ERR_CasualtyFeedback::physical);
|
||||
svc.Request(&caller,&patient);assert(svc.begins==0&&controller.messages==1);
|
||||
ERR_CasualtyFeedback::physical.clear();assert(action.CanBePerformedScript(&caller)&&action.reason.empty());
|
||||
svc.settled=false;assert(!action.CanBePerformedScript(&caller)&&action.reason.find("five seconds")!=string::npos);svc.settled=true;
|
||||
svc.pending=true;assert(svc.RejectionReason(&caller,&patient).find("already")!=string::npos);svc.pending=false;
|
||||
svc.seat=false;assert(svc.RejectionReason(&caller,&patient).find("seat")!=string::npos);svc.seat=true;
|
||||
assert(svc.RejectionReason(&stranger,&patient).find("connected")!=string::npos);
|
||||
int before=controller.messages;svc.Request(&stranger,&patient);assert(controller.messages==before&&svc.begins==0);
|
||||
svc.beginOK=false;svc.Request(&caller,&patient);assert(svc.begins==1&&controller.messages==before+1&&controller.last.find("could not start")!=string::npos);
|
||||
svc.beginOK=true;before=controller.messages;svc.Request(&caller,&patient);assert(svc.begins==2&&controller.messages==before);
|
||||
Ticket ticket;ticket.Caller=&caller;
|
||||
svc.NotifyResolution(&ticket,false);assert(controller.messages==before); // failed reservation before submission: Request gives exactly one failure
|
||||
ticket.FeedbackSubmitted=true;svc.NotifyResolution(&ticket,true);assert(controller.messages==before);
|
||||
svc.NotifyResolution(&ticket,false);assert(controller.messages==before+1&&controller.last.find("could not be confirmed")!=string::npos);
|
||||
before=controller.messages;
|
||||
svc.same=false;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);svc.same=true;
|
||||
ticket.CallerAccount=2;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);ticket.CallerAccount=1;
|
||||
ticket.CallerConnection=2;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);ticket.CallerConnection=1;
|
||||
controller.actor=&stranger;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);controller.actor=&caller;
|
||||
svc.m_Stopped=true;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);svc.m_Stopped=false;
|
||||
mode.active=false;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);mode.active=true;
|
||||
mode.phase=5;svc.NotifyResolution(&ticket,false);assert(controller.messages==before);mode.phase=4;
|
||||
Replication::server=false;svc.Request(&caller,&patient);assert(svc.begins==2&&controller.messages==before);
|
||||
// Client presentation never evaluates authoritative roster/settling: physical facts only.
|
||||
svc.settled=false;assert(action.CanBePerformedScript(&caller)&&action.reason.empty());
|
||||
ERR_CasualtyFeedback::physical="The helicopter must be upright, grounded and stationary";assert(!action.CanBePerformedScript(&caller));
|
||||
}
|
||||
''')
|
||||
# Guard the unchanged transfer path and exact recipient channel around the tested logic.
|
||||
owner=feedback.read_text()
|
||||
assert '[RplRpc(RplChannel.Reliable, RplRcver.Owner)]' in owner
|
||||
assert 'RplRcver.Broadcast' not in owner and 'RplRcver.Server' not in owner
|
||||
send=body(feedback,'void ERR_CasualtyFeedback(')
|
||||
for guard in ['!Replication.IsServer()', 'mode != ERR_GameMode.Get()', '!mode.Players().CurrentActor(actor)', 'GetPlayerController(GetPlayerId()) != this']:
|
||||
assert guard in send
|
||||
receive=body(feedback,'protected void ERR_CasualtyFeedbackOwner(')
|
||||
for guard in ['this != GetGame().GetPlayerController()', 'EntityId(mode) != modeId', 'EntityId(GetControlledEntity()) != actorId', 'mode.Phase() != ERR_Phase.EXTRACTION']:
|
||||
assert guard in receive
|
||||
assert receive.index('EntityId(GetControlledEntity()) != actorId') < receive.index('ShowCustom(')
|
||||
resolve=body(service,'protected void Resolve(')
|
||||
assert resolve.index('m_Pending.Remove(index)') < resolve.index('NotifyResolution(ticket, success)') < resolve.index('m_OnResolved.Invoke')
|
||||
begin=body(service,'bool Begin(')
|
||||
assert begin.index('IsReservedBy(casualty)') < begin.index('ticket.FeedbackSubmitted = true') < begin.index('ERR_PrepareCasualtyLoad(')
|
||||
print('PASS: actual action denial/clearing, server request failure, single submitted-result feedback, current rescuer identity and terminal/stop suppression; native UI/RPC pending')
|
||||
physical=convert(body(feedback,'static string PhysicalReason(')).replace('static ','')
|
||||
physical=physical.replace('Vehicle aircraft','Actor* aircraft').replace('aircraft.FindComponent(BaseCompartmentManagerComponent)','0')
|
||||
physical=physical.replace('BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(0);','Manager* manager = aircraft->manager;')
|
||||
physical=physical.replace('array<BaseCompartmentSlot> seats = {}','std::vector<Seat*> seats').replace('foreach (BaseCompartmentSlot seat : seats)','for (Seat* seat : seats)')
|
||||
for var in ['caller','casualty','aircraft','manager','seat']:
|
||||
physical=physical.replace(var+'.',var+'->')
|
||||
physical=physical.replace('vector.Distance','Distance').replace('ERR_Actors.','ERR_Actors::').replace('CompartmentAccessComponent.','CompartmentAccessComponent::')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using string=std::string;
|
||||
struct Actor;using IEntity=Actor*;
|
||||
struct Seat{bool transport=true,occupied=false,reserved=false,accessible=true,locked=false;bool GetOccupant(){return occupied;}bool IsReserved(){return reserved;}bool IsCompartmentAccessible(){return accessible;}bool IsGetInLockedFor(IEntity){return locked;}};
|
||||
struct Manager{std::vector<Seat*> seats;void GetCompartments(std::vector<Seat*>& out){out=seats;}};
|
||||
struct Actor{bool conscious=true,aboard=false,settled=true,ready=true;Manager* manager=nullptr;int GetOrigin(){return 0;}};
|
||||
struct Mode{bool active=true;int phase=4;Actor* aircraft=nullptr;bool Active(){return active;}int Phase(){return phase;}Actor* CasualtyLoadingAircraft(){return aircraft;}};
|
||||
namespace ERR_Phase{enum{EXTRACTION=4};}
|
||||
namespace ERR_Actors{bool CanFight(IEntity a){return a&&a->conscious;}}
|
||||
namespace CompartmentAccessComponent{bool GetVehicleIn(IEntity a){return a->aboard;}}
|
||||
float Distance(int,int){return 0;}
|
||||
namespace ERR_CasualtyLoadingRules{
|
||||
bool ranges=true,nearby=true;
|
||||
bool GroundSettled(IEntity a){return a&&a->settled;}
|
||||
bool Ranges(float,float,float){return ranges;}
|
||||
bool AircraftReady(IEntity a){return a&&a->ready;}
|
||||
bool Nearby(Mode*,IEntity,IEntity){return nearby;}
|
||||
bool TransportBench(Seat* s,IEntity){return s&&s->transport;}
|
||||
}
|
||||
''' + physical + r'''
|
||||
int main(){
|
||||
Actor caller,patient,aircraft;Mode mode;mode.aircraft=&aircraft;Manager manager;aircraft.manager=&manager;Seat seat;manager.seats={nullptr,&seat};
|
||||
assert(PhysicalReason(&mode,&caller,&patient).empty());
|
||||
for(int mask=1;mask<16;mask++){
|
||||
seat.occupied=mask&1;seat.reserved=mask&2;seat.accessible=!(mask&4);seat.locked=mask&8;
|
||||
assert(PhysicalReason(&mode,&caller,&patient).find("seat")!=string::npos);
|
||||
}
|
||||
seat=Seat{};seat.transport=false;assert(PhysicalReason(&mode,&caller,&patient).find("seat")!=string::npos);seat.transport=true;
|
||||
aircraft.manager=nullptr;assert(PhysicalReason(&mode,&caller,&patient).find("seat")!=string::npos);aircraft.manager=&manager;
|
||||
mode.active=false;assert(PhysicalReason(&mode,&caller,&patient).find("Wait")!=string::npos);mode.active=true;
|
||||
mode.phase=3;assert(!PhysicalReason(&mode,&caller,&patient).empty());mode.phase=4;
|
||||
assert(!PhysicalReason(nullptr,&caller,&patient).empty());assert(!PhysicalReason(&mode,nullptr,&patient).empty());assert(!PhysicalReason(&mode,&patient,&patient).empty());
|
||||
caller.conscious=false;assert(PhysicalReason(&mode,&caller,&patient).find("conscious rescuer")!=string::npos);caller.conscious=true;
|
||||
caller.aboard=true;assert(PhysicalReason(&mode,&caller,&patient).find("Leave")!=string::npos);caller.aboard=false;
|
||||
patient.settled=false;assert(PhysicalReason(&mode,&caller,&patient).find("stable ground")!=string::npos);patient.settled=true;
|
||||
ERR_CasualtyLoadingRules::ranges=false;assert(PhysicalReason(&mode,&caller,&patient).find("twelve metres")!=string::npos);ERR_CasualtyLoadingRules::ranges=true;
|
||||
aircraft.ready=false;assert(PhysicalReason(&mode,&caller,&patient).find("stationary")!=string::npos);aircraft.ready=true;
|
||||
ERR_CasualtyLoadingRules::nearby=false;assert(PhysicalReason(&mode,&caller,&patient).find("clear access")!=string::npos);ERR_CasualtyLoadingRules::nearby=true;
|
||||
assert(PhysicalReason(&mode,&caller,&patient).empty());
|
||||
}
|
||||
''')
|
||||
print('PASS: actual physical denial precedence and transport-seat occupancy/reservation/access/lock checks')
|
||||
@@ -103,6 +103,7 @@ struct Service {
|
||||
Mode* m_Mode;bool m_Stopped=false,m_MaintenanceQueued=false;
|
||||
Array<Ticket*> m_Pending;Array<int> m_Settling;Invoker* m_OnResolved;
|
||||
bool PhysicalTicket(Ticket* ticket){return ticket->Casualty&&ticket->Aircraft&&ticket->Seat;}
|
||||
void NotifyResolution(Ticket*,bool){} // presentation boundary covered by test_casualty_feedback.py
|
||||
bool ValidTicket(Ticket*){return false;} // disconnected participants cannot authorize again
|
||||
''' + methods + '''
|
||||
void Add(Ticket* ticket){m_Pending.Insert(ticket);ticket->Seat->SetReserved(ticket->Casualty);ScheduleMaintenance();}
|
||||
|
||||
@@ -55,6 +55,7 @@ struct Mode {
|
||||
Work* m_Extraction=&extraction;Work* m_RadioDefence=&radio;Work* m_Recovery=&recovery;
|
||||
Command* m_Command=nullptr;Roster* m_Roster=&roster;
|
||||
Hook m_OnTick{0,{},"event"},m_OnStop{0,{},"onstop"};std::vector<Entity*> m_OwnedEntities;
|
||||
bool InitialAdmissionsExhausted(){return false;}
|
||||
World* GetWorld(){return &world;}bool RefreshRadioFeedback(){return false;}
|
||||
void Transition(int phase){m_Phase=phase;StopServices();}
|
||||
'''+cpp(body(MODE,'bool Active('))+cpp(body(MODE,'protected void ServerTick('))+cpp(body(MODE,'protected void StopServices('))+r'''
|
||||
|
||||
@@ -1124,7 +1124,9 @@ class SelectionIntegration(unittest.TestCase):
|
||||
mouse=SimpleNamespace(xt=r.xt,query=r.query,keyboard=SimpleNamespace(require_idle=scene.idle),validate_pointer=position,
|
||||
alignment_move=lambda dx,dy,guard,deadline,clock:scene.move(dx,dy,guard,deadline=deadline))
|
||||
notices=[]
|
||||
def facing_evidence(*args):return {'verified':True,'last_sample':report['facing_proof']['final_sample']}
|
||||
def facing_evidence(*args, profile='standard'):
|
||||
self.assertEqual(profile, 'standard')
|
||||
return {'verified':True,'last_sample':report['facing_proof']['final_sample']}
|
||||
with patch('prison_approach_evidence.validate_approach_evidence',return_value=scene.accepted),patch('prison_alignment_evidence.validate_facing_evidence',side_effect=facing_evidence),patch.object(proof,'notify',side_effect=notices.append):
|
||||
proof.run_selection(scene,SimpleNamespace(require_idle=scene.idle),o,mouse,report,scene.clock,scene.sleep)
|
||||
self.assertEqual(calls,[(5,1),(5,0)]);self.assertTrue(report['facing_proof']['completed']);self.assertTrue(report['action_selection_proof']['completed'])
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Actual admission-exhaustion/tick rules; engine population and services substituted."""
|
||||
import re
|
||||
from policy_harness import ROOT, body, run
|
||||
MODE = ROOT/'addons/core/Scripts/Game/ERR/ERR_GameMode.c'
|
||||
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/ERR_SpawnLogic.c'
|
||||
def cpp(s):
|
||||
s=s.replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
|
||||
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('connected.IsEmpty()', 'connected.empty()')
|
||||
s=s.replace('foreach (int id : connected)', 'for (int id : connected)')
|
||||
s=s.replace('ERR_PlayerLife life', 'Life* life').replace('SCR_PlayerController controller', 'Controller* controller').replace('PlayerController controller', 'Controller* controller')
|
||||
s=s.replace('SCR_PlayerController.Cast(', 'Controller::Cast(').replace('ERR_Phase.DEFEAT','5')
|
||||
for v in ('m_Roster','controller','life','m_Command','m_Extraction','m_RadioDefence','m_Recovery','GetGame()','GetWorld()','GetPlayerManager()'):
|
||||
s=s.replace(v+'.', v+'->')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(MODE,x)) for x in ('void InitialAdmissionStarted(', 'void InitialAdmissionExhausted(', 'protected bool InitialAdmissionsExhausted(', 'protected void ServerTick('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
bool server=true;
|
||||
struct Controller {void* actor=nullptr;void* GetControlledEntity(){return actor;}static Controller* Cast(Controller* c){return c;}void ERR_UpdateObserverServer(){}};
|
||||
struct Life {bool Connected=true,UsedLife=false;void* Actor=nullptr;};
|
||||
struct Roster {std::map<int,Life> lives;bool survivor=false;Life* Get(int id){auto i=lives.find(id);return i==lives.end()?nullptr:&i->second;}bool HasSurvivor(){return survivor;}};
|
||||
struct Players {std::map<int,Controller*> connected;void GetPlayers(std::vector<int>& v){for(auto p:connected)v.push_back(p.first);}int GetPlayerCount(){return connected.size();}Controller* GetPlayerController(int id){auto i=connected.find(id);return i==connected.end()?nullptr:i->second;}};
|
||||
struct Game {Players players;Players* GetPlayerManager(){return &players;}} game;Game* GetGame(){return &game;}
|
||||
struct World {float now=0;float GetWorldTime(){return now;}} world;
|
||||
struct Service {void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
|
||||
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
|
||||
struct Mode {
|
||||
bool m_StartedPlayers=false;int m_Phase=1;float m_NoSurvivorsSince=-1;std::string m_EndReason;
|
||||
Exhausted m_ExhaustedInitialAdmissions;Roster roster;Roster* m_Roster=&roster;
|
||||
Service service,m_OnTick;Service *m_Extraction=&service,*m_RadioDefence=&service,*m_Recovery=&service,*m_Command=&service;
|
||||
World* GetWorld(){return &world;}bool Active(){return m_Phase>0&&m_Phase<5;}void Transition(int p){m_Phase=p;}bool RefreshRadioFeedback(){return false;}
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
||||
int main(){
|
||||
// Sole exhausted first admission reaches existing grace, without a life/body.
|
||||
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
||||
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
||||
assert(f.m.m_EndReason=="No prisoners could be admitted to this round.");}
|
||||
// Not-yet-audited arrivals block the timer before they have any roster entry.
|
||||
{Fixture f;f.exhaust();f.tick(0);game.players.connected[2]=&f.second;f.tick(29000);
|
||||
assert(f.m.m_NoSurvivorsSince==-1);f.m.roster.lives[2]=Life();f.tick(60000);assert(f.m.Active());
|
||||
f.m.InitialAdmissionExhausted(2,&f.second);f.tick(61000);f.tick(90999);assert(f.m.Active());f.tick(91000);assert(!f.m.Active());}
|
||||
// A retry/pending new attempt clears exhaustion immediately, not one tick later.
|
||||
{Fixture f;f.exhaust();f.tick(0);f.m.InitialAdmissionStarted(1);assert(f.m.m_NoSurvivorsSince==-1);
|
||||
f.tick(90000);assert(f.m.Active()&&!f.m.InitialAdmissionsExhausted());
|
||||
f.exhaust();f.tick(91000);f.tick(120999);assert(f.m.Active());f.tick(121000);assert(!f.m.Active());}
|
||||
// Empty population and disconnect cleanup erase the initial failure grace.
|
||||
{Fixture f;f.exhaust();f.tick(0);game.players.connected.clear();f.tick(30000);assert(f.m.m_NoSurvivorsSince==-1&&f.m.Active());
|
||||
f.m.InitialAdmissionStarted(1);game.players.connected[1]=&f.first;f.tick(60000);assert(f.m.Active());}
|
||||
// Recycled connection/controller, live body and invalid roster never inherit it.
|
||||
for(int fault=0;fault<5;fault++){Fixture f;f.exhaust();f.tick(0);
|
||||
if(fault==0)game.players.connected[1]=&f.second;
|
||||
if(fault==1)f.first.actor=&f.first;
|
||||
if(fault==2)f.m.roster.lives[1].UsedLife=true;
|
||||
if(fault==3)f.m.roster.lives[1].Actor=&f.first;
|
||||
if(fault==4)f.m.roster.lives[1].Connected=false;
|
||||
f.tick(30000);assert(f.m.Active()&&f.m.m_NoSurvivorsSince==-1);}
|
||||
// Invalid phase, authority, prior participation or controller cannot record failure.
|
||||
for(int fault=0;fault<6;fault++){Fixture f;
|
||||
if(fault==0){server=false;}if(fault==1){f.m.m_Phase=0;}if(fault==2){f.m.m_Phase=5;}
|
||||
if(fault==3){f.m.m_StartedPlayers=true;}if(fault==4){game.players.connected[1]=&f.second;}if(fault==5){f.m.roster.lives.clear();}
|
||||
f.exhaust();assert(f.m.m_ExhaustedInitialAdmissions.values.empty());}
|
||||
// Existing actual first spawn changes participation; normal death grace is unchanged.
|
||||
{Fixture f;f.exhaust();f.m.InitialAdmissionStarted(1);f.m.m_StartedPlayers=true;f.m.roster.lives[1].UsedLife=true;f.m.roster.survivor=true;
|
||||
f.tick(30000);assert(f.m.m_NoSurvivorsSince==-1&&f.m.Active());f.m.roster.survivor=false;f.tick(31000);f.tick(61000);
|
||||
assert(f.m.m_Phase==5&&f.m.m_EndReason=="No surviving prisoners remain.");}
|
||||
}
|
||||
''')
|
||||
# Tie the tested state seam to actual budget exhaustion and new/disconnected admission.
|
||||
do_spawn=body(SPAWN,'protected override void DoSpawn_S(')
|
||||
assert do_spawn.index('Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE);\n mode.InitialAdmissionExhausted')>do_spawn.index('if (attempts >= 30)')
|
||||
audit=body(SPAWN,'override void OnPlayerAuditSuccess_S(')
|
||||
assert audit.index('mode.InitialAdmissionStarted(playerId);')<audit.index('m_Attempts.Set(playerId, 0);')
|
||||
disconnect=body(SPAWN,'override void OnPlayerDisconnected_S(')
|
||||
assert 'mode.InitialAdmissionStarted(playerId);' in disconnect
|
||||
print('PASS: actual initial admission exhaustion and ServerTick grace; new/pending/empty/controller/life guards, normal death preserved')
|
||||
@@ -47,6 +47,64 @@ class ChoiceScene(Scene):
|
||||
|
||||
|
||||
class SelectionTests(unittest.TestCase):
|
||||
def aligned_pair(self, cls=ChoiceScene):
|
||||
scene=cls();scene.guard_cost=0
|
||||
scene.angle=math.degrees(math.atan2(scene.geometry['context'][0]-scene.camera[0],scene.geometry['context'][2]-scene.camera[2]))
|
||||
scene.snapshot=scene.make();scene.history=[copy.deepcopy(scene.snapshot)]
|
||||
scene.sleep(.1)
|
||||
return scene
|
||||
|
||||
def test_facing_commits_existing_fresh_pair_without_second_drain(self):
|
||||
scene=self.aligned_pair();reads=[];original=scene.observe
|
||||
def observe():reads.append(scene.clock());return original()
|
||||
scene.observe=observe
|
||||
result=scene.facing()
|
||||
self.assertTrue(result['completed']);self.assertEqual(len(reads),1)
|
||||
self.assertEqual(result['facing_samples'],[310,311])
|
||||
self.assertEqual([x['sample'] for x in result['samples']],[310,311])
|
||||
self.assertEqual(result['final_sample'],311)
|
||||
self.assertEqual(result['deadline_monotonic'],39)
|
||||
self.assertEqual(scene.motions,[]);self.assertEqual(scene.wheels,[])
|
||||
# The existing strict alignment path still performs its original reread.
|
||||
strict=self.aligned_pair(Scene);strict_reads=[];original=strict.observe
|
||||
def strict_observe():strict_reads.append(strict.clock());return original()
|
||||
strict.observe=strict_observe
|
||||
self.assertTrue(strict.run()['completed']);self.assertEqual(len(strict_reads),2)
|
||||
|
||||
def test_final_facing_idle_freshness_clock_and_deadline_rechecked(self):
|
||||
for fault in ('not-idle','stale','clock-reversed','deadline'):
|
||||
scene=self.aligned_pair();calls=[]
|
||||
if fault=='deadline':scene.calibration['alignment_deadline_monotonic']=31.15
|
||||
def idle():
|
||||
calls.append(scene.now)
|
||||
if len(calls)==2:
|
||||
if fault=='not-idle':raise alignment.Refused('native keys are not idle')
|
||||
if fault=='stale':scene.sleep(.51)
|
||||
if fault=='clock-reversed':scene.now-=.2
|
||||
if fault=='deadline':scene.sleep(.06)
|
||||
scene.idle=idle
|
||||
with self.subTest(fault=fault),self.assertRaises(alignment.Refused):scene.facing()
|
||||
self.assertEqual(len(calls),2);self.assertFalse(scene.report['facing_proof']['completed'])
|
||||
self.assertEqual(scene.motions,[]);self.assertEqual(scene.wheels,[])
|
||||
|
||||
def test_facing_complete_pair_does_not_hide_intermediate_native_reversal(self):
|
||||
scene=self.aligned_pair();scene.history[1]['real_ms']=scene.history[0]['real_ms']-1
|
||||
scene.history[1]['choices']['real_ms']=scene.history[1]['real_ms']
|
||||
with self.assertRaisesRegex(ValueError,'reversed'):scene.facing()
|
||||
self.assertEqual(scene.motions,[]);self.assertEqual(scene.wheels,[])
|
||||
self.assertFalse(scene.report['facing_proof']['completed'])
|
||||
|
||||
def test_selection_consumes_every_later_record_after_early_facing_commit(self):
|
||||
scene=self.aligned_pair();facing=scene.facing();self.assertEqual(facing['final_sample'],311)
|
||||
scene.sleep(.3)
|
||||
bad=next(x for x in scene.history if x['sample']==312)
|
||||
bad['pressed']=1
|
||||
# Later records are clean: latest-only selection would incorrectly pass.
|
||||
self.assertEqual(scene.history[-1]['pressed'],0)
|
||||
with self.assertRaisesRegex(ValueError,'Action began'):scene.select_action()
|
||||
self.assertEqual(scene.wheels,[])
|
||||
self.assertEqual(scene.report['action_selection_proof']['initial_sample'],311)
|
||||
|
||||
def test_actual_pose_facing_then_one_wheel_selects_with_same_lease(self):
|
||||
s=ChoiceScene();f=s.facing();r=s.select_action()
|
||||
self.assertTrue(f['completed']);self.assertNotIn('alignment_proof',s.report)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
"""Actual facing policy, capabilities, raw validators and runner; fake input only."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
|
||||
import prison_alignment_proof as policy
|
||||
import prison_alignment_evidence as evidence
|
||||
import prison_action_selection_evidence as selected_evidence
|
||||
import headless_input_proof as helper
|
||||
import headless_game as runner
|
||||
from test_prison_action_selection_proof import ChoiceScene
|
||||
from test_prison_action_selection_evidence import build,raw
|
||||
from test_prison_approach_proof import Scene as ApproachScene
|
||||
import test_headless_input_exercise_runner as runner_fixtures
|
||||
import test_relative_look_runner as selection_runner_fixtures
|
||||
|
||||
|
||||
def coarse_move(s,dx,dy,guard,*,deadline):
|
||||
if type(dx) is not int or not 1<=abs(dx)<=384 or dy!=0:raise AssertionError('coarse fake capability')
|
||||
guard.check();guard.check()
|
||||
if s.now>=deadline:raise policy.Refused('adapter deadline')
|
||||
s.motions.append((dx,dy,s.now,deadline))
|
||||
if not s.ignore:s.pending.append((s.now+s.delay,dx*s.gain*(-1 if s.reverse else 1)))
|
||||
|
||||
|
||||
def coarse(s):
|
||||
s.report.update(coarse_facing_requested=True,coarse_approach_requested=True,selection_requested=True)
|
||||
return policy.perform_facing(s,s.idle,s,s.observe,s.geometry,s.calibration,s.accepted,s.report,
|
||||
clock=s.clock,sleep=s.sleep,notify=s.notifications.append,profile='coarse')
|
||||
|
||||
|
||||
class FacingTests(unittest.TestCase):
|
||||
def scene(self):
|
||||
s=ChoiceScene();s.report['approach_proof']['profile']='coarse'
|
||||
s.move=lambda *a,**kw:coarse_move(s,*a,**kw)
|
||||
return s
|
||||
def test_coarse_reduces_requests_without_new_clock_or_total_budget(self):
|
||||
standard=ChoiceScene();ordinary=standard.facing()
|
||||
s=self.scene();result=coarse(s)
|
||||
self.assertTrue(result['completed']);self.assertEqual(result['profile'],'coarse')
|
||||
self.assertNotIn('profile',ordinary)
|
||||
self.assertLess(len(s.motions),len(standard.motions));self.assertTrue(any(abs(m[0])>128 for m in s.motions))
|
||||
self.assertLessEqual(len(s.motions),6);self.assertLessEqual(sum(abs(m[0]) for m in s.motions),768)
|
||||
self.assertEqual(result['deadline_monotonic'],ordinary['deadline_monotonic'])
|
||||
self.assertLess(s.now,standard.now);self.assertEqual(s.wheels,[])
|
||||
self.assertEqual(s.origin,standard.origin)
|
||||
def test_profile_and_explicit_prerequisites_refuse_before_input(self):
|
||||
for profile in (True,None,'unknown','coarse'):
|
||||
s=self.scene()
|
||||
with self.subTest(profile=profile),self.assertRaises(ValueError):
|
||||
policy.perform_facing(s,s.idle,s,s.observe,s.geometry,s.calibration,s.accepted,s.report,clock=s.clock,sleep=s.sleep,notify=s.notifications.append,profile=profile)
|
||||
self.assertEqual(s.motions,[])
|
||||
s=self.scene();s.report['approach_proof']['profile']='standard'
|
||||
with self.assertRaisesRegex(ValueError,'accepted coarse'):coarse(s)
|
||||
def test_coarse_native_overshoot_body_and_clock_guards(self):
|
||||
for fault in ('overshoot','reverse','moving','deadline','focus'):
|
||||
s=self.scene()
|
||||
if fault=='overshoot':s.gain=.5
|
||||
if fault=='reverse':s.reverse=True
|
||||
if fault=='moving':s.input=[0,0,1];s.sleep(.1)
|
||||
if fault=='deadline':s.calibration['alignment_deadline_monotonic']=31.15
|
||||
if fault=='focus':s.focus=False
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):coarse(s)
|
||||
self.assertFalse(s.report.get('facing_proof',{}).get('completed'));self.assertEqual(s.wheels,[])
|
||||
def test_policy_refuses_third_384_request_before_write(self):
|
||||
s=self.scene();s.angle-=140;s.gain=.011
|
||||
response=24*.011*math.pi/180;s.calibration['response_direction']=[math.sin(response),0,math.cos(response)]
|
||||
s.snapshot=s.make();s.history=[copy.deepcopy(s.snapshot)]
|
||||
with self.assertRaisesRegex(ValueError,'cumulative pixel'):coarse(s)
|
||||
self.assertEqual([abs(m[0]) for m in s.motions],[384,384])
|
||||
|
||||
|
||||
class Fn:
|
||||
def __init__(self,fn):self.fn=fn
|
||||
def __call__(self,*a):return self.fn(*a)
|
||||
|
||||
|
||||
class CapabilityTests(unittest.TestCase):
|
||||
def rig(self,coarse_enabled=True):
|
||||
events=[];now=[1.];target={'geometry':{'root':1},'focused_clipped_bounds':(0,0,2000,720)}
|
||||
xt=SimpleNamespace(XTestFakeRelativeMotionEvent=Fn(lambda *a:events.append(a) or 1))
|
||||
query=SimpleNamespace(d=1,x=SimpleNamespace(XQueryPointer=Fn(lambda *a:1),XFlush=lambda d:None),server_pid=lambda c:9)
|
||||
keyboard=SimpleNamespace(xt=xt,require_idle=lambda:None)
|
||||
mouse=helper.RelativeMouse(query,keyboard,{'window':100,'pid':9},enable_approach=True,enable_alignment=True,enable_coarse_approach=True,enable_coarse_facing=coarse_enabled,enable_selection=coarse_enabled)
|
||||
mouse.pointer=lambda:{'root':1,'child':0,'root_x':1000,'root_y':300,'mask':0}
|
||||
return mouse,SimpleNamespace(check=lambda:target),events,now
|
||||
def move(self,m,g,n,dx,dy=0):m.alignment_move(dx,dy,g,deadline=9,clock=lambda:n[0])
|
||||
def test_total_retired_before_actual_or_uncertain_write(self):
|
||||
m,g,events,n=self.rig()
|
||||
for dx in (384,383):self.move(m,g,n,dx)
|
||||
self.assertEqual(m.alignment_pixels,767)
|
||||
with self.assertRaisesRegex(ValueError,'budget'):self.move(m,g,n,2)
|
||||
self.move(m,g,n,1);self.assertEqual(len(events),3)
|
||||
with self.assertRaisesRegex(ValueError,'budget'):self.move(m,g,n,1)
|
||||
m,g,events,n=self.rig();m.xt.XTestFakeRelativeMotionEvent.fn=lambda *a:0
|
||||
with self.assertRaises(ValueError):self.move(m,g,n,384)
|
||||
self.assertEqual(m.alignment_pixels,384);self.assertEqual(m.alignment_requests,1)
|
||||
def test_six_request_cap_and_default_128(self):
|
||||
m,g,e,n=self.rig(False)
|
||||
with self.assertRaises(ValueError):self.move(m,g,n,129)
|
||||
for _ in range(6):self.move(m,g,n,1)
|
||||
with self.assertRaisesRegex(ValueError,'budget'):self.move(m,g,n,1)
|
||||
self.assertEqual(len(e),6)
|
||||
def test_coarse_retains_pointer_foreign_idle_and_final_deadline_guards(self):
|
||||
for fault in ('pointer','foreign','idle','deadline','pitch','bool','385'):
|
||||
m,g,e,n=self.rig()
|
||||
if fault=='pointer':m.pointer=lambda:{'root':1,'child':0,'root_x':1900,'root_y':300,'mask':0}
|
||||
if fault=='foreign':m.pointer=lambda:{'root':1,'child':1,'root_x':1000,'root_y':300,'mask':0};m.query.server_pid=lambda c:10
|
||||
if fault=='idle':m.keyboard.require_idle=lambda:(_ for _ in ()).throw(ValueError('busy'))
|
||||
if fault=='deadline':
|
||||
old=g.check
|
||||
def late():n[0]=9;return old()
|
||||
g.check=late
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):self.move(m,g,n,True if fault=='bool' else 385 if fault=='385' else 384,1 if fault=='pitch' else 0)
|
||||
self.assertEqual(e,[]);self.assertEqual(m.alignment_pixels,0)
|
||||
|
||||
|
||||
class RawTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
original_run=ApproachScene.run
|
||||
with patch.object(ApproachScene,'run',lambda s,*a,**kw:original_run(s,*a,**dict(kw,profile='coarse'))),patch.object(ChoiceScene,'facing',coarse),patch.object(ChoiceScene,'move',coarse_move):
|
||||
cls.fixture=build()
|
||||
def result(self,change=None,expected='coarse'):
|
||||
f,l,a,p,s=copy.deepcopy(self.fixture)
|
||||
if change:change(f,l,a,p,s)
|
||||
return selected_evidence.validate_selection_evidence(raw(f),'test',l,a,p,s,facing_profile=expected)
|
||||
def test_actual_policy_chain_and_raw_parsers_require_independent_profile(self):
|
||||
r=self.result();self.assertTrue(r['verified'],r);self.assertFalse(r['held_action_verified'])
|
||||
for expected in ('standard',True,None,'unknown'):
|
||||
r=self.result(expected=expected);self.assertFalse(r['verified'],r)
|
||||
def test_relabelled_missing_or_foreign_profile_refuses(self):
|
||||
for change in (lambda f,l,a,p,s:p.pop('profile'),lambda f,l,a,p,s:p.update(profile='standard'),lambda f,l,a,p,s:a.update(profile='standard')):
|
||||
r=self.result(change);self.assertFalse(r['verified'],r)
|
||||
def test_raw_coarse_intermediate_overshoot_and_budget_refuse(self):
|
||||
def overshoot(f,l,a,p,s):
|
||||
m=p['motions'][0];i=m['after_sample']+1;base=f[m['after_sample']]['direction'];angle=math.atan2(base[0],base[2])+math.radians(35.01)
|
||||
f[i]['direction']=[math.sin(angle),0,math.cos(angle)]
|
||||
for change in (overshoot,lambda f,l,a,p,s:p['motions'][0].update(dx=385),lambda f,l,a,p,s:p.update(deadline_monotonic=p['deadline_monotonic']+.01)):
|
||||
r=self.result(change);self.assertFalse(r['verified'],r)
|
||||
|
||||
|
||||
class RunnerTests(unittest.TestCase):
|
||||
def test_explicit_flags_refuse_before_runner_mutations(self):
|
||||
base=dict(exercise_prison_coarse_facing=True,exercise_prison_coarse_approach=True,exercise_prison_selection=True,exercise_prison_alignment=True,observe_prison_choices=True,exercise_prison_approach=True,exercise_relative_look=True,observe_prison_gate=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
||||
runner.validate_input_exercise(SimpleNamespace(**base))
|
||||
for change in ({'exercise_prison_selection':False},{'exercise_prison_coarse_approach':False},{'exercise_prison_coarse_facing':1},{'packed_core':Path('/x')},{'project':'vanilla-control'}):
|
||||
with patch.object(runner,'packed_request',side_effect=AssertionError('mutation reached')):
|
||||
with self.assertRaises(ValueError):runner.run(SimpleNamespace(**(base|change)))
|
||||
def test_helper_argv_requires_both_explicit_predecessors(self):
|
||||
base=['--enable-private-keyboard-proof','--prefix','/x','--game-executable','/g','--profile','/p','--report','/r','--host-display',':0','--nonce','N']
|
||||
flags=['--enable-private-look-proof','--enable-private-approach-proof','--enable-private-alignment-proof','--enable-private-selection-proof','--enable-private-coarse-approach-proof','--enable-private-coarse-facing-proof']
|
||||
self.assertFalse(helper.arguments(base).enable_private_coarse_facing_proof)
|
||||
self.assertTrue(helper.arguments(base+flags).enable_private_coarse_facing_proof)
|
||||
for missing in flags[:-1]:
|
||||
with self.assertRaises(SystemExit),patch('sys.stderr'):helper.arguments(base+[f for f in flags if f!=missing])
|
||||
def test_command_carries_explicit_capability_and_sealed_context(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
||||
for name in ('headless_input_proof.py','headless_display_probe.py',*helper.APPROACH_MODULES,*helper.ALIGNMENT_MODULES,*helper.SELECTION_MODULES):
|
||||
(root/'tools'/name).write_text('# fixture')
|
||||
base=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve','N','-errOrdinaryLookObserve','N','-errOrdinaryGateObserve','N','-errOrdinaryGateChoices','N']
|
||||
wrapped,display=runner.display_probe_command(base,root,Path('/compat'),profile,Path('/game'),'N',':0')
|
||||
args=(wrapped,root,Path('/compat'),profile,Path('/game'),'N',':0',display)
|
||||
for enabled in (False,True):
|
||||
command,context=runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=enabled)
|
||||
self.assertEqual('--enable-private-coarse-facing-proof' in command,enabled)
|
||||
self.assertEqual(context.get('coarse_facing_requested',False),enabled)
|
||||
self.assertEqual(context['command'],wrapped[4:])
|
||||
with self.assertRaises(ValueError):runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=False,coarse_approach=True,coarse_facing=True)
|
||||
def test_real_runner_forwards_launch_profile_to_full_raw_validator(self):
|
||||
f,l,a,p,s=copy.deepcopy(RawTests.fixture)
|
||||
for requested,recorded in ((True,True),(False,True),(True,False)):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
fixture=selection_runner_fixtures.SelectionRunner()
|
||||
session,context,report,observation=fixture.make_selection(Path(folder))
|
||||
context.update(nonce='test',coarse_approach_requested=True,coarse_facing_requested=requested)
|
||||
report.update(nonce='test',coarse_approach_requested=True,coarse_facing_requested=recorded,look_proof=l,approach_proof=a,facing_proof=p,action_selection_proof=s)
|
||||
report['events']=copy.deepcopy(a['events'])
|
||||
report['wheel_events']=[{'button':5,'down':down,'sent':True,'monotonic':s['wheel'][key]} for down,key in ((True,'down_monotonic'),(False,'up_monotonic'))]
|
||||
observation['nonce']='test';(session/'console.log').write_text(raw(f))
|
||||
Path(context['report']).write_text(json.dumps(report))
|
||||
status,result=runner.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
||||
if requested and recorded:
|
||||
self.assertEqual(status,'game-started',result);self.assertTrue(result['verified'],result)
|
||||
self.assertTrue(result['action_selection_proof']['verified'])
|
||||
else:self.assertEqual(status,'input-exercise-incomplete');self.assertFalse(result['verified'])
|
||||
|
||||
def test_result_rejects_self_authorized_profile_before_raw_credit(self):
|
||||
fixture=runner_fixtures.ExerciseTests()
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
session,context,report,observation=fixture.make_proof(Path(folder))
|
||||
for flags in ({'coarse_facing_requested':True},{'coarse_facing_requested':1},{'facing_proof':{'profile':'coarse'}}):
|
||||
Path(context['report']).write_text(json.dumps(report|flags))
|
||||
status,result=runner.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
||||
self.assertEqual(status,'input-exercise-incomplete');self.assertFalse(result['verified'])
|
||||
self.assertTrue('facing' in result['error'].lower() or 'action-selection' in result['error'].lower())
|
||||
(session/'input-proof.json').unlink()
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Actual dormant policies -> complete synthetic native log -> independent check.
|
||||
|
||||
No parser, prior validator or policy is mocked. Native calls/input are not run.
|
||||
"""
|
||||
import copy
|
||||
import re
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
|
||||
from test_prison_action_selection_evidence import build as selected_fixture, raw as selection_raw
|
||||
from test_prison_held_action_proof import Scene
|
||||
import prison_held_action_evidence as validator
|
||||
|
||||
|
||||
def build(profile='standard'):
|
||||
if profile=='coarse':
|
||||
from unittest.mock import patch
|
||||
from test_prison_coarse_facing import ApproachScene,ChoiceScene,coarse,coarse_move
|
||||
original_run=ApproachScene.run
|
||||
# Fake capabilities select the explicit profile; all real policy bodies
|
||||
# and raw validators still run without substitution.
|
||||
with patch.object(ApproachScene,'run',lambda s,*a,**kw:original_run(s,*a,**dict(kw,profile='coarse'))),patch.object(ChoiceScene,'facing',coarse),patch.object(ChoiceScene,'move',coarse_move):
|
||||
frames,look,route,facing,selection=selected_fixture()
|
||||
else:frames,look,route,facing,selection=selected_fixture()
|
||||
s=Scene();end=selection['samples'][-1]
|
||||
s.now=selection['finished_monotonic'];s.next_sample=(end['real_ms']-2000)/1000+.1
|
||||
s.geometry=copy.deepcopy(selection['geometry']);s.origin=list(end['origin'])
|
||||
s.camera=list(end['camera']);s.direction=list(end['direction']);s.actions=copy.deepcopy(selection['actions'])
|
||||
original_make=s.make
|
||||
def make():
|
||||
v=original_make();v['sample']=int(round((2000+s.now*1000)/100));v['real_ms']=2000+int(round(s.now*1000));v['world_ms']=v['real_ms']
|
||||
v['choices'].update(nonce='test',sample=v['sample'],real_ms=v['real_ms'])
|
||||
if v['phase']==1:v['choices']['selected_id']=s.actions[1]['id']
|
||||
return v
|
||||
s.make=make;v=copy.deepcopy(end);v['turn']=[0,0,0];v['world_ms']=v['real_ms']
|
||||
s.snapshot=v;s.history=[copy.deepcopy(v)];s.last_read=v['sample']-1
|
||||
s.calibration.update(nonce='test',ready_real_ms=2000,ready_monotonic=0.,observer_start_real_ms=0,observer_deadline_monotonic=42.5)
|
||||
s.report={'action_selection_proof':selection};s.evidence={'verified':True,'last_sample':selection['final_sample'],'selected_action_id':s.actions[1]['id'],'wheel_button':5}
|
||||
held=s.run()
|
||||
for frame in s.history:frames[frame['sample']]=copy.deepcopy(frame)
|
||||
return frames,look,route,facing,selection,held
|
||||
|
||||
|
||||
def raw(frames):
|
||||
# Retain existing native-format fixture including every prerequisite. These
|
||||
# two fields are deliberately independently variable for timing/look tests.
|
||||
content=selection_raw(frames)
|
||||
def world(m):return m[1]+str(frames[int(m[2])].get('world_ms',frames[int(m[2])]['real_ms']))
|
||||
content=re.sub(r'(ERR INPUT OBSERVE: run=test sample=(\d+) real=\d+ world=)\d+(?= SAMPLE)',world,content)
|
||||
def turn(m):
|
||||
v=frames[int(m[2])].get('turn',[0,0,0]);return m[1]+'<'+','.join(str(x) for x in v)+'>'
|
||||
return re.sub(r'(ERR LOOK OBSERVE: run=test sample=(\d+) real=\d+ SAMPLE[^\n]* turn=)<[^>]*>',turn,content)
|
||||
|
||||
|
||||
class HeldEvidenceTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):cls.template=build()
|
||||
def result(self,mutation=None,**kwargs):
|
||||
args=copy.deepcopy(self.template)
|
||||
if mutation:mutation(*args)
|
||||
f,*proofs=args
|
||||
return validator.validate_held_action_evidence(raw(f),'test',*proofs,**kwargs)
|
||||
def reject(self,mutation):
|
||||
r=self.result(mutation);self.assertFalse(r['verified'],r);self.assertTrue(r['errors'])
|
||||
def test_full_actual_policy_chain_and_native_phase_reset_pass(self):
|
||||
r=self.result();self.assertTrue(r['verified'],r)
|
||||
self.assertTrue(r['held_action_verified'] and r['unlock_observed'] and r['gate_motion_observed'])
|
||||
self.assertFalse(r['escape_verified']);self.assertFalse(r['adapter_release_confirmation_verified'])
|
||||
self.assertGreaterEqual(r['native_world_duration_ms'],4000)
|
||||
self.assertLessEqual(r['maximum_timing_allowance_seconds'],.502)
|
||||
def test_all_prior_proofs_revalidated_and_expected_profile_explicit(self):
|
||||
for which in range(1,5):self.reject(lambda *args:args[which].update(completed=False))
|
||||
self.assertFalse(self.result(facing_profile='coarse')['verified'])
|
||||
self.assertFalse(self.result(facing_profile='unknown')['verified'])
|
||||
def test_coarse_full_chain_requires_expected_profile(self):
|
||||
f,*p=build('coarse');content=raw(f)
|
||||
accepted=validator.validate_held_action_evidence(content,'test',*p,facing_profile='coarse')
|
||||
self.assertTrue(accepted['verified'],accepted)
|
||||
self.assertFalse(validator.validate_held_action_evidence(content,'test',*p)['verified'])
|
||||
def test_one_exact_f_pair_only_and_no_uncertain_cleanup(self):
|
||||
mutations=[lambda h:h['events'].append(copy.deepcopy(h['events'][0])),lambda h:h['events'].pop(),
|
||||
lambda h:h['events'][0].update(key='W'),lambda h:h['events'][1].update(cleanup=True),
|
||||
lambda h:h['events'][1].update(confirmed=False),lambda h:h.update(unconfirmed_held_key='F'),
|
||||
lambda h:h.update(paired_release_errors=['unproved release']),lambda h:h.update(notification_errors=['supervisor lost']),
|
||||
lambda h:h.update(release_confirmed=False),lambda h:h.update(events=[])]
|
||||
for mutation in mutations:self.reject(lambda *a:mutation(a[-1]))
|
||||
def test_original_clock_admission_and_five_second_lease_not_renewed(self):
|
||||
for mutation in (lambda h:h.update(original_deadline_monotonic=50),
|
||||
lambda h:h.update(held_deadline_monotonic=h['press_attempted_monotonic']+5.01),
|
||||
lambda h:h.update(held_deadline_monotonic=h['up_monotonic']),
|
||||
lambda h:h.update(original_deadline_monotonic=h['down_monotonic']+5.99),
|
||||
lambda h:h['events'][0].update(attempted_monotonic=h['press_attempted_monotonic']-.01)):
|
||||
self.reject(lambda *a:mutation(a[-1]))
|
||||
def test_compact_references_pass_and_wrong_world_reference_refuses(self):
|
||||
def compact(*a):
|
||||
h=a[-1];h['samples']=[{k:r[k] for k in ('sample','real_ms','received_monotonic')} for r in h['samples']]
|
||||
self.assertTrue(self.result(compact)['verified'])
|
||||
self.reject(lambda *a:a[-1]['samples'][-1].update(world_ms=-1))
|
||||
def test_missing_reordered_stale_and_forged_receipts_refuse(self):
|
||||
for mutation in (lambda h:h['samples'].pop(2),lambda h:h['samples'].reverse(),
|
||||
lambda h:h['samples'][-1].update(received_monotonic=float('nan')),
|
||||
lambda h:h['samples'][2].update(received_monotonic=-1),
|
||||
lambda h:h['samples'][2].update(real_ms=0),
|
||||
lambda h:h.update(finished_monotonic=h['samples'][-1]['received_monotonic']+.51)):
|
||||
self.reject(lambda *a:mutation(a[-1]))
|
||||
def test_native_progress_jump_and_early_transition_refuse(self):
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][0]].update(progress=3.9))
|
||||
def early(f,*a):
|
||||
h=a[-1];i=h['progress_samples'][2];f[i].update(phase=2,progress=0,performing=False)
|
||||
self.reject(early)
|
||||
def no_progress(f,*a):
|
||||
h=a[-1]
|
||||
for i in range(h['press_sample']+1,h['transition_sample']):f[i].update(progress=0)
|
||||
self.reject(no_progress)
|
||||
def test_reset_decrease_unbound_and_input_ack_loss_refuse(self):
|
||||
for key,value in [('progress',0),('progress',.001),('performing',False),('pressed',0),('progress_bound',False),('selected',False),('cached_performable',False),('context_selected',False)]:
|
||||
with self.subTest(key=key,value=value):self.reject(lambda f,*a:f[a[-1]['progress_samples'][5]].update({key:value}))
|
||||
def test_world_clock_is_independent_of_real_and_cannot_reverse_or_freeze(self):
|
||||
def freeze(f,*a):
|
||||
h=a[-1];world=f[h['press_sample']]['real_ms']
|
||||
for i in range(h['press_sample']+1,h['final_sample']+1):f[i]['world_ms']=world
|
||||
self.reject(freeze)
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][5]].update(world_ms=0))
|
||||
def test_frame_order_allowance_uses_observed_interval_only(self):
|
||||
def shift(f,*a):
|
||||
h=a[-1]
|
||||
for i in range(h['press_sample']+1,h['transition_sample']):
|
||||
if f[i]['progress']>0:f[i]['progress']+=.025
|
||||
# Actual duration resets within one frame; a small relative sampling
|
||||
# offset remains compatible with the documented per-frame API.
|
||||
self.assertTrue(self.result(shift)['verified'])
|
||||
def jump(f,*a):f[a[-1]['progress_samples'][3]]['progress']+=.3
|
||||
self.reject(jump)
|
||||
def test_native_body_view_turn_menu_input_and_velocity_are_authoritative(self):
|
||||
for key,value in [('turn',[.02,0,0]),('input',[0,0,1]),('forward',1),('velocity',[.1,0,0]),('inventory',True),('direction',[0,0,1])]:
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]].update({key:value}))
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]]['origin'].__setitem__(1,.051))
|
||||
def test_phase_two_moving_original_door_allowed_but_not_before_transition(self):
|
||||
f,*p=copy.deepcopy(self.template);h=p[-1]
|
||||
self.assertNotEqual(f[h['transition_sample']]['geometry']['context'],h['geometry']['context'])
|
||||
self.assertTrue(self.result()['verified'])
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]]['geometry']['context'].__setitem__(0,200))
|
||||
self.reject(lambda f,*a:f[a[-1]['released_sample']].update(phase=1))
|
||||
self.reject(lambda f,*a:f[a[-1]['released_sample']].update(phase=3))
|
||||
def test_no_control_or_motion_and_forged_reference_cannot_pass(self):
|
||||
for field in ('door_state','door_control'):
|
||||
def remove(f,*a):
|
||||
h=a[-1]
|
||||
for i in range(h['transition_sample'],h['final_sample']+1):f[i][field]=0
|
||||
self.reject(remove)
|
||||
for key in ('transition_sample','control_sample','motion_sample'):
|
||||
self.reject(lambda *a:a[-1].update({key:a[-1][key]+1}))
|
||||
self.reject(lambda *a:a[-1]['progress_samples'].pop())
|
||||
def test_release_needs_new_raw_neutral_sample_after_drain(self):
|
||||
self.reject(lambda *a:a[-1].update(released_sample=a[-1]['release_barrier_sample']))
|
||||
for key,value in [('pressed',1),('progress',1),('performing',True)]:
|
||||
self.reject(lambda f,*a:f[a[-1]['released_sample']].update({key:value}))
|
||||
self.reject(lambda *a:a[-1].update(release_barrier_sample=a[-1]['transition_sample']-1))
|
||||
def test_old_release_acknowledgement_cannot_credit_renewed_or_recovered_input(self):
|
||||
def append(f,h,pressed):
|
||||
i=h['final_sample'];v=copy.deepcopy(f[i])
|
||||
v.update(sample=i+1,real_ms=v['real_ms']+100,world_ms=v['world_ms']+100,
|
||||
received_monotonic=v['received_monotonic']+.1,pressed=pressed,progress=0,performing=False)
|
||||
v['choices'].update(sample=i+1,real_ms=v['real_ms']);f[i+1]=v
|
||||
h['samples'].append(copy.deepcopy(v));h['final_sample']=i+1;h['finished_monotonic']+=.11
|
||||
def old(f,*a):append(f,a[-1],1)
|
||||
self.reject(old)
|
||||
def recovered(f,*a):
|
||||
h=a[-1];append(f,h,1);append(f,h,0);h['released_sample']=h['final_sample']
|
||||
r=self.result(recovered);self.assertFalse(r['verified'],r)
|
||||
self.assertIn('renewed after release',str(r['errors']))
|
||||
|
||||
def test_normal_release_must_follow_observed_authoritative_transition(self):
|
||||
def premature(*a):
|
||||
h=a[-1];at=h['down_monotonic']+.5
|
||||
h['up_monotonic']=at;h['events'][1].update(attempted_monotonic=at-.01,monotonic=at)
|
||||
r=self.result(premature);self.assertFalse(r['verified'],r)
|
||||
self.assertIn('release preceded',str(r['errors']))
|
||||
def test_native_choice_identity_permission_and_proof_binding_cannot_change(self):
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]]['choices']['actions'][1].update(id=17))
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]]['choices']['actions'][1].update(permitted=False))
|
||||
self.reject(lambda *a:a[-1].update(nonce='other'))
|
||||
self.reject(lambda *a:a[-1]['actions'][1].update(id=17))
|
||||
def test_raw_intermediate_records_and_original_observer_cancellation_required(self):
|
||||
f,*p=copy.deepcopy(self.template);h=p[-1];content=raw(f);i=h['progress_samples'][4]
|
||||
for prefix in ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:', 'ERR GATE CHOICE:'):
|
||||
line=next(x for x in content.splitlines() if x.startswith(prefix) and f' sample={i} ' in x)
|
||||
r=validator.validate_held_action_evidence(content.replace(line+'\n',''),'test',*p)
|
||||
self.assertFalse(r['verified'],r)
|
||||
i=h['final_sample']
|
||||
line=f'ERR LOOK OBSERVE: run=test sample={i} real={f[i]["real_ms"]} CANCELLED menu-opened\n'
|
||||
self.assertFalse(validator.validate_held_action_evidence(content+line,'test',*p)['verified'])
|
||||
def test_early_transition_rejected_by_world_duration_before_reference_comparison(self):
|
||||
def early(f,*a):
|
||||
h=a[-1];i=h['progress_samples'][4];f[i].update(phase=2,progress=0,performing=False)
|
||||
r=self.result(early);self.assertFalse(r['verified'],r)
|
||||
self.assertIn('before four seconds',str(r['errors']))
|
||||
def test_original_duration_and_handoff_receipt_are_not_report_authority(self):
|
||||
self.reject(lambda f,*a:f[a[-1]['progress_samples'][3]].update(duration=1))
|
||||
self.reject(lambda *a:a[-1]['samples'][0].update(received_monotonic=a[-1]['samples'][0]['received_monotonic']+.01))
|
||||
|
||||
def test_unknown_payload_or_success_claim_does_not_override_raw(self):
|
||||
self.reject(lambda *a:a[-1].update(extra='ignored input'))
|
||||
self.reject(lambda *a:a[-1].update(status='unlock-observed-motion-unverified'))
|
||||
self.reject(lambda *a:a[-1].update(completed=False))
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,271 @@
|
||||
"""Actual dormant held policy on simulated native snapshots; no input/runtime."""
|
||||
import copy
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import prison_held_action_proof as policy
|
||||
|
||||
|
||||
class Scene:
|
||||
def __init__(self):
|
||||
self.now=30.;self.next_sample=30.1;self.guard_cost=.02
|
||||
self.geometry={'nonce':'N','basis':'registered-prison-lock-native-interaction','prison':[100.,0.,100.],
|
||||
'gate':[100.,0.,94.],'gate_forward':[0.,0.,1.],'context':[102.7,1.2,94.],'radius':2.}
|
||||
self.origin=[102.7,0.,95.5];self.camera=[102.7,1.7,95.4];self.direction=[0.,0.,-1.]
|
||||
self.actions=[{'class':'ERR_PrisonDoorAction','id':8,'permitted':False},{'class':'ERR_UnlockPrisonAction','id':3,'permitted':True}]
|
||||
self.focus=True;self.native_held=False;self.down=None;self.up=None;self.events=[];self.notifications=[]
|
||||
self.fail_down=False;self.fail_up=False;self.unknown_down=False;self.release_late=False;self.native_release_missing=False
|
||||
self.phase_delay=4.1;self.motion_delay=.15;self.never_complete=False;self.no_motion=False;self.no_control=False
|
||||
self.mutate=None;self.stale=False;self.history=[];self.last_read=299
|
||||
self.snapshot=self.make();self.history=[copy.deepcopy(self.snapshot)]
|
||||
prior=copy.deepcopy(self.snapshot);prior.update(sample=299,real_ms=59900,received_monotonic=29.9);prior['choices'].update(sample=299,real_ms=59900)
|
||||
self.report={'action_selection_proof':{'completed':True,'nonce':'N','geometry':copy.deepcopy(self.geometry),
|
||||
'final_sample':300,'selection_samples':[299,300],'samples':[prior,copy.deepcopy(self.snapshot)],'actions':copy.deepcopy(self.actions),
|
||||
'deadline_monotonic':30.,'finished_monotonic':30.}}
|
||||
self.calibration={'nonce':'N','observer_start_real_ms':30000,'ready_real_ms':32000,'ready_monotonic':2.,'observer_deadline_monotonic':44.5}
|
||||
self.evidence={'verified':True,'last_sample':300,'selected_action_id':3,'wheel_button':5}
|
||||
def clock(self):return self.now
|
||||
def make(self):
|
||||
elapsed=None if self.down is None else self.now-self.down
|
||||
phase=2 if elapsed is not None and elapsed>=self.phase_delay and not self.never_complete else 1
|
||||
performing=phase==1 and elapsed is not None and elapsed>=.1 and self.native_held
|
||||
progress=min(3.98,max(0,elapsed-.1)) if performing else 0.
|
||||
pressed=1. if self.native_held or (self.native_release_missing and self.down is not None) else 0.
|
||||
geo=copy.deepcopy(self.geometry)
|
||||
if phase==2:geo['context']=[102.,1.2,94.7];geo['gate_forward']=[.3,0.,.953939]
|
||||
selected=phase==1
|
||||
s={'sample':int(round(self.now*10)),'real_ms':30000+int(round(self.now*1000)),
|
||||
'received_monotonic':self.now-(1 if self.stale else 0),'geometry':geo,'phase':phase,
|
||||
'origin':list(self.origin),'camera':list(self.camera),'direction':list(self.direction),
|
||||
'body_forward':[0.,0.,-1.],'input':[0.,0.,0.],'forward':0.,'velocity':[0.,0.,0.],'turn':[0.,0.,0.],
|
||||
'inventory':False,'door_state':.25 if phase==2 and elapsed>=self.phase_delay+self.motion_delay and not self.no_motion else 0.,
|
||||
'door_control':1. if phase==2 and not self.no_control else 0.,'selected':selected,'context_selected':selected,
|
||||
'cached_performable':selected,'progress_bound':selected,'performing':performing,'progress':progress,'pressed':pressed,'duration':4.}
|
||||
s['choices']={'nonce':'N','sample':s['sample'],'real_ms':s['real_ms'],'selected_id':3 if selected else -1,
|
||||
'selected_class':'ERR_UnlockPrisonAction' if selected else 'none','actions':copy.deepcopy(self.actions) if selected else []}
|
||||
if self.mutate:self.mutate(s,elapsed)
|
||||
return s
|
||||
def sleep(self,seconds):
|
||||
end=self.now+seconds
|
||||
while self.now<end-1e-8:
|
||||
self.now+=min(.005,end-self.now)
|
||||
if self.now>=self.next_sample-1e-8:
|
||||
self.snapshot=self.make();self.history.append(copy.deepcopy(self.snapshot));self.next_sample+=.1
|
||||
def check(self):
|
||||
if not self.focus:raise policy.Refused('original private owner lost')
|
||||
self.sleep(self.guard_cost)
|
||||
def idle(self):
|
||||
if self.native_held:raise policy.Refused('native keys not idle')
|
||||
def observe(self):
|
||||
batch=[x for x in self.history if x['sample']>self.last_read] or [self.snapshot]
|
||||
self.last_read=batch[-1]['sample'];return copy.deepcopy(batch)
|
||||
def down_f(self,guard,*,deadline,not_after):
|
||||
guard.check();self.idle()
|
||||
if self.now>not_after or self.now>=deadline:raise policy.Refused('adapter admission expired')
|
||||
self.native_held=True;self.down=self.now;self.events.append(('F',True,self.now,deadline))
|
||||
if self.unknown_down:raise policy.Refused('down request outcome uncertain')
|
||||
if self.fail_down:raise policy.Refused('down refused')
|
||||
return {'key':'F','down_monotonic':self.now}
|
||||
def up_f(self,guard,*,deadline,cleanup=False):
|
||||
guard.check()
|
||||
if self.fail_up:raise policy.Refused('up refused')
|
||||
if self.release_late and not cleanup:self.sleep(2.)
|
||||
self.native_held=False;self.up=self.now;self.events.append(('F',False,self.now,deadline))
|
||||
return {'key':'F','up_monotonic':self.now,'released':True}
|
||||
def run(self):
|
||||
return policy.perform_held_action(self,self.idle,self,self.observe,self.geometry,self.calibration,self.evidence,self.report,
|
||||
clock=self.clock,sleep=self.sleep,notify=self.notifications.append)
|
||||
|
||||
|
||||
class HeldTests(unittest.TestCase):
|
||||
def test_one_real_policy_hold_progress_reset_transition_motion_and_release(self):
|
||||
s=Scene();r=s.run()
|
||||
self.assertTrue(r['completed']);self.assertEqual(r['status'],'verified')
|
||||
self.assertEqual([(e[0],e[1]) for e in s.events],[('F',True),('F',False)])
|
||||
self.assertGreaterEqual(len(r['progress_samples']),2);self.assertTrue(r['unlock_observed'] and r['gate_motion_observed'])
|
||||
self.assertTrue(r['release_confirmed']);self.assertIsNone(r['unconfirmed_held_key'])
|
||||
self.assertEqual(r['paired_release_errors'],[]);self.assertLess(r['released_sample'],451)
|
||||
self.assertGreater(r['released_sample'],r['release_barrier_sample']);self.assertLess(s.now,44.5)
|
||||
self.assertEqual(r['original_deadline_monotonic'],44.5)
|
||||
self.assertLessEqual(r['held_deadline_monotonic']-r['press_attempted_monotonic'],5)
|
||||
self.assertLess(r['up_monotonic'],r['held_deadline_monotonic'])
|
||||
frames={x['sample']:x for x in r['samples']}
|
||||
self.assertTrue(frames[r['transition_sample']]['progress']==0 and not frames[r['transition_sample']]['selected'])
|
||||
self.assertNotEqual(frames[r['motion_sample']]['geometry']['context'],s.geometry['context'])
|
||||
def test_does_not_renew_previous_selection_eight_second_lease(self):
|
||||
s=Scene();self.assertEqual(s.report['action_selection_proof']['deadline_monotonic'],30.)
|
||||
r=s.run();self.assertTrue(r['completed']);self.assertEqual(r['original_deadline_monotonic'],44.5)
|
||||
self.assertEqual(s.report['action_selection_proof']['deadline_monotonic'],30.)
|
||||
def test_insufficient_original_budget_sends_nothing(self):
|
||||
s=Scene();s.calibration['observer_deadline_monotonic']=35.9
|
||||
with self.assertRaisesRegex(ValueError,'insufficient-original-budget'):s.run()
|
||||
self.assertEqual(s.events,[]);self.assertEqual(s.report['held_action_proof']['status'],'refused-no-input')
|
||||
def test_no_verified_selection_no_press(self):
|
||||
for fault in ('verdict','nonce','action','pair','deadline'):
|
||||
s=Scene()
|
||||
if fault=='verdict':s.evidence['verified']=False
|
||||
if fault=='nonce':s.calibration['nonce']='foreign'
|
||||
if fault=='action':s.evidence['selected_action_id']=8
|
||||
if fault=='pair':s.report['action_selection_proof']['selection_samples']=[298,300]
|
||||
if fault=='deadline':s.calibration['observer_deadline_monotonic']=60
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):s.run()
|
||||
self.assertEqual(s.events,[])
|
||||
def test_early_reset_or_missing_native_input_refuses_and_releases(self):
|
||||
for fault in ('reset','pressed','performing','decrease','unbound','selection','permission'):
|
||||
s=Scene()
|
||||
def mutate(x,e):
|
||||
if e is None or e<1 or x['phase']!=1:return
|
||||
if fault=='reset':x['progress']=0
|
||||
if fault=='pressed':x['pressed']=0
|
||||
if fault=='performing':x['performing']=False
|
||||
if fault=='decrease':x['progress']=.01
|
||||
if fault=='unbound':x['progress_bound']=False
|
||||
if fault=='selection':x['selected']=False
|
||||
if fault=='permission':x['cached_performable']=False
|
||||
s.mutate=mutate
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):s.run()
|
||||
self.assertEqual([e[1] for e in s.events],[True,False]);self.assertFalse(s.native_held)
|
||||
self.assertFalse(s.report['held_action_proof']['completed'])
|
||||
def test_phase_without_advancing_progress_is_not_completion(self):
|
||||
s=Scene();s.phase_delay=.1
|
||||
with self.assertRaisesRegex(ValueError,'advancing bound progress'):s.run()
|
||||
self.assertEqual([e[1] for e in s.events],[True,False])
|
||||
def test_intermediate_identity_geometry_body_input_phase_refusals(self):
|
||||
for fault in ('phase0','phase3','body','turn','input','menu','geometry','gap','nonce'):
|
||||
s=Scene()
|
||||
def mutate(x,e):
|
||||
if e is None or not .8<e<1.:return
|
||||
if fault=='phase0':x['phase']=0
|
||||
if fault=='phase3':x['phase']=3
|
||||
if fault=='body':x['origin'][0]+=.06
|
||||
if fault=='turn':x['turn'][0]=.02
|
||||
if fault=='input':x['input'][2]=1
|
||||
if fault=='menu':x['inventory']=True
|
||||
if fault=='geometry':x['geometry']['context'][0]+=.1
|
||||
if fault=='gap':x['sample']+=1;x['choices']['sample']=x['sample']
|
||||
if fault=='nonce':x['geometry']['nonce']='foreign'
|
||||
s.mutate=mutate
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):s.run()
|
||||
self.assertEqual([e[1] for e in s.events],[True,False])
|
||||
def test_phase_two_reversal_refuses_even_after_release(self):
|
||||
s=Scene();s.motion_delay=1.
|
||||
def mutate(x,e):
|
||||
if e is not None and e>4.3:x['phase']=1
|
||||
s.mutate=mutate
|
||||
with self.assertRaisesRegex(ValueError,'phase reversed'):s.run()
|
||||
self.assertTrue(s.report['held_action_proof']['unlock_observed']);self.assertFalse(s.report['held_action_proof']['completed'])
|
||||
def test_unlock_without_motion_or_control_is_not_full_proof(self):
|
||||
for fault in ('no_motion','no_control'):
|
||||
s=Scene();setattr(s,fault,True)
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):s.run()
|
||||
r=s.report['held_action_proof'];self.assertTrue(r['unlock_observed']);self.assertFalse(r['completed'])
|
||||
self.assertEqual(r['status'],'unlock-observed-motion-unverified');self.assertFalse(s.native_held)
|
||||
def test_renewed_native_input_after_acknowledgement_refuses_before_delayed_motion(self):
|
||||
for buffered in (False,True):
|
||||
s=Scene();s.motion_delay=.8
|
||||
def mutate(x,e):
|
||||
if e is not None and 4.35<e<4.45:x['pressed']=1.
|
||||
s.mutate=mutate
|
||||
if buffered:
|
||||
observe=s.observe
|
||||
def batched():
|
||||
if s.report.get('held_action_proof',{}).get('release_barrier_sample') is not None and not hasattr(s,'extra_batch_delay'):
|
||||
s.extra_batch_delay=True;s.sleep(.35)
|
||||
return observe()
|
||||
s.observe=batched
|
||||
with self.subTest(buffered=buffered),self.assertRaisesRegex(ValueError,'renewed after release'):
|
||||
s.run()
|
||||
self.assertFalse(s.report['held_action_proof']['completed']);self.assertFalse(s.native_held)
|
||||
self.assertEqual([e[1] for e in s.events],[True,False])
|
||||
def test_delayed_motion_success_has_latest_neutral_release_acknowledgement(self):
|
||||
s=Scene();s.motion_delay=.8;r=s.run()
|
||||
self.assertTrue(r['completed']);self.assertEqual(r['released_sample'],r['final_sample'])
|
||||
self.assertEqual(r['samples'][-1]['pressed'],0)
|
||||
|
||||
def test_missing_native_released_input_refuses(self):
|
||||
s=Scene();s.native_release_missing=True
|
||||
with self.assertRaises(ValueError):s.run()
|
||||
r=s.report['held_action_proof'];self.assertTrue(r['release_confirmed']);self.assertIsNone(r['released_sample']);self.assertFalse(r['completed'])
|
||||
def test_incomplete_action_releases_before_original_five_second_lease(self):
|
||||
s=Scene();s.never_complete=True
|
||||
with self.assertRaisesRegex(ValueError,'release reserve'):s.run()
|
||||
r=s.report['held_action_proof'];self.assertEqual([e[1] for e in s.events],[True,False]);self.assertLess(r['up_monotonic'],r['held_deadline_monotonic']);self.assertFalse(r['completed'])
|
||||
def test_uncertain_down_always_attempts_up_and_preserves_primary_error(self):
|
||||
s=Scene();s.unknown_down=True
|
||||
with self.assertRaisesRegex(ValueError,'down request outcome uncertain'):s.run()
|
||||
r=s.report['held_action_proof'];self.assertEqual([e[1] for e in s.events],[True,False]);self.assertTrue(r['release_confirmed']);self.assertFalse(r['completed'])
|
||||
s=Scene();s.unknown_down=True;s.fail_up=True
|
||||
with self.assertRaisesRegex(ValueError,'down request outcome uncertain'):s.run()
|
||||
r=s.report['held_action_proof'];self.assertEqual(r['unconfirmed_held_key'],'F');self.assertIn('up refused',r['paired_release_errors'])
|
||||
def test_lost_owner_never_redirects_uncertain_release(self):
|
||||
s=Scene();original=s.down_f
|
||||
def down(*a,**kw):result=original(*a,**kw);s.focus=False;return result
|
||||
s.down_f=down
|
||||
with self.assertRaisesRegex(ValueError,'original private owner lost'):s.run()
|
||||
self.assertEqual([e[1] for e in s.events],[True]);self.assertEqual(s.report['held_action_proof']['unconfirmed_held_key'],'F')
|
||||
def test_late_release_cannot_qualify(self):
|
||||
s=Scene();s.release_late=True
|
||||
with self.assertRaisesRegex(ValueError,'release exceeded'):s.run()
|
||||
self.assertFalse(s.report['held_action_proof']['completed']);self.assertFalse(s.native_held)
|
||||
def test_final_adapter_guard_rechecks_six_seconds_before_any_down(self):
|
||||
s=Scene();s.calibration['observer_deadline_monotonic']=36.3
|
||||
original=s.down_f
|
||||
def slow(*a,**kw):s.sleep(.4);return original(*a,**kw)
|
||||
s.down_f=slow
|
||||
with self.assertRaisesRegex(ValueError,'adapter admission expired'):s.run()
|
||||
self.assertFalse(any(event[1] for event in s.events))
|
||||
# A down adapter refusal remains uncertain until guarded cleanup proves up.
|
||||
self.assertEqual([event[1] for event in s.events],[False])
|
||||
self.assertTrue(s.report['held_action_proof']['release_confirmed'])
|
||||
|
||||
def test_constant_progress_never_substitutes_for_two_advances(self):
|
||||
s=Scene()
|
||||
def mutate(x,e):
|
||||
if e is not None and x['phase']==1 and x['progress']>0:x['progress']=.5
|
||||
s.mutate=mutate
|
||||
with self.assertRaisesRegex(ValueError,'advancing bound progress'):s.run()
|
||||
self.assertFalse(s.report['held_action_proof']['unlock_observed'])
|
||||
self.assertEqual([x[1] for x in s.events],[True,False])
|
||||
|
||||
def test_first_post_release_buffered_reset_is_not_release_acknowledgement(self):
|
||||
s=Scene();s.no_motion=True
|
||||
original=s.observe
|
||||
def observe():
|
||||
batch=original()
|
||||
if s.up is not None:
|
||||
# Freeze the first post-release batch forever: all are reset,
|
||||
# but none is newer than the full drained acknowledgement barrier.
|
||||
if not hasattr(s,'frozen_after_up'):s.frozen_after_up=copy.deepcopy(batch)
|
||||
return copy.deepcopy(s.frozen_after_up[-1:]) if s.last_read>s.frozen_after_up[-1]['sample'] else batch
|
||||
return batch
|
||||
s.observe=observe
|
||||
with self.assertRaises(ValueError):s.run()
|
||||
r=s.report['held_action_proof'];self.assertTrue(r['release_confirmed']);self.assertIsNone(r['released_sample'])
|
||||
|
||||
def test_primary_failure_survives_notification_cleanup_failure(self):
|
||||
s=Scene();s.unknown_down=True
|
||||
def notify(event):
|
||||
s.notifications.append(event)
|
||||
if event['kind']=='held-action-finished':raise policy.Refused('supervisor pipe closed')
|
||||
with self.assertRaisesRegex(ValueError,'down request outcome uncertain'):
|
||||
policy.perform_held_action(s,s.idle,s,s.observe,s.geometry,s.calibration,s.evidence,s.report,
|
||||
clock=s.clock,sleep=s.sleep,notify=notify)
|
||||
r=s.report['held_action_proof'];self.assertEqual(r['error'],'down request outcome uncertain')
|
||||
self.assertEqual(r['notification_errors'],['supervisor pipe closed'])
|
||||
self.assertEqual([x[1] for x in s.events],[True,False])
|
||||
|
||||
def test_retry_cannot_press_again(self):
|
||||
s=Scene();s.run();count=len(s.events)
|
||||
with self.assertRaisesRegex(ValueError,'already exists'):s.run()
|
||||
self.assertEqual(len(s.events),count)
|
||||
def test_stale_snapshot_and_old_initial_phase_refuse_without_input(self):
|
||||
for fault in ('stale','phase'):
|
||||
s=Scene()
|
||||
if fault=='stale':s.stale=True;s.sleep(.1)
|
||||
else:s.history[0]['phase']=2
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):s.run()
|
||||
self.assertEqual(s.events,[])
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,274 @@
|
||||
"""Captured F-only adapter on fake XTest; native log reader and pure policy live."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
|
||||
import headless_input_proof as h
|
||||
import prison_held_action_proof as policy
|
||||
from test_prison_held_action_proof import Scene
|
||||
from test_prison_held_action_evidence import build as raw_fixture,raw
|
||||
|
||||
class Fn:
|
||||
def __init__(self,fn):self.fn=fn
|
||||
def __call__(self,*a):return self.fn(*a)
|
||||
|
||||
class Rig:
|
||||
def __init__(self,scene=None):
|
||||
self.scene=scene or Scene();self.keys=set();self.calls=[];self.fail_down=False;self.fail_up=False;self.pointer_busy=False
|
||||
self.scene.guard_cost=.01;self.codes={0x77:25,0xff09:23,0x66:41}
|
||||
def keymap(d,state):
|
||||
for i in range(32):state[i]=bytes([sum(1<<(code%8) for code in self.keys if code//8==i)])
|
||||
return 1
|
||||
self.x=SimpleNamespace(XKeysymToKeycode=Fn(lambda d,k:self.codes.get(k,0)),XKeycodeToKeysym=Fn(lambda d,c,g:next((k for k,v in self.codes.items() if v==c),0)),
|
||||
XQueryKeymap=Fn(keymap),XFlush=Fn(lambda d:0))
|
||||
def keyevent(d,code,down,delay):
|
||||
self.calls.append((d,code,down,self.scene.now))
|
||||
if down:
|
||||
self.keys.add(code);self.scene.native_held=True;self.scene.down=self.scene.now
|
||||
return 0 if self.fail_down else 1
|
||||
if self.fail_up:return 0
|
||||
self.keys.discard(code);self.scene.native_held=False;self.scene.up=self.scene.now
|
||||
return 1
|
||||
self.xt=SimpleNamespace(XTestQueryExtension=Fn(lambda *a:1),XTestFakeKeyEvent=Fn(keyevent))
|
||||
self.query=SimpleNamespace(d=1,x=self.x)
|
||||
self.owner={'window':100,'pid':9,'process':{'start':1}}
|
||||
def check():self.scene.check();return {'verified':True}
|
||||
self.guard=SimpleNamespace(query=self.query,owner=copy.deepcopy(self.owner),check=check)
|
||||
with patch.object(h.C,'CDLL',return_value=self.xt):self.keyboard=h.Keyboard(self.query)
|
||||
def pointer(*a):
|
||||
if self.pointer_busy:raise h.Refused('pointer outside original window')
|
||||
self.mouse=SimpleNamespace(query=self.query,owner=copy.deepcopy(self.owner),keyboard=self.keyboard,validate_pointer=pointer,enable_coarse_facing=True)
|
||||
self.report=self.scene.report;self.report.update(paired_release_errors=[],held_action_requested=True,coarse_facing_requested=True)
|
||||
self.adapter=h.HeldFKey(self.guard,self.keyboard,self.mouse,self.report,self.scene.clock,self.scene.sleep,44.5,enabled=True)
|
||||
def down(self):return self.adapter.down_f(self.guard,deadline=35,not_after=30.5)
|
||||
def up(self,cleanup=False):return self.adapter.up_f(self.guard,deadline=35,cleanup=cleanup)
|
||||
def run(self):
|
||||
return policy.perform_held_action(self.guard,self.keyboard.require_idle,self.adapter,self.scene.observe,self.scene.geometry,self.scene.calibration,self.scene.evidence,self.report,
|
||||
clock=self.scene.clock,sleep=self.scene.sleep,notify=self.scene.notifications.append)
|
||||
|
||||
class AdapterTests(unittest.TestCase):
|
||||
def test_actual_keyboard_stays_wtab_and_only_explicit_adapter_can_f(self):
|
||||
r=Rig();self.assertEqual(set(r.keyboard.codes),{'W','Tab'})
|
||||
with self.assertRaises(ValueError):r.keyboard.send('F',True)
|
||||
with self.assertRaises(ValueError):h.HeldFKey(r.guard,r.keyboard,r.mouse,r.report,r.scene.clock,r.scene.sleep,44.5)
|
||||
down=r.down();self.assertEqual(down,{'key':'F','down_monotonic':r.scene.now})
|
||||
self.assertEqual(r.calls[0][1:3],(41,1));self.assertEqual(r.report['unconfirmed_held_key'],'F')
|
||||
with self.assertRaises(ValueError):r.down()
|
||||
r.up();self.assertFalse(r.keys);self.assertIsNone(r.report['unconfirmed_held_key'])
|
||||
self.assertEqual([set(e) for e in r.report['held_events']],[{'key','down','monotonic','sent'}]*2)
|
||||
self.assertEqual([e['down'] for e in r.report['held_events']],[True,False])
|
||||
def test_real_policy_uses_actual_f_adapter_and_preserves_release_evidence(self):
|
||||
r=Rig();result=r.run()
|
||||
self.assertTrue(result['completed']);self.assertTrue(result['release_confirmed']);self.assertFalse(r.keys)
|
||||
self.assertEqual([c[2] for c in r.calls],[1,0])
|
||||
self.assertEqual(r.report['held_events'][0]['monotonic'],result['down_monotonic'])
|
||||
self.assertEqual(r.report['held_events'][1]['monotonic'],result['up_monotonic'])
|
||||
self.assertLess(result['up_monotonic'],result['held_deadline_monotonic'])
|
||||
self.assertEqual(r.report['held_release_errors'],[])
|
||||
def test_uncertain_down_cleanup_retains_original_code_and_one_attempt(self):
|
||||
r=Rig();r.fail_down=True
|
||||
with self.assertRaisesRegex(ValueError,'uncertain'):r.run()
|
||||
self.assertEqual([c[2] for c in r.calls],[1,0]);self.assertFalse(r.keys)
|
||||
self.assertFalse(r.report['held_events'][0]['sent']);self.assertTrue(r.report['held_events'][1]['finally'])
|
||||
self.assertFalse(r.report['held_action_proof']['completed'])
|
||||
with self.assertRaises(ValueError):r.down()
|
||||
def test_uncertain_up_and_lost_owner_never_redirect(self):
|
||||
for fault in ('up','owner','connection','guard','pointer'):
|
||||
r=Rig();r.down()
|
||||
if fault=='up':r.fail_up=True
|
||||
if fault=='owner':r.guard.owner['pid']=10
|
||||
if fault=='connection':r.mouse.query=object()
|
||||
if fault=='guard':r.guard.check=lambda:(_ for _ in ()).throw(h.Refused('original owner lost'))
|
||||
if fault=='pointer':r.pointer_busy=True
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):r.up(cleanup=True)
|
||||
self.assertEqual(r.report['unconfirmed_held_key'],'F');self.assertTrue(r.keys)
|
||||
self.assertTrue(r.report['held_release_errors'])
|
||||
if fault!='up':self.assertEqual(len(r.calls),1)
|
||||
def test_mapping_capture_and_late_cleanup_use_original_keycode(self):
|
||||
r=Rig();r.down();r.codes[0x66]=42;r.scene.sleep(6)
|
||||
with self.assertRaises(ValueError):r.up()
|
||||
r.up(cleanup=True);self.assertEqual(r.calls[-1][1],41);self.assertFalse(r.keys)
|
||||
self.assertTrue(r.report['held_events'][-1]['finally'])
|
||||
def test_native_final_guard_rechecks_six_seconds_pose_age_and_idle(self):
|
||||
for fault in ('late','idle','pointer','foreign','mapping','lease','not_after'):
|
||||
r=Rig()
|
||||
if fault=='late':r.scene.guard_cost=.3
|
||||
if fault=='idle':r.keys.add(25)
|
||||
if fault=='pointer':r.pointer_busy=True
|
||||
if fault=='foreign':r.mouse.owner['pid']=11
|
||||
if fault=='mapping':r.codes[0x66]=42
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):
|
||||
r.adapter.down_f(r.guard,deadline=40 if fault=='lease' else 35,not_after=40 if fault=='not_after' else 30.5)
|
||||
self.assertEqual(r.calls,[])
|
||||
def test_release_has_two_attempt_cap_and_no_other_adapter_methods(self):
|
||||
r=Rig();r.down();r.fail_up=True
|
||||
for _ in range(2):
|
||||
with self.assertRaises(ValueError):r.up(True)
|
||||
n=len(r.calls)
|
||||
with self.assertRaisesRegex(ValueError,'repeated'):r.up(True)
|
||||
self.assertEqual(len(r.calls),n)
|
||||
for method in ('send','move','pulse'):self.assertFalse(hasattr(r.adapter,method))
|
||||
|
||||
class ObserverTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):cls.frames,*cls.proofs=raw_fixture()
|
||||
def test_real_native_reader_keeps_world_turn_receipts_and_moving_original_geometry(self):
|
||||
frames=copy.deepcopy(self.frames);selection=self.proofs[3];anchor=selection['final_sample']
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
p=Path(folder)/'console.log';p.write_text('');now=[0.]
|
||||
o=h.Observer(p,'test',clock=lambda:now[0],enable_look=True,enable_approach=True,enable_selection=True,enable_held=True)
|
||||
try:
|
||||
text=raw(frames);lines=sorted(text.splitlines(True),key=lambda line:int(__import__('re').search(r'sample=(\d+)',line)[1]))
|
||||
initial=[];future=[]
|
||||
for line in lines:
|
||||
import re
|
||||
m=re.search(r'sample=(\d+)',line)
|
||||
(initial if int(m[1])<=anchor else future).append(line)
|
||||
now[0]=frames[anchor]['real_ms']/1000
|
||||
with p.open('a') as f:f.write(''.join(initial))
|
||||
while o.offset<p.stat().st_size:o.poll()
|
||||
first=o.selection_snapshots();self.assertIn('turn',first[-1]);self.assertEqual(first[-1]['world_ms'],frames[anchor]['world_ms'] if 'world_ms' in frames[anchor] else frames[anchor]['real_ms'])
|
||||
receipt=first[-1]['received_monotonic'];o.begin_held(anchor)
|
||||
cursor=0;found=False
|
||||
while cursor<len(future):
|
||||
# One complete native sample at a time, preserving receipt time.
|
||||
block=[];i=int(__import__('re').search(r'sample=(\d+)',future[cursor])[1])
|
||||
while cursor<len(future) and int(__import__('re').search(r'sample=(\d+)',future[cursor])[1])==i:block.append(future[cursor]);cursor+=1
|
||||
now[0]=frames[i]['real_ms']/1000
|
||||
with p.open('a') as f:f.write(''.join(block))
|
||||
batch=o.held_snapshots()
|
||||
if batch[0]['sample']==anchor:self.assertEqual(batch[0]['received_monotonic'],receipt)
|
||||
if batch[-1]['phase']==2:
|
||||
found=True;self.assertNotEqual(batch[-1]['geometry']['context'],first[-1]['geometry']['context']);self.assertIn('world_ms',batch[-1])
|
||||
self.assertTrue(found)
|
||||
with self.assertRaises(ValueError):o.begin_held(anchor)
|
||||
with self.assertRaises(ValueError):o.approach_data()
|
||||
finally:o.close()
|
||||
def test_cancelled_or_replaced_console_cannot_re_adopt(self):
|
||||
for fault in ('replace','cancel'):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
p=Path(folder)/'console.log';p.write_text('');o=h.Observer(p,'test',enable_look=True,enable_approach=True,enable_selection=True,enable_held=True)
|
||||
try:
|
||||
if fault=='replace':p.rename(p.with_suffix('.old'));p.write_text('')
|
||||
else:p.write_text('ERR INPUT OBSERVE: run=test sample=0 real=0 world=0 CANCELLED original-binding-changed\n')
|
||||
with self.subTest(fault=fault),self.assertRaises(ValueError):o.held_data()
|
||||
finally:o.close()
|
||||
def test_compaction_preserves_raw_world_reference_without_duplicate_pose(self):
|
||||
full={'sample':1,'real_ms':2,'world_ms':3,'received_monotonic':4.,'origin':[1,2,3],'turn':[0,0,0]}
|
||||
report={'approach_requested':True,'held_action_requested':True,'held_action_proof':{'samples':[full]}}
|
||||
compact=h.report_for_write(report)
|
||||
self.assertEqual(compact['held_action_proof']['samples'],[{k:full[k] for k in ('sample','real_ms','world_ms','received_monotonic')}]);self.assertIn('origin',full)
|
||||
|
||||
class FullIntegrationTests(unittest.TestCase):
|
||||
def test_actual_f_adapter_policy_and_unmocked_final_raw_verifier(self):
|
||||
from test_prison_coarse_facing import RawTests
|
||||
from ordinary_gate_choices import join_choice_snapshots
|
||||
RawTests.setUpClass()
|
||||
frames,look,route,facing,selection=copy.deepcopy(RawTests.fixture)
|
||||
scene=Scene();end=selection['samples'][-1]
|
||||
scene.now=selection['finished_monotonic'];scene.next_sample=(end['real_ms']-2000)/1000+.1
|
||||
scene.geometry=copy.deepcopy(selection['geometry']);scene.origin=list(end['origin'])
|
||||
scene.camera=list(end['camera']);scene.direction=list(end['direction']);scene.actions=copy.deepcopy(selection['actions'])
|
||||
make=scene.make
|
||||
def snapshot():
|
||||
item=make();item['sample']=int(round((2000+scene.now*1000)/100));item['real_ms']=2000+int(round(scene.now*1000));item['world_ms']=item['real_ms']
|
||||
item['choices'].update(nonce='test',sample=item['sample'],real_ms=item['real_ms'])
|
||||
if item['phase']==1:item['choices']['selected_id']=scene.actions[1]['id']
|
||||
return item
|
||||
scene.make=snapshot;scene.snapshot=copy.deepcopy(end);scene.snapshot.update(turn=[0,0,0],world_ms=end['real_ms'])
|
||||
scene.history=[copy.deepcopy(scene.snapshot)];scene.last_read=end['sample']-1
|
||||
for item in selection['samples']:item.update(turn=[0,0,0],world_ms=item['real_ms'])
|
||||
scene.report={'look_proof':look,'approach_proof':route,'facing_proof':facing,'action_selection_proof':selection}
|
||||
rig=Rig(scene);report=rig.report
|
||||
def content():
|
||||
for item in scene.history:frames[item['sample']]=copy.deepcopy(item)
|
||||
text=raw(frames);return text,join_choice_snapshots(text,'test')
|
||||
def begin(anchor):
|
||||
self.assertEqual(anchor,selection['final_sample']);scene.last_read=anchor-1
|
||||
observer=SimpleNamespace(enable_held=True,nonce='test',ready_at=0.,ready_real=2000,queued_real=0,
|
||||
approach_data=content,held_data=content,begin_held=begin,held_snapshots=scene.observe)
|
||||
with patch.object(h,'notify',side_effect=scene.notifications.append):
|
||||
h.run_held_action(rig.guard,rig.keyboard,observer,rig.mouse,report,scene.clock,scene.sleep)
|
||||
self.assertTrue(report['held_action_proof']['completed']);self.assertEqual([c[2] for c in rig.calls],[1,0])
|
||||
self.assertEqual(report['held_release_errors'],[]);self.assertFalse(rig.keys)
|
||||
self.assertTrue(any(e['kind']=='held-key-pending' for e in scene.notifications))
|
||||
|
||||
class SupervisorTests(unittest.TestCase):
|
||||
def pending(self):
|
||||
s=h.HeldSupervisor(True);s.consume({'kind':'held-observer-deadline','deadline_monotonic':44.5},2)
|
||||
s.consume({'kind':'held-action-pending','deadline_monotonic':44.0},30)
|
||||
s.consume({'kind':'held-key-pending','key':'F','deadline_monotonic':35},30)
|
||||
return s
|
||||
def test_five_second_lease_does_not_replace_other_key_supervision(self):
|
||||
s=self.pending();self.assertFalse(s.expired(31.6));self.assertTrue(s.expired(35));self.assertEqual(s.key,'F')
|
||||
self.assertFalse(s.consume({'kind':'release-confirmed','key':'W'},31.7));self.assertEqual(s.key,'F')
|
||||
s.cleanup_ack({'kind':'held-key-release-confirmed','key':'F'});self.assertIsNone(s.key)
|
||||
def test_f_and_observer_deadlines_cannot_be_refunded_or_renewed(self):
|
||||
for event in ({'kind':'held-key-pending','key':'F','deadline_monotonic':36},{'kind':'held-action-pending','deadline_monotonic':44.5},{'kind':'held-observer-deadline','deadline_monotonic':45},{'kind':'held-key-release-confirmed','key':'W'}):
|
||||
s=self.pending();s.consume(event,31);self.assertTrue(s.expired(31))
|
||||
s=self.pending();s.consume({'kind':'held-key-release-confirmed','key':'F'},34);self.assertFalse(s.expired(43.9));self.assertTrue(s.expired(44))
|
||||
self.assertFalse(s.expired(44,proof_finished=True))
|
||||
def test_unrequested_missing_outer_lease_busy_or_late_ack_refuses(self):
|
||||
s=h.HeldSupervisor(False);s.consume({'kind':'held-observer-deadline','deadline_monotonic':44},2);self.assertTrue(s.failed)
|
||||
for before in ('missing','busy'):
|
||||
s=h.HeldSupervisor(True)
|
||||
if before=='busy':s.consume({'kind':'held-observer-deadline','deadline_monotonic':44},2)
|
||||
s.consume({'kind':'held-key-pending','key':'F','deadline_monotonic':35},30,other_pending=before=='busy');self.assertTrue(s.failed)
|
||||
s=self.pending();s.consume({'kind':'held-key-release-confirmed','key':'F'},35);self.assertTrue(s.failed);self.assertIsNone(s.key)
|
||||
def test_existing_supervise_stops_stuck_f_and_retains_uncertainty(self):
|
||||
events=[{'kind':'held-observer-deadline','deadline_monotonic':44},{'kind':'held-key-pending','key':'F','deadline_monotonic':5.2}]
|
||||
class Pipe:
|
||||
def poll(self,*a):return bool(events)
|
||||
def recv(self):return events.pop(0)
|
||||
def close(self):pass
|
||||
class Process:
|
||||
pid=123;exitcode=0;alive=True
|
||||
def start(self):pass
|
||||
def is_alive(self):return self.alive
|
||||
def terminate(self):pass
|
||||
def join(self,*a):self.alive=False
|
||||
def kill(self):self.alive=False
|
||||
process=Process();pipe=Pipe();clock=[0.]
|
||||
def ticking():clock[0]+=.1;return clock[0]
|
||||
context=SimpleNamespace(Pipe=lambda **kw:(pipe,pipe),Process=lambda **kw:process)
|
||||
with patch.object(h.multiprocessing,'get_context',return_value=context),patch.object(h.time,'monotonic',side_effect=ticking),patch.object(h.signal,'signal'),patch.object(h,'stop_exact_child'),patch.object(h,'supervision_failure') as failed:
|
||||
self.assertEqual(h.supervise(['--enable-private-held-action-proof']),1)
|
||||
self.assertEqual(failed.call_args.args[1],'F');self.assertGreaterEqual(clock[0],5.2)
|
||||
|
||||
class FlagTests(unittest.TestCase):
|
||||
def test_exact_native_nonce_and_explicit_flag_chain(self):
|
||||
base=['--enable-private-keyboard-proof','--prefix','/x','--game-executable','/g','--profile','/p','--report','/r','--host-display',':0','--nonce','N']
|
||||
flags=['--enable-private-look-proof','--enable-private-approach-proof','--enable-private-alignment-proof','--enable-private-selection-proof','--enable-private-coarse-approach-proof','--enable-private-coarse-facing-proof','--enable-private-held-action-proof']
|
||||
self.assertFalse(h.arguments(base).enable_private_held_action_proof)
|
||||
args=h.arguments(base+flags)
|
||||
for missing in flags[:-1]:
|
||||
with self.assertRaises(SystemExit),patch('sys.stderr'):h.arguments(base+[f for f in flags if f!=missing])
|
||||
native=['-errOrdinaryGateObserve','N','-errOrdinaryGateChoices','N','-errOrdinaryHeldAction','N'];h.gate_options(native,args)
|
||||
for extra in ([],['-errOrdinaryHeldAction','wrong'],['-errOrdinaryHeldActionAlias','N'],['-errOrdinaryHeldAction','N']*2):
|
||||
with self.assertRaises(ValueError):h.gate_options(native[:-2]+extra,args)
|
||||
with self.assertRaises(ValueError):h.gate_options(native,h.arguments(base+flags[:-1]))
|
||||
def test_sequence_finally_preserves_later_uncertain_f_over_clean_approach(self):
|
||||
observer=SimpleNamespace(events=['READY'],poll=lambda:None,fresh=lambda:None,ready_at=0.,ready_real=2000,queued_real=0,
|
||||
enable_approach=True,enable_selection=True,enable_held=True)
|
||||
report={'held_action_requested':True,'coarse_facing_requested':True,'coarse_approach_requested':True}
|
||||
guard=SimpleNamespace(check=lambda:None);keyboard=SimpleNamespace(require_idle=lambda:None)
|
||||
def approach(*args):report['approach_proof']={'completed':True,'unconfirmed_held_key':None,'paired_release_errors':[]}
|
||||
def held(*args):
|
||||
report['unconfirmed_held_key']='F';report['paired_release_errors'].append('captured owner lost during F release')
|
||||
raise h.Refused('F remains uncertain')
|
||||
with patch.object(h,'perform_look'),patch.object(h,'run_approach',side_effect=approach),patch.object(h,'run_selection'),patch.object(h,'run_held_action',side_effect=held),patch.object(h,'notify'):
|
||||
with self.assertRaisesRegex(ValueError,'F remains uncertain'):
|
||||
h.sequence(guard,keyboard,observer,report,clock=lambda:1.,sleep=lambda n:None,mouse=object(),approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=True,held_action=True)
|
||||
self.assertEqual(report['unconfirmed_held_key'],'F');self.assertFalse(report['all_attempted_presses_released']);self.assertFalse(report['sequence_completed'])
|
||||
|
||||
def test_no_dynamic_held_capability_from_report_alone(self):
|
||||
with self.assertRaisesRegex(ValueError,'Held action'):
|
||||
h.sequence(None,None,None,{'held_action_requested':True},held_action=False)
|
||||
with self.assertRaises(ValueError):h.Observer(Path('/missing'),'N',enable_held=True)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Opt-in held runner wiring and independent proof boundary; no runtime or input."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace, ModuleType
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
|
||||
import headless_game as runner
|
||||
import test_relative_look_runner as selection_fixtures
|
||||
import test_headless_input_exercise_runner as exercise_fixtures
|
||||
from test_ordinary_input_runner import NONCE
|
||||
|
||||
MODULES=('headless_input_proof.py','headless_display_probe.py','prison_approach_proof.py','ordinary_gate_evidence.py','ordinary_look_evidence.py','prison_approach_evidence.py','prison_alignment_proof.py','prison_alignment_evidence.py','ordinary_gate_choices.py','prison_action_selection_proof.py','prison_action_selection_evidence.py','prison_held_action_proof.py','prison_held_action_evidence.py')
|
||||
|
||||
class HeldRunnerTests(unittest.TestCase):
|
||||
def options(self):
|
||||
return dict(exercise_prison_held_action=True,exercise_prison_coarse_facing=True,exercise_prison_coarse_approach=True,exercise_prison_selection=True,exercise_prison_alignment=True,observe_prison_choices=True,exercise_prison_approach=True,exercise_relative_look=True,observe_prison_gate=True,exercise_player_input=True,observe_player_input=True,inspect_headless_display=True,project='chat',settle=50)
|
||||
def test_all_prior_capabilities_required_before_filesystem_or_launch(self):
|
||||
good=self.options();runner.validate_input_exercise(SimpleNamespace(**good));runner.validate_input_exercise(SimpleNamespace())
|
||||
for change in [{key:False} for key in good if key not in ('exercise_prison_held_action','project','settle')]+[{'exercise_prison_held_action':1},{'exercise_prison_held_action':'yes'},{'settle':49},{'project':runner.CONTROL},{'packed_core':Path('/packed')},{'packed_addons':Path('/packed')},{'packed_session':Path('/packed')}]:
|
||||
with self.subTest(change=change),patch.object(runner,'packed_request',side_effect=AssertionError('preflight reached filesystem')):
|
||||
with self.assertRaises(ValueError):runner.run(SimpleNamespace(**(good|change)))
|
||||
def test_actual_cli_parser_forwards_optin_and_preserves_default(self):
|
||||
for enabled in (False,True):
|
||||
argv=['headless_game.py','--compat','/private/unlaunched']
|
||||
if enabled:
|
||||
argv+=['--'+key.replace('_','-') for key,value in self.options().items() if value is True]
|
||||
argv+=['--project','chat','--settle','50']
|
||||
captured=[]
|
||||
def fake_run(args):runner.validate_input_exercise(args);captured.append(args);return 17
|
||||
with patch.object(sys,'argv',argv),patch.object(runner,'run',side_effect=fake_run):
|
||||
self.assertEqual(runner.main(),17)
|
||||
self.assertEqual(captured[0].exercise_prison_held_action,enabled)
|
||||
def setup_command(self,root,held=True):
|
||||
(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
|
||||
for name in MODULES:(root/'tools'/name).write_text('# private stub '+name)
|
||||
native=['gamescope','--backend','headless','--','/proton','run','/game']
|
||||
ordinary=runner.input_observer_command(native,'chat',None,NONCE,True,True,True,held_action=held)
|
||||
wrapped,display=runner.display_probe_command(ordinary,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
|
||||
return native,ordinary,wrapped,(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display)
|
||||
def test_native_and_helper_flags_exact_nonce_inside_existing_boundary(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder);native,ordinary,wrapped,args=self.setup_command(root)
|
||||
cmd,context=runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=True,held_action=True)
|
||||
self.assertEqual(ordinary[-2:],['-errOrdinaryHeldAction',NONCE]);self.assertEqual(cmd[-len(wrapped[4:]):],wrapped[4:]);self.assertEqual(context['command'],wrapped[4:])
|
||||
self.assertEqual(cmd.count('--enable-private-held-action-proof'),1);self.assertTrue(context['held_action_requested'])
|
||||
self.assertEqual(context['held_module_sha256'],{name:hashlib.sha256((root/'tools'/name).read_bytes()).hexdigest() for name in MODULES[-2:]})
|
||||
normal=runner.input_observer_command(native,'chat',None,NONCE,True,True,True)
|
||||
self.assertEqual(ordinary,normal+['-errOrdinaryHeldAction',NONCE])
|
||||
for tail in ([],['-errOrdinaryHeldAction'],['-errOrdinaryHeldAction','foreign'],['-errOrdinaryHeldAction',NONCE]*2,['-errOrdinaryHeldAlias',NONCE]):
|
||||
altered=wrapped[:-2]+tail;changed=(altered,*args[1:])
|
||||
with self.subTest(tail=tail),self.assertRaises(ValueError):runner.input_exercise_command(*changed,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=True,held_action=True)
|
||||
with self.assertRaises(ValueError):runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=True)
|
||||
with self.assertRaises(ValueError):runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,held_action=True)
|
||||
def test_no_implicit_native_or_helper_activation_and_module_refusal(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
root=Path(folder);native,ordinary,wrapped,args=self.setup_command(root,False)
|
||||
cmd,context=runner.input_exercise_command(*args,look=True,approach=True,alignment=True,selection=True,coarse_approach=True,coarse_facing=True)
|
||||
self.assertNotIn('--enable-private-held-action-proof',cmd);self.assertNotIn('held_module_sha256',context);self.assertFalse(context.get('held_action_requested',False))
|
||||
for flags in ((False,True,True),(True,False,True),(True,True,False)):
|
||||
with self.assertRaises(ValueError):runner.input_observer_command(native,'chat',None,NONCE,*flags,held_action=True)
|
||||
for duplicate in ('-errOrdinaryHeldAction','-errOrdinaryHeldAlias'):
|
||||
with self.assertRaises(ValueError):runner.input_observer_command(native+[duplicate,NONCE],'chat',None,NONCE,True,True,True,held_action=True)
|
||||
for name in MODULES[-2:]:
|
||||
path=root/'tools'/name;old=path.read_bytes();path.unlink()
|
||||
with self.assertRaises(ValueError):runner.held_module_hashes(root)
|
||||
path.symlink_to(root/'tools'/MODULES[0])
|
||||
with self.assertRaises(ValueError):runner.held_module_hashes(root)
|
||||
path.unlink();path.write_bytes(old)
|
||||
def fixture(self,root):
|
||||
session,context,report,observation=selection_fixtures.SelectionRunner.make_selection(self,root)
|
||||
context.update(held_action_requested=True,coarse_facing_requested=True,coarse_approach_requested=True,held_module_sha256=runner.held_module_hashes(runner.ROOT))
|
||||
report.update({k:context[k] for k in ('held_action_requested','coarse_facing_requested','coarse_approach_requested','held_module_sha256')})
|
||||
report['approach_proof']['profile']='coarse';report['facing_proof']['profile']='coarse'
|
||||
held={'requested':True,'completed':True,'input_attempted':True,'status':'verified','down_monotonic':32.,'up_monotonic':36.2,'release_confirmed':True,'unconfirmed_held_key':None,'paired_release_errors':[],'notification_errors':[], 'events':[{'key':'F','down':down,'monotonic':at,'attempted_monotonic':at-.01,'confirmed':True} for down,at in ((True,32.),(False,36.2))]}
|
||||
report.update(held_action_proof=held,held_release_errors=[],held_events=[{'key':'F','down':down,'monotonic':at,'sent':True} for down,at in ((True,32.),(False,36.2))])
|
||||
return session,context,report,observation
|
||||
def result(self,root,change=None,accepted=True,prior='game-started'):
|
||||
session,context,report,observation=self.fixture(root)
|
||||
if change:change(context,report,observation)
|
||||
raw=(json.dumps(report,indent=2)+'\n').encode();Path(context['report']).write_bytes(raw)
|
||||
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
|
||||
selection=ModuleType('prison_action_selection_evidence');selection.validate_selection_evidence=lambda *a,**kw:{'verified':True}
|
||||
held=ModuleType('prison_held_action_evidence');calls=[]
|
||||
def validate(*args,**kwargs):calls.append((args,kwargs));return {'verified':accepted,'held_action_verified':accepted,'unlock_observed':accepted,'gate_motion_observed':accepted}
|
||||
held.validate_held_action_evidence=validate
|
||||
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_action_selection_evidence':selection,'prison_held_action_evidence':held}):
|
||||
status,proof=runner.input_exercise_result(prior,context,session,observation,{'verified':True})
|
||||
self.assertEqual((session/'input-proof.json').read_bytes(),raw)
|
||||
return status,proof,calls,context,report
|
||||
def test_raw_validator_receives_exact_archived_content_and_coarse_authority(self):
|
||||
for accepted,prior in ((True,'game-started'),(False,'game-started'),(True,'game-failed'),(True,'cleanup-failed')):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
status,proof,calls,context,report=self.result(Path(folder),accepted=accepted,prior=prior)
|
||||
self.assertEqual(calls,[(('native selection',NONCE,report['look_proof'],report['approach_proof'],report['facing_proof'],report['action_selection_proof'],report['held_action_proof']),{'facing_profile':'coarse'})])
|
||||
self.assertEqual(proof['verified'],accepted);self.assertFalse(proof['gameplay_qualified'])
|
||||
self.assertEqual(status,'input-exercise-incomplete' if not accepted and prior=='game-started' else prior)
|
||||
self.assertIn('no gate passage',proof['scope'])
|
||||
def test_wrong_hash_scope_or_release_and_extra_input_never_reach_raw_credit(self):
|
||||
changes=[lambda c,p,o:p.update(held_action_requested=False),lambda c,p,o:p.update(held_action_requested=1),lambda c,p,o:c.update(held_action_requested=1),lambda c,p,o:p.update(held_module_sha256={}),lambda c,p,o:c.update(held_module_sha256={}),lambda c,p,o:c.update(coarse_facing_requested=False),lambda c,p,o:p.update(held_release_errors=['failed']),lambda c,p,o:p.update(unconfirmed_held_key='F'),lambda c,p,o:p.update(all_attempted_presses_released=False),lambda c,p,o:p['held_events'].append(copy.deepcopy(p['held_events'][1])),lambda c,p,o:p['held_events'][0].update(key='W'),lambda c,p,o:p['held_events'][0].update(down=1),lambda c,p,o:p['held_events'][0].update(monotonic=True),lambda c,p,o:p['held_events'][0].update(monotonic=float('nan')),lambda c,p,o:p['held_events'][0].update(sent=False),lambda c,p,o:p['held_events'][1].update(finally_=True),lambda c,p,o:p['held_action_proof'].update(release_confirmed=False),lambda c,p,o:p['held_action_proof'].update(notification_errors=['failed']),lambda c,p,o:p['held_action_proof']['events'][1].update(cleanup=True),lambda c,p,o:p['held_action_proof']['events'][1].update(monotonic=36.199),lambda c,p,o:p['held_action_proof'].update(up_monotonic=36.199),lambda c,p,o:p['held_action_proof'].update(status='failed-after-input'),lambda c,p,o:o.update(sampler_complete=False)]
|
||||
for change in changes:
|
||||
with self.subTest(change=change),tempfile.TemporaryDirectory() as folder:
|
||||
status,proof,calls,*_=self.result(Path(folder),change)
|
||||
self.assertEqual(status,'input-exercise-incomplete');self.assertFalse(proof['verified']);self.assertEqual(calls,[])
|
||||
def test_late_or_reversed_pair_refuses_even_with_internally_equal_timestamps(self):
|
||||
for down,up in ((32.,37.001),(32.,31.),(31.,35.)):
|
||||
def change(c,p,o):
|
||||
for i,(name,value) in enumerate((('down_monotonic',down),('up_monotonic',up))):
|
||||
p['held_events'][i]['monotonic']=value;p['held_action_proof'][name]=value;p['held_action_proof']['events'][i]['monotonic']=value
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
status,proof,calls,*_=self.result(Path(folder),change);self.assertFalse(proof['verified']);self.assertEqual(calls,[])
|
||||
def test_unrequested_F_or_held_result_refuses_and_exact_failure_poll_is_bound(self):
|
||||
for payload in ({'held_events':[{'key':'F'}]},{'held_action_proof':{}},{'held_action_requested':True},{'held_release_errors':['uncertain']}):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
session,context,report,observation=exercise_fixtures.ExerciseTests.make_proof(self,Path(folder));report.update(payload);Path(context['report']).write_text(json.dumps(report))
|
||||
status,proof=runner.input_exercise_result('game-started',context,session,observation,{'verified':True});self.assertFalse(proof['verified'])
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
session,context,report,observation=self.fixture(Path(folder));report.update(error='held release failed',sequence_completed=False)
|
||||
Path(context['report']).write_text(json.dumps(report));self.assertTrue(runner.input_exercise_poll_failure(context));self.assertEqual(runner.input_exercise_poll_result(context,None,True),'game-failed')
|
||||
for update in ({'held_action_requested':False},{'held_action_requested':1},{'held_module_sha256':{}}):
|
||||
Path(context['report']).write_text(json.dumps(report|update));self.assertFalse(runner.input_exercise_poll_failure(context))
|
||||
|
||||
class FullRawHeldRunnerTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
from test_prison_held_action_evidence import build
|
||||
cls.native_fixture=build('coarse')
|
||||
def test_actual_policy_chain_through_all_raw_validators_and_runner(self):
|
||||
from test_prison_held_action_evidence import raw
|
||||
for fault in (None,'native_progress','native_door_motion','adapter_timestamp','expected_profile'):
|
||||
with self.subTest(fault=fault),tempfile.TemporaryDirectory() as folder:
|
||||
frames,look,route,facing,selected,held=copy.deepcopy(self.native_fixture)
|
||||
session,context,report,observation=HeldRunnerTests.fixture(self,Path(folder))
|
||||
context['nonce']='test';report['nonce']='test';observation['nonce']='test'
|
||||
if fault=='native_progress':frames[held['progress_samples'][0]]['progress']=3.9
|
||||
if fault=='native_door_motion':
|
||||
for index in range(held['transition_sample'],held['final_sample']+1):frames[index]['door_state']=0
|
||||
report.update(look_proof=look,approach_proof=route,facing_proof=facing,action_selection_proof=selected,held_action_proof=held)
|
||||
report['events']=copy.deepcopy(route['events'])
|
||||
report['wheel_events']=[{'button':5,'down':down,'sent':True,'monotonic':selected['wheel'][field]} for down,field in ((True,'down_monotonic'),(False,'up_monotonic'))]
|
||||
report['held_events']=[{'key':'F','down':down,'sent':True,'monotonic':held[field]} for down,field in ((True,'down_monotonic'),(False,'up_monotonic'))]
|
||||
if fault=='adapter_timestamp':report['held_events'][1]['monotonic']+=.001
|
||||
if fault=='expected_profile':context['coarse_facing_requested']=False;report['coarse_facing_requested']=False
|
||||
# Match the real wrapper's compact serialization, leaving raw native logs
|
||||
# as the independent source for every state/position/progress measurement.
|
||||
for record in (route,facing,selected,held):
|
||||
record['samples']=[{key:x[key] for key in ('sample','real_ms','received_monotonic')} for x in record['samples']]
|
||||
content=raw(frames);(session/'console.log').write_text(content)
|
||||
original=(json.dumps(report,indent=1)+'\n').encode();Path(context['report']).write_bytes(original)
|
||||
status,result=runner.input_exercise_result('game-started',context,session,observation,{'verified':True})
|
||||
self.assertEqual((session/'input-proof.json').read_bytes(),original)
|
||||
if fault is None:
|
||||
self.assertEqual(status,'game-started',result);self.assertTrue(result['verified'],result)
|
||||
self.assertTrue(result['held_action_proof']['held_action_verified']);self.assertFalse(result['held_action_proof']['escape_verified'])
|
||||
self.assertEqual(result['held_action_proof']['transition_sample'],held['transition_sample'])
|
||||
else:self.assertEqual(status,'input-exercise-incomplete',result);self.assertFalse(result['verified'],result)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -263,7 +263,8 @@ class SelectionRunner(unittest.TestCase):
|
||||
session,context,report,observation=self.make_selection(Path(folder));Path(context['report']).write_text(json.dumps(report))
|
||||
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
|
||||
selected=ModuleType('prison_action_selection_evidence');calls=[]
|
||||
def validate(*args):calls.append(args);return {'verified':accepted}
|
||||
def validate(*args, facing_profile='standard'):
|
||||
self.assertEqual(facing_profile,'standard');calls.append(args);return {'verified':accepted}
|
||||
selected.validate_selection_evidence=validate
|
||||
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_action_selection_evidence':selected}):
|
||||
outcome,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
|
||||
|
||||
@@ -31,7 +31,8 @@ struct Life {bool UsedLife=false;};
|
||||
struct Roster {bool enter=true;Life life;bool CanEnter(int,int){return enter;}Life* Get(int){return &life;}};
|
||||
struct Catalog {int PrisonerPrefab=1;};
|
||||
struct Faction {} faction;
|
||||
struct Mode {bool active=true;int phase=1;Text error;Roster roster;Catalog catalog;Faction* factionPtr=&faction;
|
||||
struct Controller;
|
||||
struct Mode {int exhausted=0;void InitialAdmissionExhausted(int,Controller*){exhausted++;}bool active=true;int phase=1;Text error;Roster roster;Catalog catalog;Faction* factionPtr=&faction;
|
||||
static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}
|
||||
Text PreparationError(){return error;}Roster* Players(){return &roster;}Catalog* Factions(){return &catalog;}Faction* PlayerFaction(){return factionPtr;}};
|
||||
Mode* Mode::current=nullptr;
|
||||
@@ -80,7 +81,9 @@ int main(){
|
||||
assert(!logic.m_Pending.Contains(1)&&!logic.m_AuthorizedPositions.Contains(1));
|
||||
assert(!mode.roster.life.UsedLife);
|
||||
}
|
||||
int previousExhausted=mode.exhausted;
|
||||
logic.DoSpawn_S(1);
|
||||
assert(mode.exhausted==previousExhausted+1);
|
||||
assert(logic.observations==1&&logic.reason==Reason::SPAWN_UNAVAILABLE&&logic.retries==30&&logic.observedBudget==30);
|
||||
logic.DoSpawn_S(1);assert(logic.observations==1); // removed attempt record
|
||||
assert(!mode.roster.life.UsedLife);mode.factionPtr=&faction;
|
||||
|
||||
Reference in New Issue
Block a user