File size: 5,291 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""Exercise the real HTTP bridge against a CLI-generated local world. Standard library only."""
import argparse
import json
import secrets
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
from pathlib import Path

p = argparse.ArgumentParser()
p.add_argument('--dotnet', default='dotnet')
p.add_argument('--build-plan', help='Optional real Twin-O-Matic export to exercise')
a = p.parse_args()
root = Path(__file__).resolve().parents[1]
cli = root / 'src/Repoverse.Cli/bin/Debug/net8.0/Repoverse.Cli.dll'
service = root / 'src/Repoverse.Sidecar/bin/Debug/net8.0/Repoverse.Sidecar.dll'
with tempfile.TemporaryDirectory(prefix='repoverse-http-') as temp:
    work = Path(temp)
    subprocess.run([a.dotnet, str(cli), 'ingest-local', str(root), '--owner', 'SNAPKITTYWEST', '--out', str(work/'snapshot.json')], check=True)
    subprocess.run([a.dotnet, str(cli), 'generate', '--snapshot', str(work/'snapshot.json'), '--out', str(work/'manifest.json')], check=True)
    with socket.socket() as sock:
        sock.bind(('127.0.0.1', 0))
        port = sock.getsockname()[1]
    token = secrets.token_hex(32)
    args = [a.dotnet, str(service), '--manifest', str(work/'manifest.json'), '--token', token,
            '--port', str(port), '--save', str(work/'save.json'), '--source-root', str(root), '--source-repo', 'snapkittywest/repoverse']
    with (work/'service.log').open('w') as log:
        child = subprocess.Popen(args, stdout=log, stderr=log)
        try:
            def request(route, data=None, auth=token, origin=None):
                headers = {'X-Repoverse-Token': auth}
                if origin: headers['Origin'] = origin
                if data is not None: headers['Content-Type'] = 'application/json'
                req = urllib.request.Request(f'http://127.0.0.1:{port}'+route, data=None if data is None else json.dumps(data).encode(), headers=headers)
                try:
                    with urllib.request.urlopen(req, timeout=15) as response:
                        return response.status, json.load(response)
                except urllib.error.HTTPError as e:
                    return e.code, e.read().decode()
            for _ in range(100):
                if child.poll() is not None: raise RuntimeError((work/'service.log').read_text())
                try:
                    if request('/v1/health')[0] == 200: break
                except urllib.error.URLError: time.sleep(.1)
            else: raise RuntimeError('Sidecar did not start')
            assert request('/v1/health', auth='bad')[0] == 403
            assert request('/v1/health', origin='https://example.com')[0] == 403
            status, session = request('/v1/session', {})
            assert status == 200 and session['schemaVersion'] == 1 and session['destinations']
            player = session['player']
            resident_ids = set()
            for _ in range(100):
                status, packet = request('/v1/focus', {'x': player['x'], 'z': player['z']})
                assert status == 200 and len(packet['chunks']) <= 4
                resident_ids.difference_update(packet['unloaded'])
                resident_ids.update(c['id'] for c in packet['chunks'])
                if packet['remaining'] == 0: break
            else: raise AssertionError('Focus did not settle')
            assert resident_ids
            plan = json.loads(Path(a.build_plan).read_text()) if a.build_plan else {
                'schemaVersion': 1, 'id': 'http-builder', 'actor': 'twin-builder', 'units': 'voxel',
                'origin': {'x': 0, 'y': 9, 'z': 0},
                'operations': [{'name': 'add_box', 'args': {'width': 2, 'height': 2, 'depth': 2, 'y': 1, 'color': '#5ad1c4'}}]}
            status, receipt = request('/v1/build', plan)
            assert status == 200 and receipt['voxels'] > 0, receipt
            assert request('/v1/build', {**plan, 'operations': plan['operations'] + [{'name': 'eval', 'args': {}}]})[0] == 400
            assert request('/v1/focus', {'x': player['x'], 'z': player['z']})[1]['builders'] == [receipt]
            player['selection'] = 'repoverse://snapkittywest/repoverse/README.md'
            assert request('/v1/save', player)[0] == 200
            assert request('/v1/load', {})[1] == player
            assert json.loads((work/'save.json').read_text())['builders'] == [receipt]
            status, source = request('/v1/source?repo=snapkittywest%2Frepoverse&path=README.md&offset=0')
            assert status == 200 and source['lines'], source
            assert request('/v1/source?repo=snapkittywest%2Frepoverse&path=.git%2Fconfig&offset=0')[0] == 400
            assert request('/v1/focus', {'x': 1e20, 'z': 0})[0] == 400
            # A newly connected renderer must receive the world again.
            assert request('/v1/session', {})[1]['builders'] == [receipt]
            assert request('/v1/focus', {'x': player['x'], 'z': player['z']})[1]['chunks']
            print('PASS: authentication, browser isolation, real mesh stream, save/load, source paging, invalid requests, builder placement/persistence, reconnect')
        finally:
            child.terminate()
            try: child.wait(timeout=5)
            except subprocess.TimeoutExpired: child.kill(); child.wait()