File size: 4,285 Bytes
11330a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Small CDP client and retry policy shared with the harness regression test.
export function nextAttempt(index, position, attempt, maxRetries) {
  if (position > index) return { index: Math.max(index + 1, position), attempt: 0 };
  if (attempt >= maxRetries) return { index: index + 1, attempt: 0, gaveUp: true };
  // Never rewind to a lagging follower after giving up on an earlier event.
  return { index, attempt: attempt + 1, retry: true };
}

// A retry is scheduled after the bounded wait, but a slow follower can commit
// during the beat of grace before that retry is emitted.  Re-read the tracker
// immediately before dispatch so a stale retry cannot re-strike an event that
// has already advanced.
export function retryAlreadyCommitted(index, position) {
  return Number.isFinite(position) && position > index;
}

export function notePilotTarget(targets) {
  return targets.find(t => t.type === 'page' && /^http:\/\/(localhost|127\.0\.0\.1):8000\//.test(t.url));
}

export function summariseRun(trace) {
  const attempts = trace.events.filter(e => e.type === 'input.testEvent');
  const commits = trace.events.filter(e => e.type === 'follow.advance' && e.accepted !== false);
  const indices = new Set(attempts.map(e => e.index));
  const onTime = new Set(), late = new Set();
  const unmatched = [];
  const latencies = [];
  for (const c of commits) {
    const previous = attempts.filter(a => a.index === c.toIndex && a.t <= c.t);
    const a = previous.at(-1);
    if (!a) { unmatched.push({index:c.toIndex,t:c.t,reason:'no prior attempt for this score event'}); continue; }
    const lag = c.t - a.t;
    if (lag <= 450) { onTime.add(c.toIndex); latencies.push(lag); }
    else late.add(c.toIndex);
  }
  return {
    attemptedEvents:indices.size, attempts:attempts.length,
    detectedWithin450ms:onTime.size,
    detectedOnlyLate:[...late].filter(i => !onTime.has(i)).length,
    undetected:[...indices].filter(i => !onTime.has(i) && !late.has(i)),
    commitsWithoutPriorAttempt:unmatched,
    retries:attempts.filter(a => a.attempt > 0).length,
    autoSkips:trace.events.filter(e => e.type === 'skip' && e.reason === 'auto')
      .map(e => ({index:e.fromIndex,heldMs:e.heldMs,dueHeldMs:e.dueHeldMs})),
    medianDetectionMs:latencies.length ? latencies.sort((a,b)=>a-b)[Math.floor(latencies.length/2)] : null,
    invalid:trace.events.filter(e => e.type === 'test.invalid').map(e => e.reason),
  };
}

export async function connectNotePilot(port = 9222) {
  const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`, {signal:AbortSignal.timeout(5000)})).json();
  const page = notePilotTarget(targets);
  if (!page) throw Error('No NotePilot tab found on localhost:8000');
  const ws = new WebSocket(page.webSocketDebuggerUrl);
  await new Promise((resolve, reject) => {
    const timer = setTimeout(() => { ws.close(); reject(Error('CDP connection timed out')); }, 5000);
    ws.onopen = () => { clearTimeout(timer); resolve(); };
    ws.onerror = () => { clearTimeout(timer); reject(Error('CDP connection failed')); };
  });
  let id = 0;
  const pending = new Map();
  ws.onmessage = e => {
    const msg = JSON.parse(e.data), call = pending.get(msg.id);
    if (!call) return;
    pending.delete(msg.id); clearTimeout(call.timer);
    if (msg.error) call.reject(Error(JSON.stringify(msg.error))); else call.resolve(msg.result);
  };
  ws.onclose = () => {
    for (const call of pending.values()) { clearTimeout(call.timer); call.reject(Error('CDP closed')); }
    pending.clear();
  };
  const send = (method, params = {}, timeout = 15000) => new Promise((resolve, reject) => {
    const requestId = ++id;
    const timer = setTimeout(() => { pending.delete(requestId); reject(Error(`CDP timed out: ${method}`)); }, timeout);
    pending.set(requestId, {resolve, reject, timer});
    ws.send(JSON.stringify({id:requestId, method, params}));
  });
  const evaluate = async (expression, timeout = 15000) => {
    const r = await send('Runtime.evaluate', {expression, awaitPromise:true, returnByValue:true, userGesture:true}, timeout);
    if (r.exceptionDetails) throw Error(r.exceptionDetails.exception?.description || JSON.stringify(r.exceptionDetails));
    return r.result.value;
  };
  return {send, evaluate, close: () => ws.close()};
}