"""Local daemon + two CLI clients + mock provider. No paid model calls.""" import json, os, signal, socket, sqlite3, subprocess, tempfile, threading, time from pathlib import Path from http.server import ThreadingHTTPServer from runtime_flow import Provider from cli_flow import BINARY, tool, response def wait(fn, seconds=15): end=time.monotonic()+seconds while time.monotonic() result.txt','yield_time_ms':10000})) if len(results)==1:return response(tool('external_read_file',{'path':'result.txt'})) assert results[-1]['content']=='artifact',results return response(tool('report_done',{'message':'Read result.txt and verified artifact.'})) if goal=='QUESTION': if not results:return response(tool('request_user_input',{'question':'Choose a label','options':['alpha','beta']})) assert results[-1].get('answer')=='beta' or any('beta' in m.get('content','') and 'unanswered question' in m.get('content','') for m in msgs),results return response(tool('report_done',{'message':'selected beta'})) if goal=='STEER': if not results: first=tool('external_exec_command',{'cmd':'echo started > steer.started; sleep 0.8','yield_time_ms':10000}) second=tool('external_write_file',{'path':'forbidden.txt','content':'bad'})['tool_calls'][0];second['id']='second';first['tool_calls'].append(second) return response(first) assert not (workspace/'forbidden.txt').exists() assert any('do not write forbidden' in m.get('content','') for m in msgs) return response(tool('report_done',{'message':'steering applied; no forbidden file'})) if goal in ('BROWSER_SET','BROWSER_CHECK','BROWSER_OTHER'): if not results:return response(tool('browser_navigate',{'url':f'http://127.0.0.1:{server.server_port}/fixture'})) if len(results)==1: expression="document.cookie='private_cookie=one'; localStorage.setItem('private','one'); 'set'" if goal=='BROWSER_SET' else "({cookie:document.cookie,storage:localStorage.getItem('private')})" return response(tool('browser_eval',{'expression':expression})) assert 'error' not in results[-1],results if goal=='BROWSER_SET' and len(results)==2:return response(tool('browser_release',{})) if goal=='BROWSER_CHECK':assert results[-1]['result']=={'cookie':'private_cookie=one','storage':'one'},results[-1] if goal=='BROWSER_OTHER':assert results[-1]['result']=={'cookie':'','storage':None},results[-1] return response(tool('report_done',{'message':'browser profile verified'})) if goal=='LONG': if not results:return response(tool('external_exec_command',{'cmd':'echo $$ > running.pid; sleep 60','yield_time_ms':10000})) return response(tool('report_done',{'message':'should not finish'})) raise AssertionError((goal,results)) server.callback=callback;threading.Thread(target=server.serve_forever,daemon=True).start() env={**os.environ,'GROKBOY_DATA_DIR':str(data),'GROKBOY_API_KEY':'mock','GROKBOY_MODEL':'mock','GROKBOY_BASE_URL':f'http://127.0.0.1:{server.server_port}/v1','GROKBOY_MAX_ROUNDS_TOTAL':'48','GROKBOY_BROWSER_HEADED':'0','GROKBOY_BROWSER_SURFACE':'local'} logs=[];clients=[];daemons=[] def start(): p=subprocess.Popen([str(BINARY),'serve'],cwd=workspace,env=env,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) daemons.append(p) threading.Thread(target=lambda:[logs.append(l) for l in p.stderr],daemon=True).start() wait(lambda:(data/'service.sock').exists() and p.poll() is None) wait(lambda:rpc('ping')) return p def rpc(op,agent=None,**kw): with socket.socket(socket.AF_UNIX) as s: s.settimeout(5);s.connect(str(data/'service.sock'));s.sendall((json.dumps({'op':op,'agent':agent,**kw})+'\n').encode());f=s.makefile();v=json.loads(f.readline()) assert 'error' not in v,v return v def tasks():return rpc('tasks','a') def latest(goal):return next((t for t in reversed(tasks()) if t['goal']==goal),None) def terminal(goal): t=latest(goal);return t if t and t['state']=='terminal' else None def event_reply(agent,text):return any(text in e['payload'].get('message','') for e in rpc('events',agent,after=0)['events'] if e['kind']=='reply') def client(name): p=subprocess.Popen([str(BINARY),'agent','--name',name],cwd=workspace,env=env,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True) out=[];threading.Thread(target=lambda:[out.append(l) for l in p.stdout],daemon=True).start();threading.Thread(target=lambda:[logs.append(l) for l in p.stderr],daemon=True).start();clients.append(p);return p,out def send(p,text):p.stdin.write(text+'\n');p.stdin.flush() daemon=None try: daemon=start() for name in ['a','b']:rpc('create',name=name,cwd=str(workspace)) a,aout=client('a');b,bout=client('b') send(b,'teach research');wait(lambda:rpc('expertise','b')['expertise']=='research analysis') assert rpc('memory','a')==[] assert 'private example note' not in json.dumps(rpc('agents')) print('PASS automatic expertise extraction and private memory isolation',flush=True) send(a,'start ROOT_TASK');wait(lambda:latest('CHILD_TASK')) send(a,'ping');send(b,'ping') wait(lambda:any('pong' in x for x in aout));wait(lambda:any('pong' in x for x in bout)) root_task=wait(lambda:terminal('ROOT_TASK')) assert root_task['verdict']=='done',root_task assert (workspace/'result.txt').read_text()=='artifact' wait(lambda:any('Background report' in x for x in aout)) print('PASS expertise routing, nested delegation, verified artifact and two live chats',flush=True) send(a,'start QUESTION');wait(lambda:latest('QUESTION') and latest('QUESTION')['state']=='waiting_input') q=latest('QUESTION');send(a,'/exit');a.wait(timeout=5) assert latest('QUESTION')['state']=='waiting_input' send(b,'ping');wait(lambda:event_reply('b','pong')) rpc('chat','a',message='2') assert wait(lambda:terminal('QUESTION'))['verdict']=='done' a,aout=client('a');wait(lambda:any('Background report' in x for x in aout)) print('PASS durable question, CLI disconnect and reconnect notification',flush=True) send(a,'start LONG');wait(lambda:(workspace/'running.pid').exists());long=latest('LONG') rpc('stop','a',task=long['id']);wait(lambda:terminal('LONG')) pid=int((workspace/'running.pid').read_text()) def dead(): try:os.kill(pid,0);return False except ProcessLookupError:return True wait(dead) send(b,'ping');wait(lambda:event_reply('b','pong')) print('PASS task cancellation kills process group without stopping peer chat',flush=True) send(a,'start STEER');wait(lambda:(workspace/'steer.started').exists()) steering=latest('STEER');rpc('say','a',task=steering['id'],message='do not write forbidden.txt') assert wait(lambda:terminal('STEER'))['verdict']=='done' assert not (workspace/'forbidden.txt').exists() print('PASS task-specific steering skips unstarted actions',flush=True) for goal in ('BROWSER_SET','BROWSER_CHECK'): send(a,'start '+goal);result=wait(lambda:terminal(goal),30);assert result['verdict']=='done',result send(b,'start BROWSER_OTHER');assert wait(lambda:next((t for t in rpc('tasks','b') if t['goal']=='BROWSER_OTHER' and t['state']=='terminal'),None),30)['verdict']=='done' profiles=list((data/'profiles').glob('owner-*.browser'));assert len(profiles)==2,profiles print('PASS real Chromium login persists across tasks; different owners remain isolated',flush=True) send(a,'start QUESTION');wait(lambda:latest('QUESTION')['state']=='waiting_input') q=latest('QUESTION');daemon.kill();daemon.wait(timeout=5) daemon=start();t=latest('QUESTION');assert t['verdict']=='interrupted' and t['question'],t rpc('resume','a',task=q['id']);wait(lambda:latest('QUESTION')['state']=='waiting_input') rpc('say','a',task=q['id'],message='2') assert wait(lambda:terminal('QUESTION'))['verdict']=='done' print('PASS daemon crash, unknown-state recovery and explicit resume',flush=True) db=sqlite3.connect(data/'team.sqlite3'); for (raw,) in db.execute('select data from tasks'): t=json.loads(raw);assert t['requests']<=t['limit'] assert not server.errors,server.errors except Exception: print('daemon/client logs:\n'+''.join(logs));raise finally: for p in clients: if p.poll() is None:p.terminate() for p in daemons: if p.poll() is None:p.send_signal(signal.SIGINT);p.wait(timeout=10) server.shutdown() if __name__=='__main__':main()