Prepare independent successor playtest with spawn recovery and casualty feedback

This commit is contained in:
2026-09-19 22:12:52 +02:00
parent 61e165ef5b
commit 6cf989a060
266 changed files with 134064 additions and 154 deletions
+33 -1
View File
@@ -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')
+43 -3
View File
@@ -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);
+100 -11
View File
@@ -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')
+16 -12
View File
@@ -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));
+12 -4
View File
@@ -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)
+145
View File
@@ -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')
+1
View File
@@ -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();}
+1
View File
@@ -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'''
+3 -1
View File
@@ -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)
+207
View File
@@ -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()
+208
View File
@@ -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()
+271
View File
@@ -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()
+274
View File
@@ -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()
+162
View File
@@ -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()
+2 -1
View File
@@ -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})
+4 -1
View File
@@ -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;