tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

io-source-probe.mjs (8287B)


      1 // Real compiled source libraries, native evaluators and the private event session.
      2 export async function run(_control,backend,terminalMode=false){
      3  const base=new URL('../',document.querySelector('script[type=module][src]').src),fixture=await(await fetch('/io-source.json')).json(),checks=[];
      4  const input=[...new TextEncoder().encode('\ufeffA馃榾位e\u0301\r\n\0')];
      5  async function drive(name,start,turn){
      6   if(terminalMode)return driveTerminal(name,start,turn);
      7   let report=await start(),queue=[...report.events],offset=0,pendingRead=null,cancelled=false;const output=[];
      8   for(let i=0;i<2000&&!report.done;i++){
      9    if(!queue.length){report=await turn({run:report.run,kind:'poll'});queue.push(...report.events);continue;}
     10    const event=queue.shift();let reply;
     11    if(event.kind==='read'){
     12     if(name==='cancellation'){pendingRead=event.id;continue;}
     13     if(name!=='roundTrip')throw Error('Unexpected read');
     14     const bytes=input.slice(offset,offset+Math.min(event.limit,1));offset+=bytes.length;
     15     reply={...event,bytes,eof:bytes.length===0};
     16    }else if(event.kind==='write'){
     17     if(name==='cancellation'&&pendingRead===null)throw Error('Write ran without the expected pending reader');
     18     // Deliberate positive partial progress: the SOURCE writeAll must advance
     19     // exactly once, including through a split multi-byte scalar.
     20     output.push(event.bytes[0]);reply={...event,count:1};
     21    }else{
     22     if(event.id!==pendingRead||cancelled)throw Error('Unexpected cancellation');
     23     cancelled=true;reply={...event,kind:'cancelled'};
     24    }
     25    report=await turn({run:report.run,kind:'reply',reply});if(report.accepted!==true)throw Error('Provider reply rejected');queue.push(...report.events);
     26   }
     27   const expected=name.startsWith('invalid')?[]:name==='roundTrip'?input:[...new TextEncoder().encode(name==='cancellation'?'ready':'位馃榾e\u0301\r\n')];
     28   if(!report.done||report.outcome.kind!=='completed'||report.outcome.value!=='42'||report.outcome.fault)throw Error(name+': '+JSON.stringify(report));
     29   if(JSON.stringify(output)!==JSON.stringify(expected))throw Error(name+' output byte mismatch: '+JSON.stringify(output));
     30   if(name==='cancellation'&&!cancelled)throw Error('Missing source cancellation');
     31   if(!Object.values(report.counts).every(group=>Object.values(group).every(v=>v===0)))throw Error('Retained run records');
     32   checks.push(name+': full bytes/result42; split UTF8 and source partial-progress handling');
     33  }
     34  async function driveTerminal(name,start,turn){
     35   const {ioTerminalView:{terminalView}}=await import(new URL('dist/probe-api.js',base));
     36   let report=await start(),stop=false,sent=false,aborted=false;const replies=[],parent=document.createElement('div');document.body.append(parent);
     37   const widget=terminalView(parent,report.run,r=>replies.push(r),()=>{stop=true;});
     38   try{
     39    for(let i=0;i<2000&&!report.done;i++){
     40     for(const event of report.events){
     41      const old=parent.querySelector('form'),oldEOF=old?.querySelector('button[type=button]');
     42      widget.handle(event);
     43      if(event.kind==='cancel'){
     44       aborted=true;if(!parent.querySelector('form').hidden)throw Error('Input still visible at cancellation acknowledgement');
     45       const count=replies.length;old.dispatchEvent(new Event('submit',{cancelable:true}));oldEOF?.click();
     46       if(replies.length!==count)throw Error('Stale DOM prompt emitted reply');
     47      }
     48      if(event.kind==='read'&&name==='roundTrip'){
     49       const form=parent.querySelector('form');
     50       if(!sent){form.querySelector('input').value='\ufeffA馃榾位e\u0301\0';form.dispatchEvent(new Event('submit',{cancelable:true}));sent=true;}
     51       else form.querySelector('button[type=button]').click();
     52      }
     53      if(event.kind==='read'&&name==='externalStop')parent.querySelector('section > button').click();
     54     }
     55     const command=stop?{kind:'stop'}:replies.length?{kind:'reply',reply:replies.shift()}:{kind:'poll'};stop=false;
     56     report=await turn({run:report.run,...command});
     57     if(command.kind==='reply'&&report.accepted!==true)throw Error('UI completion rejected');
     58    }
     59    if(!report.done||report.outcome.kind!==(name==='externalStop'?'cancelled':'completed')||report.outcome.fault)throw Error('UI run: '+JSON.stringify(report));
     60    if(name!=='externalStop'&&report.outcome.value!=='42')throw Error('UI result mismatch');
     61    widget.finish();const snapshot=widget.snapshot();
     62    const expected=name==='binary'?[92,120,102,102,255,240,159]:name==='roundTrip'?[...new TextEncoder().encode('\ufeffA馃榾位e\u0301\0\n')]:name.startsWith('invalid')||name==='externalStop'?[]:[...new TextEncoder().encode(name==='cancellation'?'ready':'位馃榾e\u0301\r\n')];
     63    if(JSON.stringify(snapshot.output.bytes)!==JSON.stringify(expected))throw Error('UI output mismatch '+name);
     64    if(parent.querySelector('[data-hex=output]').textContent!==expected.map(b=>b.toString(16).padStart(2,'0')).join(' '))throw Error('Wrong hex view');
     65    if(name==='binary'){
     66     const text=parent.querySelector('[data-stream=output]');
     67     if(text.firstChild.nodeType!==Node.TEXT_NODE||text.firstChild.textContent!=='\\xff'||text.querySelectorAll('mark').length!==2||!text.querySelector('[data-byte-reason=incomplete]'))throw Error('Byte escapes indistinguishable from literal text');
     68    }
     69    if(['externalStop','cancellation'].includes(name)&&!aborted)throw Error('No UI cancellation');
     70    if(!Object.values(report.counts).every(group=>Object.values(group).every(v=>v===0)))throw Error('UI run retained records');
     71    if(replies.length||parent.querySelector('form'))throw Error('UI not retired');
     72    checks.push('terminal '+name+': exact bytes, DOM lifecycle and outcome');
     73   }finally{parent.remove();}
     74  }
     75  if(terminalMode){
     76   const {ioTerminalView:{terminalView}}=await import(new URL('dist/probe-api.js',base));
     77   const parent=document.createElement('div'),replies=[];document.body.append(parent);let stops=0;
     78   const widget=terminalView(parent,'900',r=>replies.push(r),()=>{stops++;});
     79   try{
     80    widget.handle({run:'900',id:1,kind:'read',limit:4096});const stale=parent.querySelector('form'),staleEOF=stale.querySelector('button[type=button]');
     81    widget.handle({run:'900',id:1,kind:'cancel',operation:'read'});widget.handle({run:'900',id:2,kind:'read',limit:4096});
     82    stale.dispatchEvent(new Event('submit',{cancelable:true}));staleEOF.click();
     83    if(replies.length!==1||parent.querySelector('form').hidden)throw Error('Old prompt consumed successor');
     84    const form=parent.querySelector('form');form.querySelector('input').value='\ud800';form.dispatchEvent(new Event('submit',{cancelable:true}));
     85    if(replies.length!==1||form.hidden)throw Error('Invalid line consumed wait');
     86    form.querySelector('input').value='';form.dispatchEvent(new Event('submit',{cancelable:true}));
     87    if(replies.at(-1).eof||JSON.stringify(replies.at(-1).bytes)!=='[10]')throw Error('Empty line conflated with EOF');
     88    const text='<script>throw Error("injected")</script><img src=x onerror=alert(1)>';
     89    widget.handle({run:'900',id:3,kind:'write',stream:'error',bytes:[...new TextEncoder().encode(text)]});
     90    if(parent.querySelector('script,img')||parent.querySelector('[data-stream=error]').textContent!==text)throw Error('Output interpreted as markup');
     91    const button=parent.querySelector('section > button');button.click();button.click();if(stops!==1)throw Error('Repeated stop');
     92    widget.finish();checks.push('terminal DOM: stale prompt isolation, malformed/empty line, inert markup, once-only stop');
     93   }finally{parent.remove();}
     94  }
     95  const names=terminalMode?['direct','handled','roundTrip','cancellation','invalidUTF8','invalidScalar','binary','externalStop']:['direct','handled','roundTrip','cancellation','invalidUTF8','invalidScalar'];
     96   const {CompilerWorker}=await import(new URL('runtime/haskell/client.mjs',base)),client=new CompilerWorker();
     97   try{
     98    await client.request({type:'init',compiler:false});await client.request({type:'load',bytes:new Uint8Array(fixture.bytes).buffer,hash:fixture.hash});
     99    for(const name of names)await drive(name,
    100     async()=>JSON.parse((await client.request({type:'io-session-start',name:name==='externalStop'?'roundTrip':name,arguments:[]})).text),
    101     async command=>JSON.parse((await client.request({type:'io-session-turn',...command})).text));
    102   }finally{client.dispose();}
    103  return {backend,checks};
    104 }