#!/usr/bin/env python3 """Actual Paper integration tests in the disposable .runtime/server fixture (EULA must already be accepted).""" import json import os from pathlib import Path import re import signal import subprocess import time import urllib.request import urllib.error ROOT=Path(__file__).resolve().parents[1] SERVER=ROOT/'.runtime/server' TOKEN='' ADMIN_TOKEN='' SCOPE={'player_id':'console','project_id':'default'} def request(method, params=None, expect_error=False, admin=False): payload={'method':method,'params':{**(params or {}),**SCOPE},'requestId':'live-test'} req=urllib.request.Request('http://127.0.0.1:8765/v1/rpc',data=json.dumps(payload).encode(),headers={'Authorization':'Bearer '+(ADMIN_TOKEN if admin else TOKEN),'Content-Type':'application/json'}) try: with urllib.request.urlopen(req,timeout=25) as r: body=json.load(r) except urllib.error.HTTPError as e: body=json.load(e) if expect_error: assert not body['ok'],body return body['error'] assert body['ok'],body return body['result'] def pos(x=8,y=96,z=8):return {'x':x,'y':y,'z':z} def inspect(at):return request('region_inspect',{'min':at,'max':at,'detail':'blocks'})['blocks'][0]['state'] def prepare(lo=None,hi=None,block='minecraft:stone_bricks',dependencies=None): return request('build_prepare',{'recipe':{'version':1,'operations':[{'type':'box','min':lo or pos(),'max':hi or pos(10,98,10),'block':block}]},'dependencies':dependencies or []}) def apply(plan,key):return request('build_apply',{'plan_id':plan['plan_id'],'plan_hash':plan['plan_hash'],'idempotency_key':key}) def wait(predicate,timeout=30): end=time.monotonic()+timeout while time.monotonic()0 and s['status']=='applying' else None progress=wait(in_progress) os.killpg(process.pid,signal.SIGKILL);process.wait(timeout=10) time.sleep(.5) # Simulate an administrator protecting an affected part while offline. # Recovery must retain authorization without requiring write permission. metadata=json.loads(previous_metadata) if previous_metadata else {'parts':[],'cameras':{}} metadata['parts'].append({'id':'live-recovery-protection','name':'Recovery test protection', 'operationId':op['operation_id'],'positions':[lo],'locked':True}) metadata_path.write_text(json.dumps(metadata)) process=launch() wait(lambda:urllib.request.urlopen('http://127.0.0.1:8765/health',timeout=1).status==200,90) console('forceload add 0 0 31 31') recovered=wait(lambda:terminal(op));assert recovered['status']=='recovery_required',recovered review=request('recovery_review',{'operation_id':op['operation_id']},admin=True) request('recovery_review',{'operation_id':op['operation_id']},expect_error=True) console('setblock 8 100 8 minecraft:granite');wait(lambda:inspect(lo)=='minecraft:granite') request('recovery_abandon',{'operation_id':op['operation_id'],'expected_digest':review['currentDigest']},expect_error=True,admin=True) fresh=request('recovery_review',{'operation_id':op['operation_id']},admin=True) abandoned=request('recovery_abandon',{'operation_id':op['operation_id'],'expected_digest':fresh['currentDigest']},admin=True) assert abandoned['status']=='failed' and inspect(lo)=='minecraft:granite',abandoned request('operation_undo_prepare',{'operation_id':op['operation_id']},expect_error=True) report('crash-recovery-abandon',written_before_crash=progress['written'],recovery_positions=fresh['positions'],foreign_block='preserved',replay=False,protected_part='recovery-allowed') console('fill 8 100 8 23 115 23 minecraft:air') wait(lambda:request('region_inspect',{'min':lo,'max':hi,'detail':'summary'})['palette']=={'minecraft:air':4096}) protected_plan=prepare(lo,lo,block='minecraft:stone') blocked=request('build_apply',{'plan_id':protected_plan['plan_id'],'plan_hash':protected_plan['plan_hash'], 'idempotency_key':'live-protected-'+protected_plan['plan_id']},expect_error=True) assert blocked['code']=='permission_denied' and inspect(lo)=='minecraft:air',blocked report('recovery-retains-part-write-protection',status='permission_denied') plan=prepare(pos(9,100,8),pos(9,100,8));new=wait(lambda:terminal(apply(plan,'live-after-recovery-'+plan['plan_id']))) assert new['status']=='applied',new undo=request('operation_undo_prepare',{'operation_id':new['operation_id']}) assert wait(lambda:terminal(apply(undo,'live-after-recovery-undo-'+undo['plan_id'])))['status']=='applied' report('editing-after-durable-recovery',status='applied-and-undone') # Exercise the actual MCP stdio transport, including .schem roundtrip. smoke_env=dict(os.environ,MCB_AGENT_TOKEN=TOKEN,MCB_PLAYER_ID='console', MCB_PROJECT_ID='default',MCB_TEST_X='8',MCB_TEST_Y='96',MCB_TEST_Z='8', MCB_BACKEND_URL='http://127.0.0.1:8765') smoke_env.pop('MCB_TOKEN',None) smoke=subprocess.run(['node','test/live-paper.mjs'],cwd=ROOT/'bridge',env=smoke_env, text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=90) (ROOT/'.runtime/live-mcp-results.log').write_text(smoke.stdout) print(smoke.stdout,flush=True) assert smoke.returncode==0,'Live MCP smoke failed; inspect .runtime/live-mcp-results.log' report('mcp-stdio-schematic-roundtrip',status='passed') finally: if process.poll() is None: try:console('stop');process.wait(timeout=30) except (OSError,subprocess.TimeoutExpired):process.terminate();process.wait(timeout=15) log.close();config.write_text(previous) if previous_metadata is None:metadata_path.unlink(missing_ok=True) else:metadata_path.write_bytes(previous_metadata) (ROOT/'.runtime/live-server-results.json').write_text(json.dumps(results,indent=2)+'\n') print('LIVE PAPER CONFLICT/UNDO/CANCEL/CRASH TESTS PASSED',flush=True)