返回 CodeWhale
check-shared.py
根目录 / pet / scripts / check-shared.py
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 # The macOS/Unix owner may miss a 250 ms clock deadline under CI load.
60 # Coverage becomes unknown, but a delay shorter than the 2 s producer
61 # lease must not discard its sequence and reject the next valid packet.
62 child.send_signal(signal.SIGSTOP)
63 try: time.sleep(.4)
64 finally: child.send_signal(signal.SIGCONT)
65 packet=dict(producer,seq=1,events=[{'event':'thinking_started','index':1}]);r=request(d,'/v1/producer',packet)
66 assert not r.get('duplicate',False) and r['seq']==1
67 passed('a scheduling pause inside the lease preserves the producer sequence')
68 assert request(d,'/v1/producer',packet)['duplicate']
69 reject(d,'/v1/producer',dict(producer,seq=3));request(d,'/v1/producer',producer)
70 reject(d,'/v1/producer',dict(producer,seq=1,events=[{'event':'thinking_started','index':2},{'event':'response_delta','index':2,'content':'PRIVATE'}]))
71 request(d,'/v1/producer',dict(producer,seq=1,events=[]))
72 time.sleep(.1);assert request(d,'/v1/frame')['producerConnected']
73 passed('producer duplicates, gaps and atomic metadata rejection preserve a usable owner')
74 request(d,'/v1/action',dict(select,seq=3,source_revision=a['sourceRevision'],action={'kind':'select','source':'other-source'}))
75 reject(d,'/v1/producer',dict(producer,seq=2));reject(d,'/v1/action',dict(action,seq=4))
76 a=frame_when(d,lambda f:f['source']=='other-source');time.sleep(.4);b=request(d,'/v1/frame')
77 assert b['tick']>a['tick'] and b['identity']==a['identity'] and not b['producerConnected']
78 passed('source changes reject old producers and actions; clock continues without a view')
79 audio_a=str(uuid.uuid4());audio_b=str(uuid.uuid4())
80 assert request(d,'/v1/audio',{'client':audio_a,'enabled':True})['granted']
81 competitor=request(d,'/v1/audio',{'client':audio_b,'enabled':True})
82 if competitor['granted']:
83 # Headless hosts can lose the sink and release the first lease before
84 # this request. A second grant is valid only if the owner reports the
85 # failed device and also releases that lease; it is no audio proof.
86 frame_when(d,lambda f:f['audioUnavailable'] and not f['audioOwner'])
87 passed('unavailable audio releases its lease and reports device failure (no live audio proof)')
88 else:
89 passed('only one view can lease the companion audio device')
90 request(d,'/v1/audio',{'client':audio_a,'enabled':False})
91 request(d,'/v1/audio',{'client':audio_b,'enabled':False})
92 before_style=request(d,'/v1/export')
93 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}
94 style_action={'identity':d['identity'],'client':str(uuid.uuid4()),'seq':1,'source_revision':b['sourceRevision'],'action':{'kind':'appearance','appearance':appearance}}
95 reject(d,'/v1/action',dict(style_action,action={'kind':'appearance','appearance':dict(appearance,brightness=999)}))
96 request(d,'/v1/action',style_action)
97 styled=frame_when(d,lambda f:f['appearance']==appearance)
98 assert [styled['style'][k] for k in ['r','g','b']]==appearance['particle']
99 assert request(d,'/v1/export')['interactions']==before_style['interactions']
100 passed('appearance is validated and durable without adding simulation interactions')
101 recording=request(d,'/v1/export');assert 'checkpoint' in recording and 'token' not in recording
102 checkpoint=json.loads((root/'habitat.json').read_text());epoch=b['epoch']
103 child.kill();child.wait(5);d2,a=start()
104 assert d2==d and a['epoch']!=epoch and a['identity']==d['identity']
105 assert a['cursor']>=checkpoint['cursor'] and a['tick']>=checkpoint['recording']['checkpoint']['tick']
106 assert not a['producerConnected'] and not a['audioOwner']
107 assert a['appearance']==appearance
108 assert request(d,'/v1/action',dict(select,seq=3,source_revision=b['sourceRevision']-1,action={'kind':'select','source':'other-source'}))['duplicate']
109 passed('crash restart restores identity, checkpoint and durable action receipt with fresh unobserved leases')
110 habitat=root/'habitat.json';original=habitat.read_bytes();foreign=b'{"external":"fixture writer"}'
111 habitat.write_bytes(foreign);time.sleep(1.2);a=request(d,'/v1/frame');assert not a['storageAvailable']
112 reject(d,'/v1/action',{'identity':d['identity'],'client':str(uuid.uuid4()),'seq':1,'source_revision':a['sourceRevision'],'action':action['action']})
113 assert habitat.read_bytes()==foreign
114 habitat.write_bytes(original)
115 passed('storage conflict refuses interaction and preserves the external writer')
116 print(json.dumps({'checks':len(checks),'passed':checks,'fixture':str(root),'binary':str(binary)},indent=2))
117 finally:
118 if child and child.poll() is None:
119 child.send_signal(signal.SIGINT)
120 try: child.wait(8)
121 except subprocess.TimeoutExpired: child.kill();child.wait()
122 log.close()
123
123 lines PYTHON