| 1 | #!/usr/bin/env python3 |
| 2 | """Black-box local ownership contract; only its unique fixture process/files change.""" |
| 3 | import json, os, pathlib, signal, subprocess, sys, tempfile, time, urllib.request, urllib.error, uuid |
| 4 | binary = pathlib.Path(sys.argv[1]).resolve() |
| 5 | root = pathlib.Path(tempfile.mkdtemp(prefix='codewhale-shared-contract-')) |
| 6 | env = dict(os.environ, CODEWHALE_PET_HOME=str(root), CODEWHALE_PET_PORT='0') |
| 7 | log = (root/'owner.log').open('ab'); child = None; checks=[] |
| 8 | def start(): |
| 9 | global child |
| 10 | child = subprocess.Popen([str(binary),'pet','serve'],env=env,stdout=log,stderr=log) |
| 11 | deadline=time.monotonic()+15 |
| 12 | while time.monotonic()<deadline: |
| 13 | try: |
| 14 | d=json.loads((root/'connection.json').read_text()) |
| 15 | value=request(d,'/v1/frame') |
| 16 | return d,value |
| 17 | except (OSError,ValueError): |
| 18 | if child.poll() is not None: raise RuntimeError((root/'owner.log').read_text()) |
| 19 | time.sleep(.05) |
| 20 | raise RuntimeError('Owner did not become ready') |
| 21 | def request(d,path,body=None,headers=None): |
| 22 | h={'Authorization':'Bearer '+d['token'],'Content-Type':'application/json'};h.update(headers or {}) |
| 23 | r=urllib.request.Request(f"http://127.0.0.1:{d['port']}{path}",data=None if body is None else json.dumps(body).encode(),headers=h) |
| 24 | with urllib.request.urlopen(r,timeout=3) as response: return json.load(response) |
| 25 | def frame_when(d,predicate): |
| 26 | deadline=time.monotonic()+5 |
| 27 | while time.monotonic()<deadline: |
| 28 | frame=request(d,'/v1/frame') |
| 29 | if predicate(frame): return frame |
| 30 | time.sleep(.03) |
| 31 | raise AssertionError('Accepted state did not reach the frame projection') |
| 32 | def reject(d,path,body=None,headers=None): |
| 33 | try: request(d,path,body,headers) |
| 34 | except urllib.error.HTTPError as e: |
| 35 | assert e.code in (401,409,422),e.code |
| 36 | return |
| 37 | raise AssertionError('Invalid request was accepted') |
| 38 | def passed(name): checks.append(name);print('PASS '+name,flush=True) |
| 39 | try: |
| 40 | d,a=start(); assert len(a['points'])==980 |
| 41 | b=request(d,'/v1/frame?tick='+str(a['tick'])) |
| 42 | assert (a['identity'],a['epoch'],a['tick'],a['digest'])==(b['identity'],b['epoch'],b['tick'],b['digest']) |
| 43 | passed('two attachments match identity, epoch, tick and digest at one retained frame') |
| 44 | competing=subprocess.run([str(binary),'pet','serve'],env=env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=15) |
| 45 | assert competing.returncode!=0 and b'Another pet owner' in competing.stderr |
| 46 | passed('competing owner cannot open the same world') |
| 47 | reject(d,'/v1/frame',headers={'Authorization':'Bearer invalid'}) |
| 48 | reject(d,'/v1/frame',headers={'Origin':'https://example.invalid'}) |
| 49 | passed('loopback bearer and browser origin checks reject foreign access') |
| 50 | client=str(uuid.uuid4()); action={'identity':d['identity'],'client':client,'seq':1,'source_revision':a['sourceRevision'],'action':{'kind':'interact','food':True,'x':.2,'y':-.15}} |
| 51 | receipt=request(d,'/v1/action',action); repeated=request(d,'/v1/action',action) |
| 52 | assert repeated['duplicate'] and repeated['cursor']==receipt['cursor'] |
| 53 | reject(d,'/v1/action',dict(action,seq=3));reject(d,'/v1/action',dict(action,action=dict(action['action'],food=False))) |
| 54 | passed('interaction receipt is durable and idempotent; changed duplicates and gaps rejected') |
| 55 | select=dict(action,seq=2,action={'kind':'select','source':'contract-source'}) |
| 56 | request(d,'/v1/action',select);a=frame_when(d,lambda f:f['source']=='contract-source') |
| 57 | producer={'identity':d['identity'],'epoch':a['epoch'],'client':str(uuid.uuid4()),'source':a['source'],'source_revision':a['sourceRevision'],'seq':0,'waiting':False,'events':[]} |
| 58 | request(d,'/v1/producer',producer) |
| 59 | packet=dict(producer,seq=1,events=[{'event':'thinking_started','index':1}]);r=request(d,'/v1/producer',packet) |
| 60 | assert request(d,'/v1/producer',packet)['duplicate'] |
| 61 | reject(d,'/v1/producer',dict(producer,seq=3));request(d,'/v1/producer',producer) |
| 62 | reject(d,'/v1/producer',dict(producer,seq=1,events=[{'event':'thinking_started','index':2},{'event':'response_delta','index':2,'content':'PRIVATE'}])) |
| 63 | request(d,'/v1/producer',dict(producer,seq=1,events=[])) |
| 64 | time.sleep(.1);assert request(d,'/v1/frame')['producerConnected'] |
| 65 | passed('producer duplicates, gaps and atomic metadata rejection preserve a usable owner') |
| 66 | request(d,'/v1/action',dict(select,seq=3,source_revision=a['sourceRevision'],action={'kind':'select','source':'other-source'})) |
| 67 | reject(d,'/v1/producer',dict(producer,seq=2));reject(d,'/v1/action',dict(action,seq=4)) |
| 68 | a=frame_when(d,lambda f:f['source']=='other-source');time.sleep(.4);b=request(d,'/v1/frame') |
| 69 | assert b['tick']>a['tick'] and b['identity']==a['identity'] and not b['producerConnected'] |
| 70 | passed('source changes reject old producers and actions; clock continues without a view') |
| 71 | audio_a=str(uuid.uuid4());audio_b=str(uuid.uuid4()) |
| 72 | assert request(d,'/v1/audio',{'client':audio_a,'enabled':True})['granted'] |
| 73 | assert not request(d,'/v1/audio',{'client':audio_b,'enabled':True})['granted'] |
| 74 | request(d,'/v1/audio',{'client':audio_a,'enabled':False}) |
| 75 | passed('only one view can lease the companion audio device') |
| 76 | before_style=request(d,'/v1/export') |
| 77 | appearance={'background':[238,239,235],'backgroundTop':[255,255,250],'particle':[32,79,83],'eventColors':False,'brightness':1.4,'dotScale':1.2,'glow':.25,'environment':False} |
| 78 | style_action={'identity':d['identity'],'client':str(uuid.uuid4()),'seq':1,'source_revision':b['sourceRevision'],'action':{'kind':'appearance','appearance':appearance}} |
| 79 | reject(d,'/v1/action',dict(style_action,action={'kind':'appearance','appearance':dict(appearance,brightness=999)})) |
| 80 | request(d,'/v1/action',style_action) |
| 81 | styled=frame_when(d,lambda f:f['appearance']==appearance) |
| 82 | assert [styled['style'][k] for k in ['r','g','b']]==appearance['particle'] |
| 83 | assert request(d,'/v1/export')['interactions']==before_style['interactions'] |
| 84 | passed('appearance is validated and durable without adding simulation interactions') |
| 85 | recording=request(d,'/v1/export');assert 'checkpoint' in recording and 'token' not in recording |
| 86 | checkpoint=json.loads((root/'habitat.json').read_text());epoch=b['epoch'] |
| 87 | child.kill();child.wait(5);d2,a=start() |
| 88 | assert d2==d and a['epoch']!=epoch and a['identity']==d['identity'] |
| 89 | assert a['cursor']>=checkpoint['cursor'] and a['tick']>=checkpoint['recording']['checkpoint']['tick'] |
| 90 | assert not a['producerConnected'] and not a['audioOwner'] |
| 91 | assert a['appearance']==appearance |
| 92 | assert request(d,'/v1/action',dict(select,seq=3,source_revision=b['sourceRevision']-1,action={'kind':'select','source':'other-source'}))['duplicate'] |
| 93 | passed('crash restart restores identity, checkpoint and durable action receipt with fresh unobserved leases') |
| 94 | habitat=root/'habitat.json';original=habitat.read_bytes();foreign=b'{"external":"fixture writer"}' |
| 95 | habitat.write_bytes(foreign);time.sleep(1.2);a=request(d,'/v1/frame');assert not a['storageAvailable'] |
| 96 | reject(d,'/v1/action',{'identity':d['identity'],'client':str(uuid.uuid4()),'seq':1,'source_revision':a['sourceRevision'],'action':action['action']}) |
| 97 | assert habitat.read_bytes()==foreign |
| 98 | habitat.write_bytes(original) |
| 99 | passed('storage conflict refuses interaction and preserves the external writer') |
| 100 | print(json.dumps({'checks':len(checks),'passed':checks,'fixture':str(root),'binary':str(binary)},indent=2)) |
| 101 | finally: |
| 102 | if child and child.poll() is None: |
| 103 | child.send_signal(signal.SIGINT) |
| 104 | try: child.wait(8) |
| 105 | except subprocess.TimeoutExpired: child.kill();child.wait() |
| 106 | log.close() |
| 107 |