Buckets:
| """Stage original bucket assets, repair observed dependencies, then test a network-blocked local mirror. | |
| HF_TOKEN is read from the environment only. Game assets live under /tmp and are deleted after each game. | |
| """ | |
| import os,json,hashlib,shutil,time,threading,mimetypes,re,sys,traceback | |
| from pathlib import Path | |
| from urllib.parse import urlparse,unquote,quote | |
| from http.server import ThreadingHTTPServer,BaseHTTPRequestHandler | |
| import requests | |
| from huggingface_hub import HfApi | |
| from playwright.sync_api import sync_playwright | |
| HOME=Path('/home/user/game-archive');REPORT=HOME/'reports';EVIDENCE=REPORT/'evidence';EVIDENCE.mkdir(exist_ok=True) | |
| BUCKET='smodusermc/offline-games';BASE='https://garbsoftball.com';api=HfApi(token=os.environ['HF_TOKEN']) | |
| SITE_HOST='garbsoftball.com';VENDORS=('unpkg.com','cdn.jsdelivr.net','ajax.googleapis.com','maxcdn.bootstrapcdn.com','cdn-factory.marketjs.com','cdn.fbrq.io') | |
| IGNORE=('googletagmanager.com','google-analytics.com','cloudflareinsights.com','fonts.googleapis.com','fonts.gstatic.com') | |
| STAGE=Path(os.environ.get('GAME_VERIFY_STAGE','/tmp/game-verify-stage')) | |
| remote={f.path:f for f in api.list_bucket_tree(BUCKET,recursive=True) if hasattr(f,'size')} | |
| # Inventory check for every existing manifest; file size presence is NOT a runtime test. | |
| audit=[] | |
| for mp in sorted(REPORT.glob('game-*.json')): | |
| m=json.loads(mp.read_text());missing=[];mismatch=[] | |
| for f in m['files']: | |
| rf=remote.get(f['path']) | |
| if rf is None:missing.append(f['path']) | |
| elif rf.size!=f['bytes']:mismatch.append({'path':f['path'],'expected':f['bytes'],'stored':rf.size}) | |
| audit.append({'id':int(mp.stem.split('-')[1]),'name':m['name'],'recorded_files':len(m['files']),'missing_objects':missing,'size_mismatches':mismatch,'check':'remote metadata presence/size only, not SHA256 or gameplay'}) | |
| (REPORT/'storage-audit.json').write_text(json.dumps(audit,indent=2)) | |
| class State: | |
| def __init__(self,m): | |
| self.manifest=m;self.files={f['path']:f for f in m['files']};self.added={};self.redirects={};self.hits=[];self.failed={};self.repair=True;self.fetch_bytes=0;self.lock=threading.RLock();self.busy={};self.fetched=set() | |
| def path_for_url(self,url): | |
| u=urlparse(url);path=unquote(u.path) | |
| if path.endswith('/'):path+='index.html' | |
| if '..' in Path(path).parts:raise ValueError('unsafe path') | |
| return ('site' if u.hostname==SITE_HOST else '_vendor/'+u.netloc)+path | |
| def localurl(self,u): | |
| p=urlparse(u) | |
| return ('' if p.hostname==SITE_HOST else '/__vendor/'+p.netloc)+p.path+('?' + p.query if p.query else '') | |
| def fetch(self,source): | |
| key=self.path_for_url(source);dest=STAGE/key | |
| with self.lock: | |
| if dest.exists():return key | |
| if key in self.failed:return None | |
| if not self.repair:self.failed[key]='not staged; network disabled';return None | |
| lock=self.busy.setdefault(key,threading.Lock()) | |
| with lock: | |
| if dest.exists():return key | |
| try: | |
| if self.fetch_bytes>700*1024**2:raise ValueError('per-game repair size limit reached') | |
| if key in remote: | |
| api.download_bucket_files(BUCKET,[(key,dest)],raise_on_missing_files=True) | |
| info={'url':source,'path':key,'bytes':dest.stat().st_size,'sha256':hashlib.file_digest(dest.open('rb'),'sha256').hexdigest(),'content_type':mimetypes.guess_type(source)[0] or 'application/octet-stream'} | |
| else: | |
| with requests.get(source,stream=True,timeout=(8,25)) as r: | |
| r.raise_for_status() | |
| if urlparse(r.url).hostname not in (SITE_HOST,*VENDORS):raise ValueError('external redirect not permitted') | |
| if int(r.headers.get('Content-Length',0))>350*1024**2:raise ValueError('file exceeds 350 MiB repair limit') | |
| # Redirects matter for ES modules and dynamic Ruffle chunks. | |
| final=r.url | |
| finalkey=self.path_for_url(final) | |
| if unquote(final.split('?')[0])!=unquote(source.split('?')[0]):self.redirects[self.localurl(source)]=self.localurl(final) | |
| dest=STAGE/finalkey;dest.parent.mkdir(parents=True,exist_ok=True);size=0;h=hashlib.sha256();head=b'' | |
| with dest.open('wb') as f: | |
| for chunk in r.iter_content(1024*1024): | |
| size+=len(chunk) | |
| if size>350*1024**2:raise ValueError('file exceeds 350 MiB repair limit') | |
| if len(head)<256:head+=chunk[:256-len(head)] | |
| f.write(chunk);h.update(chunk) | |
| if head.strip().startswith(b'PS 404 Not Found!'):raise ValueError('source returned plain-text 404 body with HTTP 200') | |
| if re.match(br'\s*(?:<!doctype html|<html)',head,re.I) and not urlparse(final).path.endswith(('.html','/','.htm')):raise ValueError('server returned HTML in place of an asset') | |
| key=finalkey;self.fetch_bytes+=size | |
| info={'url':final,'path':key,'bytes':size,'sha256':h.hexdigest(),'content_type':r.headers.get('Content-Type','application/octet-stream')} | |
| with self.lock: | |
| self.files[key]=info;self.added[key]=info;self.fetched.add(source) | |
| return key | |
| except Exception as e: | |
| dest.unlink(missing_ok=True) | |
| with self.lock:self.failed[key]=str(e)[:180] | |
| return None | |
| class Handler(BaseHTTPRequestHandler): | |
| def log_message(self,*a):pass | |
| def do_HEAD(self): | |
| self.do_GET() | |
| def do_GET(self): | |
| S=self.server.state;p=urlparse(self.path);req=self.path | |
| if p.path.startswith('/__vendor/'): | |
| rest=p.path[len('/__vendor/'):];host,_,path=rest.partition('/') | |
| if host not in VENDORS:self.send_error(403);return | |
| # Only runtime-library dependencies are admitted, not external full game embeds. | |
| if not any(term in path.lower() for term in ('ruffle','peerjs','jquery','font-awesome','generic.png')):self.send_error(403);return | |
| source='https://'+host+'/'+path+('?' + p.query if p.query else '') | |
| else:source=BASE+req | |
| if req in S.redirects: | |
| self.send_response(302);self.send_header('Location',S.redirects[req]);self.end_headers();return | |
| try:key=S.path_for_url(source) | |
| except:self.send_error(400);return | |
| S.hits.append({'path':key,'repair_phase':S.repair}) | |
| file=STAGE/key | |
| if not file.exists(): | |
| found=S.fetch(source) | |
| if req in S.redirects: | |
| self.send_response(302);self.send_header('Location',S.redirects[req]);self.end_headers();return | |
| if found:key=found;file=STAGE/key | |
| if not file.exists():self.send_error(404);return | |
| info=S.files.get(key,{}) | |
| ctype=info.get('content_type') or mimetypes.guess_type(str(file))[0] or 'application/octet-stream' | |
| if file.suffix in ('.js','.mjs'):ctype='application/javascript; charset=utf-8' | |
| if file.suffix=='.wasm':ctype='application/wasm' | |
| data=None | |
| if file.stat().st_size<16*1024**2 and ('text/' in ctype or 'javascript' in ctype or file.suffix in ('.html','.js','.mjs','.css')): | |
| data=file.read_bytes() | |
| # Original files remain unchanged; local-mirror routing is applied only in the test HTTP response. | |
| data=data.replace(b'https://garbsoftball.com',b'').replace(b'http://garbsoftball.com',b'') | |
| for host in VENDORS:data=data.replace(('https://'+host).encode(),('/__vendor/'+host).encode()) | |
| self.send_response(200);self.send_header('Content-Type',ctype) | |
| self.send_header('Cross-Origin-Opener-Policy','same-origin');self.send_header('Cross-Origin-Embedder-Policy','credentialless');self.send_header('Cross-Origin-Resource-Policy','cross-origin');self.send_header('Access-Control-Allow-Origin','*');self.send_header('Cache-Control','no-store');self.send_header('Content-Length',str(len(data) if data is not None else file.stat().st_size));self.end_headers() | |
| if self.command=='HEAD':return | |
| try: | |
| if data is not None:self.wfile.write(data) | |
| else: | |
| with file.open('rb') as f:shutil.copyfileobj(f,self.wfile,1024*1024) | |
| except (BrokenPipeError,ConnectionResetError):pass | |
| def browser_pass(p,server,ident,phase): | |
| b=p.chromium.launch(headless=True,args=['--no-sandbox','--enable-unsafe-swiftshader','--disable-dev-shm-usage']) | |
| ctx=b.new_context(viewport={'width':1024,'height':720},service_workers='block') | |
| blocked=[];errors=[];logs=[];crashed=[] | |
| def gate(route): | |
| u=urlparse(route.request.url) | |
| if u.hostname in ('127.0.0.1','localhost'):route.continue_() | |
| elif u.scheme in ('data','blob'):route.continue_() | |
| else: | |
| if not any(h in (u.hostname or '') for h in IGNORE):blocked.append(route.request.url) | |
| route.abort() | |
| ctx.route('**/*',gate);page=ctx.new_page();page.set_default_timeout(5000);page.on('pageerror',lambda e:errors.append(str(e)[:500]));page.on('crash',lambda _:crashed.append(True));page.on('console',lambda m:logs.append(m.text[:350]) if m.type in ('error','warning') else None) | |
| result={'phase':phase,'external_network':'blocked in browser','runtime_test':'not_yet_classified','input_assertions':[]} | |
| try: | |
| u='http://127.0.0.1:'+str(server.server_port)+urlparse(server.state.manifest['entry']).path | |
| page.goto(u,wait_until='domcontentloaded',timeout=45000) | |
| page.wait_for_timeout(3500) | |
| if ident==143: | |
| result['tutorial_steps_clicked']=[] | |
| for step in range(8): | |
| try: | |
| control=page.get_by_text(re.compile(r'^\s*(Next|Done|Finish)\s*$',re.I)).first | |
| if not control.is_visible(timeout=500):break | |
| label=control.inner_text();control.click(timeout=1500);result['tutorial_steps_clicked'].append(label);page.wait_for_timeout(500) | |
| except:break | |
| page.keyboard.press('Enter');page.wait_for_timeout(1200) | |
| if ident==182: | |
| page.goto(u.rsplit('/',1)[0]+'/play/index.html',wait_until='domcontentloaded');result['declared_play_link_followed']=True;page.wait_for_timeout(6000) | |
| if ident==152: | |
| try: | |
| page.get_by_text('Flash',exact=True).click(timeout=1500);page.wait_for_timeout(3500) | |
| except:pass | |
| for sel in ['#loadFly','#startButton','#start-button','#playButton','#playBtn','button:has-text("Play")','button:has-text("Start")']: | |
| try: | |
| loc=page.locator(sel).first | |
| if loc.is_visible(timeout=300):loc.click(timeout=1000);result['auto_clicked']=sel;break | |
| except:pass | |
| try: | |
| flash=page.locator('ruffle-player,ruffle-object,ruffle-embed').first | |
| if flash.count() and flash.is_visible(): | |
| box=flash.bounding_box() | |
| if box:page.mouse.click(box['x']+box['width']/2,box['y']+box['height']/2,delay=150);result['flash_play_clicked']=True | |
| for selector in ['#play-button','.play-button','button[title="Play"]']: | |
| try: | |
| button=page.locator(selector).first | |
| if button.is_visible(timeout=200):button.click(timeout=1000) | |
| except:pass | |
| result['flash_api']=page.evaluate('''() => {const p=document.querySelector('ruffle-player,ruffle-object,ruffle-embed'); if(!p)return null; const r=p.ruffle?.(); try {r?.play?.();p.play?.();}catch(e){} return {ready:p.readyState, isPlaying:p.isPlaying, shadow: p.shadowRoot?.innerHTML.slice(0,700)};}''') | |
| except:pass | |
| if ident in (0,4): | |
| before=page.locator('.tile-container').inner_html() | |
| for k in ['ArrowLeft','ArrowUp','ArrowRight','ArrowDown']*3:page.keyboard.press(k) | |
| page.wait_for_timeout(500);after=page.locator('.tile-container').inner_html() | |
| result['input_assertions'].append({'assertion':'tile board changes after arrow-key moves','passed':before!=after,'tiles':page.locator('.tile').count()}) | |
| if ident==101: | |
| try: | |
| english=page.get_by_text('English',exact=True).first | |
| if english.is_visible(timeout=300):english.click(timeout=1500);page.wait_for_timeout(2000) | |
| before=page.evaluate('Game.cookies');page.locator('#bigCookie').click(timeout=2000);after=page.evaluate('Game.cookies') | |
| result['input_assertions'].append({'assertion':'cookie count increases after cookie click','passed':after>before,'before':before,'after':after}) | |
| except Exception as e:result['input_assertions'].append({'assertion':'cookie click','passed':False,'error':str(e)[:150]}) | |
| if ident in (373,374,575,582):page.wait_for_timeout(6500) | |
| if ident in (182,183,359,373,374,376,575,582): | |
| result['batch_interactions']=[] | |
| if ident==359: | |
| page.locator('#splash').click();page.wait_for_timeout(700) | |
| before=page.evaluate('({cycle:simulation.cycle,x:m.pos.x,title:simulation.onTitlePage})') | |
| page.keyboard.down('d');page.wait_for_timeout(1000);page.keyboard.up('d') | |
| after=page.evaluate('({cycle:simulation.cycle,x:m.pos.x,title:simulation.onTitlePage})') | |
| result['input_assertions'].append({'assertion':'n-gon starts, simulates, and responds to D movement','passed':not after['title'] and after['cycle']>before['cycle'] and abs(after['x']-before['x'])>1,'before':before,'after':after}) | |
| elif ident==376: | |
| page.locator('a.start').click();page.wait_for_timeout(4000) | |
| before=page.locator('.p1-score span').inner_text() | |
| for key in ['ArrowLeft','ArrowDown','ArrowRight']: | |
| page.keyboard.down(key);page.wait_for_timeout(900);page.keyboard.up(key) | |
| after=page.locator('.p1-score span').inner_text() | |
| result['input_assertions'].append({'assertion':'Pacman score increases after starting and arrow-key movement','passed':int(after)>int(before),'before':before,'after':after}) | |
| elif ident==373: | |
| before=page.evaluate('window.cr_getC2Runtime?.()?.running_layout?.name') | |
| page.mouse.click(512,350,delay=180);page.wait_for_timeout(2000) | |
| page.keyboard.down('ArrowRight');page.wait_for_timeout(900);page.keyboard.press('ArrowUp');page.keyboard.up('ArrowRight') | |
| after=page.evaluate('window.cr_getC2Runtime?.()?.running_layout?.name') | |
| result['batch_interactions'].append({'action':'Play, right movement, jump','layout_before':before,'layout_after':after}) | |
| elif ident==374: | |
| for key in ['ArrowLeft','ArrowRight','Space']: | |
| page.keyboard.down(key);page.wait_for_timeout(700);page.keyboard.up(key) | |
| result['batch_interactions'].append('Exercised left/right/jump in the introductory scene') | |
| elif ident==582: | |
| page.mouse.click(512,360,delay=180);page.wait_for_timeout(4500) | |
| page.screenshot(path=str(REPORT/f'evidence/{ident:04d}-{phase}-mode-menu.jpg'),type='jpeg',quality=55) | |
| page.mouse.click(300,425,delay=180);page.wait_for_timeout(2000) | |
| for n in range(4):page.keyboard.press('ArrowUp');page.keyboard.press('w');page.wait_for_timeout(500) | |
| result['batch_interactions'].append('Play, clicked 1-player control, four Up/W presses') | |
| elif ident==575: | |
| page.mouse.click(512,600,delay=180);page.wait_for_timeout(2000) | |
| page.mouse.click(300,320,delay=180);page.wait_for_timeout(2200) | |
| for key in ['ArrowRight','ArrowUp']: | |
| page.keyboard.down(key);page.wait_for_timeout(500);page.keyboard.up(key) | |
| result['batch_interactions'].append('Clicked splash Play, menu Play Game, and exercised right/jump') | |
| else: | |
| page.mouse.click(512,600,delay=180);page.wait_for_timeout(2000) | |
| page.mouse.click(512,440,delay=180);page.wait_for_timeout(1800) | |
| result['batch_interactions'].append('Attempted splash Play and main Play') | |
| result['later_level_cases']=[] | |
| for fraction in [0.5,1.0]: | |
| case=page.evaluate("""(fraction)=>{const g=globalThis.Phaser?.GAMES?.find(x=>x);if(!g?.progress||g.state.current!=='level')return {attempted:false,reason:'Engine/level API not available',state:g?.state?.current};const temple=g.progress.get('temples')[0];const index=Math.round((temple.levels.length-1)*fraction);const meta=temple.levels[index];g.state.start('level',true,false,meta);return {attempted:true,index,filename:meta.filename,method:'Existing engine state API; not normal progression or a completed playthrough'};}""",fraction) | |
| if case.get('attempted'): | |
| page.wait_for_timeout(3500) | |
| case['observed']=page.evaluate("() => {const g=Phaser.GAMES.find(x=>x);const s=g.state.getCurrentState();return {state:g.state.current,levelPath:s.levelPath,width:s.mapData?.width,height:s.mapData?.height};}") | |
| case['map_loaded']=case['observed'].get('width',0)>0 and case['observed'].get('height',0)>0 | |
| result['later_level_cases'].append(case) | |
| if ident in (155,180,181,281): | |
| result['deeper_interaction_attempts']=[] | |
| if ident==155: | |
| page.mouse.click(860,650,delay=150);result['deeper_interaction_attempts'].append('Clicked the Duck Life 4 Play position') | |
| elif ident in (180,181): | |
| page.mouse.click(512,440,delay=150);result['deeper_interaction_attempts'].append('Clicked the Fireboy/Watergirl Play position') | |
| else: | |
| page.evaluate("() => {window.__clickAudioStarts=[];const native=AudioBufferSourceNode.prototype.start;AudioBufferSourceNode.prototype.start=function(...a){__clickAudioStarts.push({frames:this.buffer?.length||0});return native.apply(this,a);};}") | |
| for x,y in [(530,190),(555,190),(580,190),(605,190),(630,190),(655,190),(680,190),(705,190)]:page.mouse.click(x,y,delay=80);page.wait_for_timeout(100) | |
| result['deeper_interaction_attempts'].append('Clicked eight positions in the brick grid') | |
| result['audio_sources_during_clicks']=page.evaluate('__clickAudioStarts') | |
| page.wait_for_timeout(2500) | |
| if ident in (180,181): | |
| page.screenshot(path=str(REPORT/f'evidence/{ident:04d}-{phase}-before-movement.jpg'),type='jpeg',quality=55) | |
| for key in ['ArrowRight','d']: | |
| page.keyboard.down(key);page.wait_for_timeout(600);page.keyboard.up(key) | |
| result['deeper_interaction_attempts'].append('Held ArrowRight and D for 600ms each after Play') | |
| if ident==543: | |
| page.mouse.click(512,360,delay=150);page.wait_for_timeout(12000) | |
| result['construct_state']=page.evaluate('() => {const r=window.cr_getC2Runtime?.();return {layout:r?.running_layout?.name,isloading:r?.isloading,loadingprogress:r?.loadingprogress};}') | |
| result['activation_note']='Clicked canvas to allow audio and waited for opening sequence; not a playthrough.' | |
| if ident==591: | |
| try: | |
| for selector in ['game-icon[icon="close"]','#close','.close']: | |
| for el in page.locator(selector).all(): | |
| try: | |
| if el.is_visible(timeout=200):el.click(timeout=700) | |
| except:pass | |
| page.keyboard.press('Escape');page.keyboard.type('crane');page.keyboard.press('Enter');page.wait_for_timeout(2500) | |
| tiles=page.locator('game-tile[evaluation]').evaluate_all("els => els.map(e=>({letter:e.getAttribute('letter'),evaluation:e.getAttribute('evaluation')}))") | |
| good=[t for t in tiles if t['evaluation'] in ['correct','present','absent']] | |
| result['input_assertions'].append({'assertion':'Wordle accepts CRANE and evaluates five tiles','passed':len(good)>=5,'evaluated_tiles':good}) | |
| except Exception as e:result['wordle_input_error']=str(e)[:250] | |
| if ident==136: | |
| try: | |
| before=page.evaluate('Boolean(window.Runner?.instance_?.playing)');page.keyboard.press('Space');page.wait_for_timeout(400) | |
| after=page.evaluate('Boolean(window.Runner?.instance_?.playing)') | |
| result['input_assertions'].append({'assertion':'Dinosaur runner starts after Space','passed':not before and after,'before':before,'after':after}) | |
| except Exception as e:result['dinosaur_check_error']=str(e)[:150] | |
| page.wait_for_timeout(14500 if ident in (153,154,155,156,174,175) else (10500 if result.get('flash_play_clicked') else 4500)) | |
| if phase=='repair' and server.state.added: | |
| stable=0;last=len(server.state.hits) | |
| for _ in range(18): | |
| active=any(lock.locked() for lock in server.state.busy.values()) | |
| current=len(server.state.hits) | |
| stable=stable+1 if current==last and not active else 0 | |
| if stable>=2:break | |
| last=current;page.wait_for_timeout(1000) | |
| result['title']=page.title();result['body_text']=page.locator('body').inner_text(timeout=3000)[:1800] | |
| result['canvas_count']=page.locator('canvas').count();result['screenshot']=f'evidence/{ident:04d}-{phase}.jpg' | |
| page.screenshot(path=str(REPORT/result['screenshot']),type='jpeg',quality=55,timeout=10000) | |
| result['runtime_test']='input_smoke_passed' if result['input_assertions'] and all(x['passed'] for x in result['input_assertions']) else 'startup_observed_needs_visual_review' | |
| except Exception as e:result['runtime_test']='browser_crashed' if crashed else 'startup_error';result['exception']=str(e)[:500] | |
| result['page_errors']=errors[:30];result['console_warnings_errors']=logs[-25:];result['blocked_external_requests']=sorted(set(blocked));result['browser_crashed']=bool(crashed) | |
| b.close();return result | |
| ids=[int(x) for x in sys.argv[1:]] | |
| with sync_playwright() as p: | |
| for ident in ids: | |
| mp=REPORT/f'game-{ident}.json';m=json.loads(mp.read_text());res={'id':ident,'name':m['name'],'entry':m['entry'],'verification_scope':'staged bucket bytes + observed repairs, network-blocked local HTTP mirror, not file:// or self-contained HTML'} | |
| shutil.rmtree(STAGE,ignore_errors=True);STAGE.mkdir(exist_ok=True) | |
| server=None | |
| try: | |
| total=sum(x['bytes'] for x in m['files']) | |
| if total>350*1024**2: | |
| res['status']='runtime_deferred_memory_limit';res['reason']='Captured assets exceed 350 MiB; 2 GiB test host cannot reliably stage and execute this build concurrently.' | |
| else: | |
| unique={x['path']:x for x in m['files']} | |
| api.download_bucket_files(BUCKET,[(k,STAGE/k) for k in unique if k in remote],raise_on_missing_files=True) | |
| mismatches=[];verified=0 | |
| for key,f in unique.items(): | |
| local=STAGE/key | |
| if not local.exists():continue | |
| with local.open('rb') as stream:sha=hashlib.file_digest(stream,'sha256').hexdigest() | |
| if f.get('sha256') and f['sha256']!=sha:mismatches.append(key) | |
| else:verified+=1 | |
| res['sha256_checked_files']=verified;res['sha256_mismatches']=mismatches | |
| S=State(m);server=ThreadingHTTPServer(('127.0.0.1',0),Handler);server.daemon_threads=False;server.state=S | |
| threading.Thread(target=server.serve_forever,daemon=True).start() | |
| print('Testing',ident,m['name'],round(total/1e6,1),'MB',flush=True) | |
| res['repair_pass']=browser_pass(p,server,ident,'repair') | |
| # Let in-flight requests settle before freezing the file set. | |
| time.sleep(1) | |
| res['repair_fetch_failures']=dict(S.failed) | |
| S.repair=False;S.failed={};hits0=len(S.hits) | |
| res['offline_pass']=browser_pass(p,server,ident,'offline') | |
| res['offline_missing_requests']=dict(S.failed);res['observed_offline_paths']=sorted(set(x['path'] for x in S.hits[hits0:])) | |
| res['repairs']=list(S.added.values());res['runtime_redirects']=S.redirects | |
| res['status']=res['offline_pass']['runtime_test'] | |
| additions=[(STAGE/k,k) for k in S.added if (STAGE/k).exists()] | |
| if additions: | |
| api.batch_bucket_files(BUCKET,add=additions) | |
| lookup={f.path:f for f in api.get_bucket_paths_info(BUCKET,[k for _,k in additions])} | |
| for local,k in additions:assert k in lookup and lookup[k].size==local.stat().st_size | |
| remote.update(lookup) | |
| merged={f['path']:f for f in m['files']};merged.update(S.added);m['files']=list(merged.values());m['runtime_repairs_note']='Browser-observed missing dependencies captured; see verification report for offline test results.' | |
| mp.write_text(json.dumps(m,indent=2));api.batch_bucket_files(BUCKET,add=[(mp,f'_reports/games/{ident:04d}.json')]) | |
| # Always record failures: no loading-screen-only evidence gets promoted to gameplay verified. | |
| out=REPORT/f'verification-{ident:04d}.json';out.write_text(json.dumps(res,indent=2));api.batch_bucket_files(BUCKET,add=[(out,f'_verification/games/{ident:04d}.json')]) | |
| print('RESULT',ident,res['status'],'repairs',len(res.get('repairs',[])),'offline missing',len(res.get('offline_missing_requests',{})),flush=True) | |
| except Exception as e: | |
| res['status']='verification_exception';res['exception']=str(e)[:500];(REPORT/f'verification-{ident:04d}.json').write_text(json.dumps(res,indent=2));print('FAIL',ident,str(e)[:200],flush=True) | |
| finally: | |
| if server:server.shutdown();server.server_close() | |
| shutil.rmtree(STAGE,ignore_errors=True) | |
Xet Storage Details
- Size:
- 23.3 kB
- Xet hash:
- dae8a07014a85fb4dec9dad2559787dcef50684857bd54585043054f5099e7c8
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.