"""Real Docker contract: shared desktop profile, reconnect, restart, transfer paths. Build grokboy-box:surface-test first; uses an isolated container and temporary data. """ import json, subprocess, time, uuid, select NAME = 'grokboy-surface-test-' + uuid.uuid4().hex[:10] IMAGE = 'grokboy-box:surface-test' def docker(*args, **kw): return subprocess.run(['docker', *args], check=True, capture_output=True, text=True, **kw).stdout.strip() def ready(): for _ in range(120): try: docker('exec', NAME, 'python3', '-c', 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:9222/json/version",timeout=1)') return except subprocess.CalledProcessError: time.sleep(.5) raise AssertionError('Chromium did not become ready') def fixture(): docker('exec', NAME, 'sh', '-c', 'mkdir -p /workspace/fixture; printf "fixturedownload" > /workspace/fixture/index.html; printf fixture-bytes > /workspace/fixture/download.bin; printf upload-fixture > /workspace/upload.txt') docker('exec', '-d', NAME, 'python3', '-m', 'http.server', '8765', '--directory', '/workspace/fixture') def helper(): return subprocess.Popen(['docker','exec','-i','-w','/workspace','-e','GROKBOY_BROWSER_CDP=http://127.0.0.1:9222',NAME,'flock','-n','/home/box/.grokboy-browser.lock','node','/opt/grokboy/playwright/browser_helper.mjs'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) def request(p, op, **args): p.stdin.write(json.dumps({'id':'fixture', 'op':op, **args})+'\n'); p.stdin.flush() assert select.select([p.stdout], [], [], 30)[0], f'helper timed out: {op}' line=p.stdout.readline(); assert line, p.stderr.read() value=json.loads(line); assert value.get('ok'), value return value def close(p): request(p,'close'); p.stdin.close(); p.wait(timeout=10) try: docker('run','-d','--name',NAME,'--shm-size=1g',IMAGE) ready(); fixture() p=helper() request(p,'navigate',url='http://127.0.0.1:8765') request(p,'eval',expression='document.cookie="login=fixture; max-age=3600; path=/"; localStorage.setItem("account","fixture"); sessionStorage.setItem("tab","same"); true') request(p,'upload',selector='#upload',path='/workspace/upload.txt') assert request(p,'eval',expression='document.querySelector("#upload").files[0].name')['result']=='upload.txt' request(p,'download',selector='#download',path='/workspace/saved.bin') assert docker('exec',NAME,'cat','/workspace/saved.bin')=='fixture-bytes' print('PASS upload/download use Docker workspace', flush=True) info=request(p,'handoff_prepare'); assert info['headed'] and not info['relaunched'], info close(p) ready() # releasing the tool must not close the desktop browser p=helper() result=request(p,'eval',expression='[document.cookie,localStorage.getItem("account"),sessionStorage.getItem("tab")]')['result'] assert result==['login=fixture','fixture','same'], result invalid=request(p,'status'); assert invalid['headed'] other=helper() other.communicate(json.dumps({'id':'busy','op':'ping'})+'\n',timeout=5) assert other.returncode != 0, 'second helper stole the browser lease' p.kill(); p.wait(timeout=5) p.stdin.close(); p.stdout.close(); p.stderr.close() for _ in range(30): check=subprocess.run(['docker','exec',NAME,'flock','-n','/home/box/.grokboy-browser.lock','true'],capture_output=True) if check.returncode == 0: break time.sleep(.1) assert check.returncode == 0, 'cancelled helper leaked its browser lease' p=helper() assert request(p,'eval',expression='localStorage.getItem("account")')['result']=='fixture' close(p) print('PASS competing helpers excluded; cancellation releases the lease', flush=True) print('PASS handoff and helper reconnect retain same browser, cookie, localStorage, sessionStorage', flush=True) docker('restart', NAME); ready(); fixture() p=helper(); request(p,'navigate',url='http://127.0.0.1:8765') result=request(p,'eval',expression='[document.cookie,localStorage.getItem("account")]')['result'] assert result==['login=fixture','fixture'], result close(p) print('PASS Docker restart retains Chromium login profile', flush=True) finally: subprocess.run(['docker','rm','-f',NAME],capture_output=True)