Qualify route gain and observe native gate context through ordinary input

This commit is contained in:
2026-09-19 19:44:22 +02:00
parent ec1071595d
commit 53eaf3894d
72 changed files with 71413 additions and 36 deletions
+133
View File
@@ -0,0 +1,133 @@
"""Execute actual route demand and control callback; native boundaries are fakes."""
from policy_harness import ROOT, body, run
BASE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftFlight.c'
ROUTE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftRouteFlight.c'
RULES = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_AircraftRouteEnvelope.c'
PROBE = ROOT / 'addons/ai/Scripts/Game/ERR/ERR_NativeAircraftRouteProbe.c'
def cpp(text):
for old, new in (
('protected ', ''), ('override ', ''), ('super.', 'Base::'),
('ERR_AircraftFlightState.', 'State::'),
('ERR_AircraftFlightRules.', 'FlightRules::'),
('ERR_AircraftTranslationRules.', 'Shared::'),
('ERR_AircraftRouteRules.', 'RouteRules::'),
('Math.', 'Math::'), ('vector.', 'vector::'),
('CharacterControllerComponent controller', 'int controller'),
('IEntity owner', 'int owner'), ('ActionManager actions', 'Actions* actions'),
('actions.', 'actions->'), ('m_Route.', 'm_Route->'),
):
text = text.replace(old, new)
# Enforce default-initializes scalar locals, unlike C++.
return text.replace('float collective, torque, forward, aside;',
'float collective=0, torque=0, forward=0, aside=0;')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <string>
#include <initializer_list>
using string=std::string;
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
struct Math {
static float Clamp(float v,float lo,float hi){return std::clamp(v,lo,hi);}
static float Min(float a,float b){return std::min(a,b);}
};
struct vector {
float x=0,y=0,z=0;
float& operator[](int i){return i==0?x:(i==1?y:z);}
float operator[](int i)const{return i==0?x:(i==1?y:z);}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator*(float n)const{return {x*n,y*n,z*n};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
};
bool close(float a,float b){return std::abs(a-b)<.000001f;}
struct Shared {
''' + cpp(body(BASE, 'static vector Demand(')) + r'''
};
struct RouteRules {
''' + cpp(body(RULES, 'static vector Demand(')) + r'''
};
struct FlightRules {
''' + cpp(body(BASE, 'static float Vertical(')) + cpp(body(BASE, 'static int CancelAction(')) + r'''
};
struct Actions {bool active=true;bool IsContextActive(const char*){return active;}void ActivateContext(const char*,int){active=true;}};
struct Aircraft {
vector position={0,8,0};
vector GetOrigin(){return position;}
vector GetYawPitchRoll(){return {};}
void GetWorldTransform(vector (&t)[4]){t[0]={1,0,0};t[1]={0,1,0};t[2]={0,0,1};t[3]=position;}
};
struct Controller {bool hover=true;bool GetAutohoverEnabled(){return hover;}explicit operator bool()const{return true;}bool IsEngineOn(){return true;}};
struct Simulation {vector velocity;vector GetVelocity(){return velocity;}vector GetAngularVelocityLocal(){return {};}};
struct Route {vector target={20,0,0};vector Destination(){return target;}bool Contains(vector,float){return true;}};
struct Base {
bool leased=true,owned=true,health=true,grounded=false,control=true,m_Readback=true,m_Cancelled=false;
int m_State=State::TRANSLATING,m_Character=1,m_Pilot=2,m_ActionUpdates=0,m_TranslationUpdates=0,pairs=0,landings=0;
float clock=1000,m_Deadline=240000,m_StageDeadline=160000,m_Stable=-1,m_Collective=0,m_Heading=0,m_LastControl=-1;
float writtenForward=0,writtenAside=0,writtenCollective=0;
vector m_Home;string m_Reason;Actions local;Actions* m_Actions=&local;
Aircraft m_Aircraft;Controller m_Controller;Simulation m_Simulation;
bool Lease(){return leased;}bool Owns(){return owned;}bool Healthy(){return health;}bool Grounded(){return grounded;}bool ControlLease(){return control;}
float Now(){return clock;}void Fail(string){m_State=State::FAILED;}void Relinquish(string){m_State=State::RELINQUISHED;}
void RecordState(){}void Release(bool){}void Shutdown(float){m_State=State::SHUTDOWN;}
bool Landing(float now){landings++;m_State=State::LANDING;m_Stable=-1;m_StageDeadline=Math::Min(m_Deadline,now+65000);return true;}
bool Pair(string increase,string,float value){pairs++;if(increase=="HelicopterCyclicForward")writtenForward=value;if(increase=="HelicopterCyclicRight")writtenAside=value;if(increase=="HelicopterCollectiveIncrease")writtenCollective=value;return true;}
};
struct Flight:Base {
Route route;Route* m_Route=&route;bool m_RouteArrived=false;vector m_LandingPoint;
''' + cpp(body(ROUTE, 'protected override bool Landing(')) + cpp(body(BASE, 'void Cancel(')) + cpp(body(ROUTE, 'override void PrepareActions(')) + r'''
void Apply(){PrepareActions(1,2,m_Actions,.05f);}
};
int main(){
// The unchanged .5m/s requested speed is visible at its zero-error velocity.
assert(RouteRules::Demand({20,900,0},{0,-900,0},{.5f,500,0}).Length()==0);
assert(RouteRules::Demand({200,0,0},{0,0,0},{.5f,0,0}).Length()==0);
// Below saturation the route candidate doubles gain, not desired speed.
auto route=RouteRules::Demand({20,0,0},{0,8,0},{.4f,0,0});
auto shared=Shared::Demand({20,0,0},{0,8,0},{.4f,0,0});
assert(close(route.x,.01f)&&close(shared.x,.005f));
for(float x:{-100.f,-2.f,-.1f,0.f,.1f,2.f,100.f})
for(float z:{-100.f,-1.f,0.f,1.f,100.f})
for(float v:{-100.f,-1.f,0.f,1.f,100.f}){
auto d=RouteRules::Demand({x,999,z},{0,-999,0},{v,999,-v});
assert(d.Length()<=.030001f&&d.y==0);
}
auto away=RouteRules::Demand({20,0,0},{0,8,0},{-1,50,0});assert(close(away.x,.03f)&&away.y==0&&away.z==0);
auto overspeed=RouteRules::Demand({20,0,0},{0,8,0},{1,-50,0});assert(close(overspeed.x,-.03f));
auto brake=RouteRules::Demand({0,0,0},{0,8,0},{1,0,-1});assert(brake.x<0&&brake.z>0&&close(brake.Length(),.03f));
assert(RouteRules::Demand({0,100,0},{0,-100,0},{0,99,0}).Length()==0);
// Execute the real callback: TRANSLATING selects candidate; ordinary landing
// keeps the original .05 gain with precisely the same target and velocity.
Flight translating;translating.m_Simulation.velocity={.4f,0,0};translating.Apply();
assert(close(translating.writtenAside,.01f)&&translating.writtenForward==0&&translating.m_TranslationUpdates==1);
Flight landing;landing.m_State=State::LANDING;landing.m_LandingPoint={20,0,0};landing.m_Simulation.velocity={.4f,0,0};landing.Apply();
assert(close(landing.writtenAside,.005f)&&landing.m_TranslationUpdates==0);
// The actual inherited Cancel and route Landing freeze the original position,
// retain authority/lease, and thereafter use old landing braking—not route gain.
Flight cancelled;cancelled.m_Aircraft.position={10,8,1};cancelled.m_Simulation.velocity={.2f,0,0};cancelled.Cancel("radio lost");
assert(cancelled.m_State==State::LANDING&&cancelled.m_Cancelled&&cancelled.leased&&cancelled.landings==1);
assert(cancelled.m_LandingPoint.x==10&&cancelled.m_LandingPoint.z==1&&cancelled.m_Deadline==240000);
cancelled.Apply();assert(close(cancelled.writtenAside,-.01f));
float deadline=cancelled.m_StageDeadline;cancelled.clock=5000;cancelled.m_Aircraft.position={11,8,1};cancelled.Cancel("duplicate");
assert(cancelled.landings==1&&cancelled.m_LandingPoint.x==10&&cancelled.m_StageDeadline==deadline);
// Grounded landing suppresses both axes; wrong owner or revoked lease writes none.
landing.grounded=true;landing.Apply();assert(landing.writtenAside==0&&landing.writtenForward==0&&landing.writtenCollective==-1);
Flight foreign;foreign.PrepareActions(1,99,foreign.m_Actions,.05f);assert(foreign.pairs==0);
foreign.control=false;foreign.Apply();assert(foreign.pairs==0);
}
''')
# Read-only diagnostic must describe the same state-specific equation, not the
# historical shared demand. It remains a computed sample, not input readback.
diagnostic = body(PROBE, 'protected void Diagnostic(')
assert 'if (state == ERR_AircraftFlightState.TRANSLATING) demand = ERR_AircraftRouteRules.Demand(' in diagnostic
assert 'else if (state == ERR_AircraftFlightState.LANDING) demand = ERR_AircraftTranslationRules.Demand(' in diagnostic
# The shared controller still has no dependency on the experimental route policy.
assert 'ERR_AircraftRouteRules' not in BASE.read_text()
print('PASS: actual route gain/caps/braking, callback selection, old landing/cancellation control and matching read-only telemetry')
+171
View File
@@ -718,6 +718,8 @@ class ApproachTests(unittest.TestCase):
report={'approach_requested':True,'approach_proof':{'samples':full,'motions':[{'dx':128,'dy':0,'monotonic':10,'after_sample':i,'response_sample':i+1} for i in range(16)],'events':[{'key':'W','down':bool(i%2),'monotonic':10} for i in range(24)]},
'look_observation':observer.look_records,'gate_observation':observer.gate_records,
'observer':{'records':observer.raw,'events':observer.events},'provenance_budget':'x'*65536}
report['alignment_requested']=True
report['alignment_proof']={'samples':full[-80:],'motions':[{'dx':128,'dy':0,'monotonic':10,'after_sample':i,'response_sample':i+1} for i in range(6)]}
payload=json.dumps(proof.report_for_write(report),indent=2).encode()
self.assertEqual(len(observer.look_records),451);self.assertEqual(len(observer.gate_records),901)
self.assertLess(len(payload),1024*1024)
@@ -749,4 +751,173 @@ class ApproachTests(unittest.TestCase):
stop.assert_called_once();self.assertIsNone(failed.call_args.args[1])
class AlignmentIntegrationTests(unittest.TestCase):
def test_cli_requires_explicit_approach_and_look(self):
base=['--enable-private-keyboard-proof','--prefix','/tmp/pfx','--game-executable','/tmp/game',
'--profile','/tmp/profile','--report','/tmp/report','--host-display',':0','--nonce','N']
for flags in ([],['--enable-private-look-proof'],['--enable-private-approach-proof']):
with self.assertRaises(SystemExit),patch('sys.stderr'):
proof.arguments(base+flags+['--enable-private-alignment-proof'])
args=proof.arguments(base+['--enable-private-look-proof','--enable-private-approach-proof','--enable-private-alignment-proof'])
self.assertTrue(args.enable_private_alignment_proof)
self.assertFalse(proof.arguments(base).enable_private_alignment_proof)
def test_alignment_follows_completed_approach_without_more_keyboard_events(self):
clock=Clock();o=FakeObserver();o.enable_approach=True;o.ready_at=0;k=FakeKeyboard(o);r={};order=[];notices=[]
def look(*a):order.append('look')
def approach(g,k,o,m,r,c,s):
order.append('approach');k.send('W',True);k.send('W',False)
r['approach_proof']={'completed':True,'final_sample':99,'samples':[{'sample':99}], 'paired_release_errors':[],'unconfirmed_held_key':None}
def alignment(g,k,o,m,r,c,s):
self.assertEqual(order,['look','approach']);self.assertTrue(r['approach_proof']['completed'])
self.assertEqual(notices[-1],{'kind':'approach-finished','successful':True})
order.append('alignment');r['alignment_proof']={'completed':True}
with patch.object(proof,'perform_look',side_effect=look),patch.object(proof,'run_approach',side_effect=approach),patch.object(proof,'run_alignment',side_effect=alignment),patch.object(proof,'notify',side_effect=notices.append):
proof.sequence(FakeGuard(),k,o,r,clock,clock.sleep,mouse=object(),approach=True,alignment=True)
self.assertEqual(order,['look','approach','alignment'])
self.assertEqual(k.events,[('W',True),('W',False)]);self.assertTrue(r['sequence_completed'])
self.assertEqual(r['approach_proof']['samples'],[{'sample':99}])
self.assertEqual(r['approach_proof']['final_sample'],99)
def test_failed_alignment_does_not_erase_approach_or_reactivate_its_lease(self):
clock=Clock();o=FakeObserver();o.enable_approach=True;o.ready_at=0;k=FakeKeyboard(o);r={};notices=[]
def approach(*args):r['approach_proof']={'completed':True,'final_sample':100,'paired_release_errors':[],'unconfirmed_held_key':None}
with patch.object(proof,'perform_look'),patch.object(proof,'run_approach',side_effect=approach),patch.object(proof,'run_alignment',side_effect=proof.Refused('selection missing')),patch.object(proof,'notify',side_effect=notices.append):
with self.assertRaisesRegex(proof.Refused,'selection missing'):
proof.sequence(FakeGuard(),k,o,r,clock,clock.sleep,mouse=object(),approach=True,alignment=True)
self.assertFalse(r['sequence_completed']);self.assertTrue(r['approach_proof']['completed'])
self.assertTrue(all(e['successful'] for e in notices if e['kind']=='approach-finished'))
self.assertEqual(k.events,[])
def test_yaw_only_capability_checks_optin_bounds_original_deadline_and_native_guard(self):
class Fn:
def __init__(self,fn):self.fn=fn
def __call__(self,*args):return self.fn(*args)
events=[];xt=SimpleNamespace(XTestFakeRelativeMotionEvent=Fn(lambda *a:events.append(a) or 1))
query=SimpleNamespace(x=SimpleNamespace(XQueryPointer=Fn(lambda *a:1),XFlush=lambda d:None),d=123)
mouse=proof.RelativeMouse(query,SimpleNamespace(xt=xt,require_idle=lambda:None),{'window':100,'pid':9},enable_approach=True)
guard=FakeGuard();clock=Clock();mouse.validate_pointer=lambda *a:None
with self.assertRaises(proof.Refused):mouse.alignment_move(24,0,guard,deadline=8,clock=clock)
mouse.enable_alignment=True
for dx in (1,-128,128):mouse.alignment_move(dx,0,guard,deadline=8,clock=clock)
for dx,dy in ((0,0),(129,0),(-129,0),(1,1),(True,0),(1,False)):
with self.assertRaises(proof.Refused):mouse.alignment_move(dx,dy,guard,deadline=8,clock=clock)
wrapped=proof.AlignmentMouse(mouse,{},clock,8)
with self.assertRaisesRegex(proof.Refused,'extend'):wrapped.move(1,0,guard,deadline=8.001)
def late(*a):clock.now=8
mouse.validate_pointer=late
with self.assertRaisesRegex(proof.Refused,'deadline'):wrapped.move(1,0,guard,deadline=8)
self.assertEqual(len(events),3)
def test_snapshot_has_native_camera_action_fields_and_independent_alignment_cursor(self):
from test_ordinary_gate_evidence import fixture,NONCE
clean='\n'.join(s for s in fixture().splitlines() if 'CANCELLED' not in s)+'\n'
with tempfile.TemporaryDirectory() as folder:
path=Path(folder)/'console.log';path.write_text('')
o=proof.Observer(path,NONCE,clock=lambda:10.,enable_look=True,enable_approach=True)
try:
with path.open('a') as out:out.write(clean)
first=o.approach_snapshots()[-1];self.assertEqual(first['sample'],30)
self.assertEqual(first['camera'],(6088.58,213.019,7158.56))
for key in ('selected','context_selected','cached_performable','progress_bound','performing'):self.assertIs(first[key],False)
self.assertEqual((first['pressed'],first['progress'],first['duration']),(0,0,4))
o.alignment_previous=27
self.assertEqual([s['sample'] for s in o.alignment_snapshots()],[28,29,30])
self.assertEqual(o.approach_previous,30);self.assertEqual(o.alignment_previous,30)
self.assertEqual(o.approach_snapshots(),[first])
finally:o.close()
def test_failed_independent_approach_blocks_alignment_module(self):
o=SimpleNamespace(ready_at=0,ready_real=3000,queued_real=1000,nonce='N',approach_data=lambda:('raw',{}))
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':False,'errors':['raw route mismatch']}),patch('prison_alignment_proof.perform_alignment') as module:
with self.assertRaisesRegex(proof.Refused,'prerequisite refused'):
proof.run_alignment(None,None,o,None,{'look_proof':{}},lambda:30,lambda s:None)
module.assert_not_called()
def test_alignment_adapter_passes_readonly_keyboard_and_original_lease(self):
import copy
clock=Clock();clock.now=34
route={'completed':True,'final_sample':100,'geometry':{'nonce':'N'},'samples':[{'sample':100,'origin':[1,2,3]}]}
original=copy.deepcopy(route);report={'approach_proof':route,'look_proof':{'baseline_samples':[20,21],'response_sample':22}}
o=SimpleNamespace(ready_at=0,ready_real=3000,queued_real=1000,nonce='N',approach_data=lambda:('raw',{}),
look_records=[{'kind':'sample','sample':21,'direction':[0,0,1]},{'kind':'sample','sample':22,'direction':[.03,0,.999]}],
alignment_snapshots=lambda:[{'sample':100}],alignment_previous=None)
keyboard=SimpleNamespace(require_idle=lambda:None);motions=[];notices=[]
mouse=SimpleNamespace(alignment_move=lambda *a,**k:motions.append((a,k)))
def module(guard,require_idle,m,observe,geometry,calibration,evidence,r,**kwargs):
self.assertTrue(callable(require_idle));self.assertFalse(hasattr(require_idle,'send'))
self.assertEqual(o.alignment_previous,99)
self.assertEqual(calibration['observer_deadline_monotonic'],42.5)
self.assertEqual(calibration['alignment_deadline_monotonic'],42)
m.move(12,0,guard,deadline=42)
r['alignment_proof']={'completed':True,'samples':[{'sample':100}]}
kwargs['notify']({'kind':'alignment-pending','deadline_monotonic':42})
kwargs['notify']({'kind':'alignment-finished','successful':True})
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':True,'last_sample':100}),patch('prison_alignment_proof.perform_alignment',side_effect=module),patch.object(proof,'notify',side_effect=notices.append):
proof.run_alignment(FakeGuard(),keyboard,o,mouse,report,clock,clock.sleep)
self.assertEqual(route,original);self.assertEqual(motions[0][1]['deadline'],42)
self.assertEqual([n['kind'] for n in notices],['alignment-pending','alignment-pending','alignment-finished'])
self.assertTrue(notices[-1]['successful'])
# A later start has only the original remaining lifetime, never a new 8s.
clock.now=40;notices.clear();report.pop('alignment_proof')
def late_module(g,k,m,observe,geometry,calibration,evidence,r,**kwargs):
self.assertEqual(calibration['alignment_deadline_monotonic'],42.5)
clock.now=42.5;r['alignment_proof']={'completed':True}
with patch('prison_approach_evidence.validate_approach_evidence',return_value={'verified':True,'last_sample':100}),patch('prison_alignment_proof.perform_alignment',side_effect=late_module),patch.object(proof,'notify',side_effect=notices.append):
with self.assertRaisesRegex(proof.Refused,'deadline'):proof.run_alignment(FakeGuard(),keyboard,o,mouse,report,clock,clock.sleep)
self.assertFalse(notices[-1]['successful'])
def test_actual_alignment_module_runs_through_helper_capabilities(self):
import copy
from test_prison_alignment_proof import Scene
scene=Scene();report=scene.report;prior=copy.deepcopy(report['approach_proof'])
report['look_proof']={'baseline_samples':[20,21],'response_sample':22}
report['events']=[]
o=SimpleNamespace(ready_at=2.,ready_real=32000,queued_real=30000,nonce='N',approach_data=lambda:('raw',{}),
look_records=[{'kind':'sample','sample':21,'direction':scene.calibration['baseline_direction']},
{'kind':'sample','sample':22,'direction':scene.calibration['response_direction']}],
alignment_snapshots=scene.observe,alignment_previous=None)
mouse=SimpleNamespace(alignment_move=lambda dx,dy,guard,deadline,clock:scene.move(dx,dy,guard,deadline=deadline))
with patch('prison_approach_evidence.validate_approach_evidence',return_value=scene.accepted):
proof.run_alignment(scene,SimpleNamespace(require_idle=scene.idle),o,mouse,report,scene.clock,scene.sleep)
self.assertTrue(report['alignment_proof']['completed']);self.assertGreater(len(scene.motions),0)
self.assertTrue(report['input_sent']);self.assertEqual(report['events'],[])
self.assertEqual(report['approach_proof'],prior)
self.assertLessEqual(report['alignment_proof']['deadline_monotonic'],39.)
with patch('prison_approach_evidence.validate_approach_evidence',return_value=scene.accepted):
with self.assertRaisesRegex(ValueError,'already exists'):
proof.run_alignment(scene,SimpleNamespace(require_idle=scene.idle),o,mouse,report,scene.clock,scene.sleep)
def test_conditional_hashes_and_compaction_preserve_indexed_approach(self):
import copy,hashlib
report={'approach_requested':True,'alignment_requested':True,'approach_proof':{'final_sample':100,'samples':[{'sample':100,'real_ms':12000,'received_monotonic':10,'origin':[1,2,3]}]},'alignment_proof':{'completed':False,'samples':[{'sample':100,'real_ms':12000,'received_monotonic':10,'camera':[1,3,3]}]}}
prior=copy.deepcopy(report);encoded=proof.report_for_write(report)
self.assertEqual(report,prior);self.assertEqual(encoded['approach_proof']['final_sample'],100)
self.assertEqual(encoded['approach_proof']['samples'],encoded['alignment_proof']['samples'])
self.assertEqual(set(encoded['alignment_proof']['samples'][0]),{'sample','real_ms','received_monotonic'})
with tempfile.TemporaryDirectory() as folder:
root=Path(folder)
for name in proof.ALIGNMENT_MODULES:(root/name).write_text(name)
with patch.object(proof,'__file__',str(root/'headless_input_proof.py')):
values=proof.module_hashes(proof.ALIGNMENT_MODULES)
self.assertEqual(set(values),{'prison_alignment_proof.py','prison_alignment_evidence.py'})
self.assertEqual(values['prison_alignment_proof.py'],hashlib.sha256(b'prison_alignment_proof.py').hexdigest())
def test_alignment_watchdog_is_separate_and_cannot_renew(self):
events=[{'kind':'report','metadata':{}},{'kind':'child','identity':{}},
{'kind':'approach-pending','deadline_monotonic':5},{'kind':'approach-finished','successful':True},
{'kind':'alignment-pending','deadline_monotonic':9},{'kind':'alignment-pending','deadline_monotonic':12}]
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;terminated=0
def start(self):pass
def is_alive(self):return self.alive
def terminate(self):self.terminated+=1
def join(self,*a):self.alive=False
def kill(self):self.alive=False
child=Process();pipe=Pipe();clock=Clock()
def ticking():clock.now+=.5;return clock.now
context=SimpleNamespace(Pipe=lambda **k:(pipe,pipe),Process=lambda **k:child)
with patch.object(proof.multiprocessing,'get_context',return_value=context),patch.object(proof.time,'monotonic',side_effect=ticking),patch.object(proof.signal,'signal'),patch.object(proof,'stop_exact_child') as stop,patch.object(proof,'supervision_failure') as failed:
self.assertEqual(proof.supervise([]),1);self.assertEqual(child.terminated,1)
self.assertGreaterEqual(clock.now,9);self.assertLessEqual(clock.now,9.5)
stop.assert_called_once();self.assertIsNone(failed.call_args.args[1])
if __name__ == '__main__': unittest.main()
+152
View File
@@ -0,0 +1,152 @@
"""Independent alignment validation with complete synthetic native-format records."""
import copy
import math
from pathlib import Path
import sys
import unittest
sys.path[:0]=[str(Path(__file__).resolve().parents[1]/'tools'),str(Path(__file__).resolve().parent)]
import prison_alignment_evidence as validator
import prison_approach_evidence as route_validator
import prison_alignment_proof as alignment
from test_prison_approach_proof import Scene as ApproachScene
from test_prison_alignment_proof import Scene as AlignmentScene
def build():
route_scene=ApproachScene();route=route_scene.run()
scene=AlignmentScene()
scene.now=route_scene.now;scene.next_sample=route_scene.next_sample
scene.geometry=copy.deepcopy(route_scene.geometry)
scene.origin=list(route_scene.position);scene.camera=[scene.origin[0],scene.origin[1]+1.7,scene.origin[2]-.1]
scene.angle=route_scene.angle;scene.guard_cost=.18;scene.pending=[]
make=scene.make
def snapshot():
value=make();value['sample']=int(round((2000+scene.now*1000)/100));value['real_ms']=2000+int(round(scene.now*1000))
wanted=math.degrees(math.atan2(scene.geometry['context'][0]-scene.camera[0],scene.geometry['context'][2]-scene.camera[2]))
chosen=abs((wanted-scene.angle+180)%360-180)<=3
for key in ('selected','context_selected','cached_performable','progress_bound'):value[key]=chosen
return value
scene.make=snapshot
scene.prior=copy.deepcopy(route['samples'][-1])
value=snapshot();value['sample']=scene.prior['sample'];value['real_ms']=scene.prior['real_ms'];value['received_monotonic']=scene.prior['received_monotonic']
scene.snapshot=value;scene.history=[copy.deepcopy(value)];scene.last_read=value['sample']-1
scene.calibration.update(nonce='test',ready_real_ms=2000,ready_monotonic=0.,observer_start_real_ms=0,
observer_deadline_monotonic=42.5,alignment_deadline_monotonic=scene.now+8)
scene.report={'approach_proof':route};scene.accepted={'verified':True,'last_sample':route['final_sample']}
proof=scene.run()
look={'requested':True,'completed':True,'started_monotonic':0.,'finished_monotonic':.65,
'baseline_samples':[21,22],'response_sample':24,'return_sample':26,
'motions':[{'dx':24,'dy':0,'monotonic':.25,'after_sample':22,'after_real_ms':2200},
{'dx':-24,'dy':0,'monotonic':.45,'after_sample':24,'after_real_ms':2400}]}
frames={}
for i in range(1,46):
degrees=24*.085 if i in (24,25) else 0.
radians=math.radians(degrees)
frames[i]={'sample':i,'real_ms':100*i,'origin':[98.5,0.,98.5],
'direction':[math.sin(radians),0.,math.cos(radians)],'body_forward':[0.,0.,1.],
'camera':[98.5,1.7,98.4],'input':[0.,0.,0.],'velocity':[0.,0.,0.],
'forward':0.,'inventory':False,'phase':1,'geometry':copy.deepcopy(route_scene.geometry),
'selected':False,'context_selected':False,'cached_performable':False,'progress_bound':False,
'performing':False,'progress':0.,'pressed':0.,'duration':4.,'door_state':0.,'door_control':0.}
for s in route_scene.history:
value=copy.deepcopy(s)
value.update(camera=[s['origin'][0],s['origin'][1]+1.7,s['origin'][2]-.1],
selected=False,context_selected=False,cached_performable=False,progress_bound=False,
performing=False,progress=0.,pressed=0.,duration=4.)
frames[s['sample']]=value
for s in scene.history:frames[s['sample']]=copy.deepcopy(s)
return frames,look,route,proof
def raw(frames):
vector=lambda v:'<'+','.join(format(x,'.10g') for x in v)+'>'
lines=['ERR INPUT OBSERVE: run=test sample=0 real=0 world=0 QUEUED']
for i,s in sorted(frames.items()):
stamp=f'run=test sample={i} real={s["real_ms"]}'
lines.append(f'ERR INPUT OBSERVE: {stamp} world={s["real_ms"]} SAMPLE origin={vector(s["origin"])} forward={s["forward"]} input={vector(s["input"])} velocity={vector(s["velocity"])} inventory={int(s["inventory"])}')
if i==1:lines.append(f'ERR LOOK OBSERVE: {stamp} BASIS native-player-camera-world-transform')
lines.append(f'ERR LOOK OBSERVE: {stamp} SAMPLE direction={vector(s["direction"])} camera={vector(s["camera"])} body={vector(s["origin"])} bodyForward={vector(s["body_forward"])} turn=<0,0,0>')
if i==1:lines.append(f'ERR GATE OBSERVE: {stamp} BASIS registered-prison-lock-native-interaction')
lines.append(f'ERR GATE OBSERVE: {stamp} SAMPLE phase={s["phase"]} selected={int(s["selected"])} context={int(s["context_selected"])} cachedPerformable={int(s["cached_performable"])} progressBound={int(s["progress_bound"])} performing={int(s["performing"])} progress={s["progress"]} duration={s["duration"]} pressed={s["pressed"]}')
g=s['geometry']
lines.append(f'ERR GATE OBSERVE: {stamp} GEOMETRY body={vector(s["origin"])} context={vector(g["context"])} radius={g["radius"]} doorState={s["door_state"]} doorControl={s["door_control"]} prison={vector(g["prison"])} gate={vector(g["gate"])} gateForward={vector(g["gate_forward"])}')
if i==20:lines.append('ERR INPUT OBSERVE: run=test sample=20 real=2000 world=2000 READY')
return '\n'.join(lines)+'\n'
class AlignmentEvidenceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):cls.template=build()
def result(self,mutate=None):
f,l,a,p=copy.deepcopy(self.template)
if mutate:mutate(f,l,a,p)
return validator.validate_alignment_evidence(raw(f),'test',l,a,p)
def reject(self,mutate):
result=self.result(mutate);self.assertFalse(result['verified'],result);self.assertTrue(result['errors'])
def test_actual_approach_and_alignment_modules_pass_unmocked_raw_validators(self):
f,l,a,p=copy.deepcopy(self.template)
prior=route_validator.validate_approach_evidence(raw(f),'test',l,a)
self.assertTrue(prior['verified'],prior)
result=self.result();self.assertTrue(result['verified'],result)
self.assertFalse(result['held_action_verified']);self.assertFalse(result['escape_verified'])
def test_prior_approach_and_calibration_cannot_be_claimed(self):
self.reject(lambda f,l,a,p:a.update(completed=False))
self.reject(lambda f,l,a,p:l.update(completed=False))
self.reject(lambda f,l,a,p:p.update(completed=False))
def test_historical_first_receipt_is_not_rewritten_to_stage_start(self):
f,l,a,p=copy.deepcopy(self.template)
p['started_monotonic']=p['samples'][0]['received_monotonic']+.6
# Still before the first motion; original historical receipts preserved.
result=validator.validate_alignment_evidence(raw(f),'test',l,a,p)
self.assertTrue(result['verified'],result)
def test_full_3d_drift_refused_even_below_each_axis_threshold(self):
def mutation(f,l,a,p):
i=p['initial_sample']+1;f[i]['origin'][0]+=.04;f[i]['origin'][1]+=.04
self.reject(mutation)
def test_action_selection_requires_exact_bound_stable_native_pair(self):
for field,value in [('selected',False),('context_selected',False),('cached_performable',False),('progress_bound',False)]:
with self.subTest(field=field):self.reject(lambda f,l,a,p:f[p['selection_samples'][-1]].update({field:value}))
self.reject(lambda f,l,a,p:p.update(selection_samples=[p['selection_samples'][0],p['selection_samples'][0]]))
def test_any_intermediate_gate_input_or_body_motion_refused(self):
for field,value in [('pressed',1.),('progress',.1),('performing',True),('door_state',.1),('input',[0,0,1])]:
with self.subTest(field=field):self.reject(lambda f,l,a,p:f[a['final_sample']+1].update({field:value}))
def test_deadline_and_original_lifetime_cannot_be_extended(self):
self.reject(lambda f,l,a,p:p.update(deadline_monotonic=p['started_monotonic']+8.1))
self.reject(lambda f,l,a,p:p.update(finished_monotonic=p['deadline_monotonic']+.1))
self.reject(lambda f,l,a,p:a.update(deadline_monotonic=10.))
def test_mouse_axis_caps_gain_sign_and_receipts_are_authoritative(self):
self.reject(lambda f,l,a,p:p['motions'][0].update(dy=1))
self.reject(lambda f,l,a,p:p['motions'][0].update(dx=129))
self.reject(lambda f,l,a,p:p['motions'][0].update(dx=-p['motions'][0]['dx']))
self.reject(lambda f,l,a,p:p['motions'][0].update(monotonic=p['started_monotonic']-.1))
self.reject(lambda f,l,a,p:p['samples'][-1].update(received_monotonic=float('nan')))
def test_intermediate_turn_overshoot_not_hidden_by_later_good_pose(self):
def mutation(f,l,a,p):
m=p['motions'][0];s=f[m['after_sample']+1]
baseline=math.degrees(math.atan2(f[m['after_sample']]['direction'][0],f[m['after_sample']]['direction'][2]))
angle=math.radians(baseline+20);s['direction']=[math.sin(angle),0.,math.cos(angle)]
self.reject(mutation)
def test_no_keyboard_events_or_stale_completion_can_be_smuggled(self):
self.reject(lambda f,l,a,p:p.update(events=[{'key':'F','down':True}]))
self.reject(lambda f,l,a,p:p.update(finished_monotonic=p['samples'][-1]['received_monotonic']+.6))
def test_unsettled_response_or_too_late_acknowledgement_refused(self):
def change(f,l,a,p):
m=p['motions'][0];i=m['response_sample']-1
direction=f[m['response_sample']]['direction'];angle=math.atan2(direction[0],direction[2])+math.radians(.3)
f[i]['direction']=[math.sin(angle),0,math.cos(angle)]
self.reject(change)
self.reject(lambda f,l,a,p:p['motions'][0].update(response_sample=p['final_sample']))
def test_wrong_final_bearing_and_original_queued_bound_refused(self):
def wrong(f,l,a,p):
for i in p['selection_samples']:f[i]['direction']=[0,0,1]
self.reject(wrong)
f,l,a,p=copy.deepcopy(self.template)
content=raw(f).replace('sample=0 real=0 world=0 QUEUED','sample=0 real=1000 world=0 QUEUED')
self.assertFalse(validator.validate_alignment_evidence(content,'test',l,a,p)['verified'])
def test_missing_raw_sample_and_changed_geometry_refused(self):
self.reject(lambda f,l,a,p:f.pop(p['initial_sample']+1))
self.reject(lambda f,l,a,p:f[p['initial_sample']+1]['geometry']['context'].__setitem__(0,999))
if __name__=='__main__':unittest.main()
+175
View File
@@ -0,0 +1,175 @@
"""Synthetic stationary-camera/action simulations only; no native input backend."""
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_alignment_proof as policy
class Scene:
def __init__(self):
self.now=31.;self.next_sample=31.1;self.angle=126.154
self.geometry={'nonce':'N','basis':'registered-prison-lock-native-interaction',
'prison':[6090.,211.444,7160.],'gate':[6090.,211.344,7154.],
'gate_forward':[0.,0.,1.],'context':[6092.77,213.226,7154.],'radius':2.}
self.origin=[6092.42,211.297,7155.81];self.camera=[6092.42,212.921,7155.71]
self.gain=.085;self.delay=.15;self.guard_cost=.08;self.pending=[];self.motions=[];self.notifications=[]
self.select=True;self.bound=True;self.input=[0.,0.,0.];self.velocity=[0.,0.,0.]
self.pressed=0.;self.progress=0.;self.performing=False;self.cancel=False;self.stale=False
self.phase=1;self.door=0.;self.focus=True;self.ignore=False;self.reverse=False
self.snapshot=self.make();self.history=[copy.deepcopy(self.snapshot)];self.last_read=309
self.prior=copy.deepcopy(self.snapshot)
response=math.radians(24*.085)
self.calibration={'nonce':'N','completed':True,'pixels':24,'baseline_direction':[0.,0.,1.],
'response_direction':[math.sin(response),0.,math.cos(response)],
'observer_start_real_ms':30000,'ready_real_ms':32000,'ready_monotonic':2.,
'observer_deadline_monotonic':44.5,'alignment_deadline_monotonic':39.}
self.report={'approach_proof':{'completed':True,'nonce':'N','geometry':copy.deepcopy(self.geometry),
'final_sample':310,'samples':[self.prior]}}
self.accepted={'verified':True,'last_sample':310}
def clock(self):return self.now
def make(self):
r=math.radians(self.angle)
desired=math.degrees(math.atan2(self.geometry['context'][0]-self.camera[0],self.geometry['context'][2]-self.camera[2]))
chosen=self.select and abs(policy.delta(desired,self.angle))<=2.5
return {'sample':int(round(self.now*10)),'real_ms':30000+int(round(self.now*1000)),
'received_monotonic':self.now-1 if self.stale else self.now,
'origin':list(self.origin),'camera':list(self.camera),'direction':[math.sin(r),0.,math.cos(r)],
'body_forward':[.807436,0.,-.589955],'input':list(self.input),'forward':0.,'velocity':list(self.velocity),
'inventory':False,'phase':self.phase,'door_state':self.door,'door_control':0.,'geometry':copy.deepcopy(self.geometry),
'selected':chosen,'context_selected':chosen,'cached_performable':chosen,'progress_bound':chosen and self.bound,
'performing':self.performing,'pressed':self.pressed,'progress':self.progress,'duration':4.}
def sleep(self,seconds):
end=self.now+seconds
while self.now < end-1e-8:
self.now+=min(.005,end-self.now)
while self.pending and self.pending[0][0]<=self.now:self.angle+=self.pending.pop(0)[1]
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('focus lost')
self.sleep(self.guard_cost)
def idle(self):return None
def move(self,dx,dy,guard,*,deadline):
if dy or not 1<=abs(dx)<=128:raise AssertionError('Only bounded horizontal relative moves permitted')
guard.check();guard.check()
if self.now>=deadline:raise policy.Refused('adapter deadline')
self.motions.append((dx,dy,self.now,deadline))
if not self.ignore:self.pending.append((self.now+self.delay,dx*self.gain*(-1 if self.reverse else 1)))
def observe(self):
if self.cancel:raise policy.Refused('native camera identity changed')
batch=[s for s in self.history if s['sample']>self.last_read] or [self.snapshot]
self.last_read=batch[-1]['sample'];return copy.deepcopy(batch)
def run(self):
return policy.perform_alignment(self,self.idle,self,self.observe,self.geometry,self.calibration,self.accepted,self.report,
clock=self.clock,sleep=self.sleep,notify=self.notifications.append)
class AlignmentTests(unittest.TestCase):
def test_actual_pose_bearing_aligns_without_movement_or_interaction(self):
scene=Scene();result=scene.run()
self.assertTrue(result['completed']);self.assertLessEqual(len(scene.motions),6)
self.assertGreater(len(scene.motions),0);self.assertLess(scene.now,39)
self.assertEqual(scene.origin,[6092.42,211.297,7155.81])
a,b=result['selection_samples'];samples={s['sample']:s for s in result['samples']}
self.assertEqual(b,a+1);self.assertGreaterEqual(samples[b]['real_ms']-samples[a]['real_ms'],100)
self.assertTrue(all(samples[i]['progress_bound'] for i in (a,b)))
self.assertTrue(all(m[1]==0 for m in scene.motions))
def test_delayed_turn_requires_real_rotation_before_settling(self):
scene=Scene();scene.delay=.55
result=scene.run();self.assertTrue(result['completed'])
self.assertTrue(all(m['response_sample']>m['after_sample'] for m in result['motions']))
def test_aligned_missing_selection_refuses_without_movement_fallback(self):
scene=Scene();scene.angle=168.433;scene.select=False
scene.snapshot=scene.make();scene.history=[copy.deepcopy(scene.snapshot)]
with self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[]);self.assertLessEqual(scene.now,39.2)
self.assertFalse(scene.report['alignment_proof']['completed'])
def test_unbound_progress_is_not_zero_progress_proof(self):
scene=Scene();scene.bound=False
with self.assertRaises(policy.Refused):scene.run()
self.assertFalse(scene.report['alignment_proof']['completed'])
def test_original_observer_deadline_wins_and_is_not_renewed(self):
scene=Scene();scene.calibration['observer_deadline_monotonic']=31.2
with self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[])
self.assertLessEqual(scene.report['alignment_proof']['deadline_monotonic'],31.2)
scene=Scene();scene.calibration['alignment_deadline_monotonic']=31.2
with self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[])
def test_expired_original_native_samples_refuse_even_with_future_host_deadline(self):
scene=Scene();scene.snapshot['real_ms']=75000;scene.history=[copy.deepcopy(scene.snapshot)]
with self.assertRaisesRegex(policy.Refused,'lifetime'):scene.run()
self.assertEqual(scene.motions,[])
def test_prior_approach_and_same_run_are_required(self):
for fault in ('verified','sample','nonce'):
scene=Scene()
if fault=='verified':scene.accepted['verified']=False
elif fault=='sample':scene.accepted['last_sample']=309
else:scene.calibration['nonce']='other'
with self.subTest(fault=fault),self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[])
def test_movement_and_action_in_intermediate_batch_abort(self):
for field,value in (('origin',[6092.6,211.297,7155.81]),('pressed',1.),('progress',.1),('performing',True),('input',[0,0,1])):
scene=Scene();scene.sleep(.3)
scene.history[1][field]=value
with self.subTest(field=field),self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[])
def test_overshoot_reverse_and_missing_rotation_refuse(self):
for fault in ('overshoot','reverse','ignore'):
scene=Scene()
if fault=='overshoot':scene.gain=.5
elif fault=='reverse':scene.reverse=True
else:scene.ignore=True
with self.subTest(fault=fault),self.assertRaises(policy.Refused):scene.run()
self.assertEqual(len(scene.motions),1)
def test_stale_camera_or_focus_loss_never_authorizes_input(self):
for fault in ('stale','cancel','focus'):
scene=Scene()
if fault=='focus':scene.focus=False
else:setattr(scene,fault,True);scene.sleep(.1)
with self.subTest(fault=fault),self.assertRaises(policy.Refused):scene.run()
self.assertEqual(scene.motions,[])
def test_measured_slow_guards_and_delayed_response_fit_without_renewal(self):
scene=Scene();scene.guard_cost=.18;scene.delay=.55
result=scene.run()
self.assertTrue(result['completed']);self.assertLessEqual(scene.now-31,8)
self.assertEqual(result['deadline_monotonic'],39)
self.assertEqual(len(scene.motions),5)
def test_diagonal_body_drift_over_five_centimetres_refuses(self):
scene=Scene();scene.sleep(.1)
scene.history[-1]['origin'][0]+=.04;scene.history[-1]['origin'][1]+=.04
with self.assertRaisesRegex(policy.Refused,'body moved'):scene.run()
self.assertEqual(scene.motions,[])
def test_transient_yaw_overshoot_in_intermediate_batch_refuses(self):
scene=Scene()
def move(dx,dy,guard,*,deadline):
guard.check();scene.angle+=20;scene.sleep(.2);scene.angle-=10
scene.motions.append((dx,dy,scene.now,deadline))
scene.move=move
with self.assertRaisesRegex(policy.Refused,'turn bound'):scene.run()
self.assertEqual(len(scene.motions),1)
def test_selected_but_wrong_bearing_is_not_success(self):
scene=Scene();scene.ignore=True
original=scene.observe
def falsely_selected():
batch=original()
for s in batch:
s['selected']=s['context_selected']=s['cached_performable']=s['progress_bound']=True
return batch
scene.observe=falsely_selected
with self.assertRaises(policy.Refused):scene.run()
self.assertFalse(scene.report['alignment_proof']['completed'])
def test_retry_same_report_refuses(self):
scene=Scene();scene.report['alignment_proof']={}
with self.assertRaisesRegex(policy.Refused,'already exists'):scene.run()
self.assertEqual(scene.motions,[])
if __name__=='__main__':unittest.main()
+59
View File
@@ -134,4 +134,63 @@ class ApproachRunner(unittest.TestCase):
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
self.assertFalse(proof['verified']);self.assertEqual(result,'input-exercise-incomplete')
class AlignmentRunner(unittest.TestCase):
def test_alignment_requires_explicit_approach_and_separate_hashes(self):
with self.assertRaises(ValueError):r.validate_input_exercise(SimpleNamespace(exercise_prison_alignment=True))
valid=dict(exercise_prison_alignment=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)
r.validate_input_exercise(SimpleNamespace(**valid))
with tempfile.TemporaryDirectory() as folder:
root=Path(folder);(root/'tools').mkdir();profile=root/'profile';profile.mkdir()
names=('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')
for name in names:(root/'tools'/name).write_text('# fake')
native=['gamescope','--backend','headless','--','/proton','run','/game','-errOrdinaryInputObserve',NONCE,'-errOrdinaryLookObserve',NONCE,'-errOrdinaryGateObserve',NONCE]
wrapped,display=r.display_probe_command(native,root,Path('/compat'),profile,Path('/game'),NONCE,':0')
args=(wrapped,root,Path('/compat'),profile,Path('/game'),NONCE,':0',display)
command,context=r.input_exercise_command(*args,True,True,True)
self.assertIn('--enable-private-alignment-proof',command);self.assertTrue(context['alignment_requested'])
self.assertEqual(set(context['alignment_module_sha256']),set(names[-2:]))
for look,approach,alignment in ((True,False,True),(True,True,1)):
with self.assertRaises(ValueError):r.input_exercise_command(*args,look,approach,alignment)
def test_final_alignment_is_independent_and_preserves_failures(self):
for verified,prior in ((True,'game-started'),(False,'game-started'),(True,'cleanup-failed')):
with tempfile.TemporaryDirectory() as folder:
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
context.update(look_requested=True,approach_requested=True,alignment_requested=True,approach_module_sha256=r.approach_module_hashes(r.ROOT),alignment_module_sha256=r.alignment_module_hashes(r.ROOT))
report.update({k:context[k] for k in ('look_requested','approach_requested','alignment_requested','approach_module_sha256','alignment_module_sha256')})
report.update(look_proof={'completed':True},alignment_proof={'completed':True})
report['events']=report['events'][:2];report['observer']['events']=['READY'];report['approach_proof']={'events':copy.deepcopy(report['events'])}
(session/'console.log').write_text('native');Path(context['report']).write_text(json.dumps(report))
approach=ModuleType('prison_approach_evidence');approach.validate_approach_evidence=lambda *a:{'verified':True}
alignment=ModuleType('prison_alignment_evidence');calls=[]
def validate(*args):calls.append(args);return {'verified':verified}
alignment.validate_alignment_evidence=validate
with patch.dict(sys.modules,{'prison_approach_evidence':approach,'prison_alignment_evidence':alignment}):
result,proof=r.input_exercise_result(prior,context,session,observation,{'verified':True})
self.assertEqual(calls,[('native',NONCE,report['look_proof'],report['approach_proof'],report['alignment_proof'])])
self.assertEqual(proof['verified'],verified)
self.assertEqual(result,'input-exercise-incomplete' if not verified and prior=='game-started' else prior)
def test_unrequested_alignment_and_changed_hashes_refuse(self):
for payload in ({'alignment_requested':True},{'alignment_requested':1},{'alignment_proof':{}}):
with tempfile.TemporaryDirectory() as folder:
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder));report.update(payload)
Path(context['report']).write_text(json.dumps(report))
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
self.assertFalse(proof['verified'])
def test_alignment_hash_mismatch_and_current_module_mutation_refuse(self):
for changed_current in (False,True):
with tempfile.TemporaryDirectory() as folder:
session,context,report,observation=fixtures.ExerciseTests.make_proof(self,Path(folder))
context.update(look_requested=True,approach_requested=True,alignment_requested=True,alignment_module_sha256=r.alignment_module_hashes(r.ROOT))
report.update(alignment_requested=True,alignment_module_sha256=dict(context['alignment_module_sha256']))
altered=dict(context['alignment_module_sha256']);altered['prison_alignment_proof.py']='0'*64
if not changed_current:report['alignment_module_sha256']=altered
Path(context['report']).write_text(json.dumps(report))
current=altered if changed_current else context['alignment_module_sha256']
with patch.object(r,'alignment_module_hashes',return_value=current):
result,proof=r.input_exercise_result('game-started',context,session,observation,{'verified':True})
self.assertFalse(proof['verified']);self.assertIn('unchanged module sources',proof['error'])
if __name__=='__main__':unittest.main()