oxedyne/daimond/dev/shot_agentctl.mjs
5.3 KiB, 1 run
created by r2519314175:137, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // shot_agentctl.mjs — behavioural check of the Agents panel's pause / resume / |
| 2 | // stop controls (header, acting on all) and the running/paused/queued tally. |
| 3 | // |
| 4 | // Drives the real UI: a fan-out of slow (@long) workers, then Pause-all → Resume-all → |
| 5 | // Stop-all, asserting the tiles' state and the header at each step. Needs dev/serve.mjs |
| 6 | // (DAIMOND_PORT, default 8777) + dev/mockllm.mjs. |
| 7 | import { open, shot } from './harness.mjs'; |
| 8 | |
| 9 | const s = await open({ name: 'agentctl-shot', signIn: true, connect: true }); |
| 10 | const { page } = s; |
| 11 | const pause = (ms) => page.waitForTimeout(ms); |
| 12 | |
| 13 | async function newDiamond(name) { |
| 14 | await page.click('#new-diamond-btn', { force: true }); |
| 15 | await page.waitForSelector('.dlg-input', { timeout: 8000 }); |
| 16 | await page.fill('.dlg-input', name); |
| 17 | await page.click('.dlg-ok'); |
| 18 | await page.waitForSelector('#steer-input', { timeout: 10000 }); |
| 19 | await pause(300); |
| 20 | } |
| 21 | |
| 22 | async function fanOut(names) { |
| 23 | // Each worker runs a slow @long stream, so it stays running long enough to |
| 24 | // be caught and paused. |
| 25 | const calls = names.map(n => `spawn_agent {"name":"${n}","task":"@long 60"}`).join(' ;; '); |
| 26 | await page.fill('#steer-input', '@tools ' + calls); |
| 27 | await page.click('#steer-send'); |
| 28 | // The governor's predictive dispatch gate may ask to confirm a fan-out that |
| 29 | // would outpace the pace budget — accept it and let the workers run. |
| 30 | await pause(500); |
| 31 | const okBtn = await page.$('.dlg-ok'); |
| 32 | if (okBtn) { await okBtn.click(); await pause(300); } |
| 33 | } |
| 34 | |
| 35 | // Snapshot the pane + header state. |
| 36 | const snap = () => page.evaluate(() => { |
| 37 | const cards = [...document.querySelectorAll('#agents-list .acard')]; |
| 38 | const st = (cls) => cards.filter(c => c.classList.contains(cls)).length; |
| 39 | const btn = (id) => { const b = document.getElementById(id); return b ? { shown: b.offsetParent !== null, disabled: !!b.disabled } : null; }; |
| 40 | const ctl = document.getElementById('agents-ctl'); |
| 41 | return { |
| 42 | cards: cards.length, |
| 43 | running: st('running'), paused: st('paused'), stopped: st('stopped'), queued: st('queued'), |
| 44 | stat: (document.getElementById('agents-stat') || {}).textContent || '', |
| 45 | statShown: (() => { const e = document.getElementById('agents-stat'); return e && e.offsetParent !== null; })(), |
| 46 | ctlShown: ctl ? ctl.offsetParent !== null : false, |
| 47 | holding: ctl ? ctl.classList.contains('holding') : false, |
| 48 | btnPause: btn('agents-pause'), btnPlay: btn('agents-play'), btnStop: btn('agents-stop'), |
| 49 | }; |
| 50 | }); |
| 51 | |
| 52 | // ── Fan out four slow workers ─────────────────────────────────────────── |
| 53 | await newDiamond('Runaway test'); |
| 54 | await fanOut(['worker-a', 'worker-b', 'worker-c', 'worker-d']); |
| 55 | await pause(1600); |
| 56 | const running = await snap(); |
| 57 | console.log('RUNNING:', JSON.stringify(running)); |
| 58 | await shot(s, 'agentctl-running'); |
| 59 | |
| 60 | // ── Pause all ─────────────────────────────────────────────────────────── |
| 61 | await page.click('#agents-pause', { force: true }); |
| 62 | await pause(900); |
| 63 | const paused = await snap(); |
| 64 | console.log('PAUSED :', JSON.stringify(paused)); |
| 65 | await shot(s, 'agentctl-paused'); |
| 66 | |
| 67 | // ── Resume all ────────────────────────────────────────────────────────── |
| 68 | await page.click('#agents-play', { force: true }); |
| 69 | await pause(2200); |
| 70 | const resumed = await snap(); |
| 71 | console.log('RESUMED:', JSON.stringify(resumed)); |
| 72 | await shot(s, 'agentctl-resumed'); |
| 73 | |
| 74 | // ── Fan out again, then Stop all (the kill switch) ────────────────────── |
| 75 | await page.click('.diamond-box:has-text("Runaway test")', { force: true }); |
| 76 | await page.waitForSelector('#steer-input', { timeout: 8000 }); |
| 77 | await fanOut(['kill-a', 'kill-b', 'kill-c']); |
| 78 | await pause(1600); |
| 79 | const running2 = await snap(); |
| 80 | console.log('RUNNING2:', JSON.stringify(running2)); |
| 81 | await page.click('#agents-stop', { force: true }); |
| 82 | await pause(900); |
| 83 | const stopped = await snap(); |
| 84 | console.log('STOPPED:', JSON.stringify(stopped)); |
| 85 | await shot(s, 'agentctl-stopped'); |
| 86 | |
| 87 | // ── Verdicts ──────────────────────────────────────────────────────────── |
| 88 | const realErrs = s.errs.filter(e => !/502|Bad Gateway|\/api\b/.test(e)); |
| 89 | const ok = |
| 90 | // Four workers running, header visible with the right button states. |
| 91 | running.running >= 3 && running.ctlShown |
| 92 | && running.btnPause.disabled === false && running.btnPlay.disabled === true |
| 93 | && /running/.test(running.stat) |
| 94 | // Pause hangs them all up: paused tiles, resume enabled, pause disabled, held. |
| 95 | && paused.paused >= 3 && paused.running === 0 |
| 96 | && paused.btnPause.disabled === true && paused.btnPlay.disabled === false |
| 97 | && paused.holding === true && /paused/.test(paused.stat) |
| 98 | // Resume clears the paused state (they run on and finish). |
| 99 | && resumed.paused === 0 |
| 100 | // Stop-all finalises the second fan-out to stopped. |
| 101 | && running2.running >= 2 |
| 102 | && stopped.stopped >= 2 && stopped.running === 0 && stopped.paused === 0 |
| 103 | && realErrs.length === 0; |
| 104 | console.log(ok ? '\n✅ PASS — pause / resume / stop all work; counts track state' : '\n❌ FAIL — see above'); |
| 105 | if (realErrs.length) console.log('PAGE ERRORS:', realErrs); |
| 106 | await s.close(); |
| 107 | process.exit(ok ? 0 : 1); |