oxedyne/daimond/dev/verify_agents.mjs
5.9 KiB, 1 run
created by r2519314175:229, 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 | // The conductor dispatches workers; a finished worker folds in ONCE (not |
| 2 | // repeatedly), and finished tiles can be cleared. |
| 3 | // |
| 4 | // The header used to claim "a failed worker is not offered a fold" as well. |
| 5 | // Every worker in this fixture succeeds, so that sentence was never under test |
| 6 | // and has gone rather than being left standing as a claim the run does not make. |
| 7 | // |
| 8 | // "FOLDS IN ONCE" is a claim about what is left afterwards: the card that folded |
| 9 | // carries the folded mark and is no longer OFFERED the fold, and the offer count |
| 10 | // drops by exactly one. That is checked per card now; the old version counted |
| 11 | // buttons in the panel as a whole, which a second card losing its button would |
| 12 | // also satisfy. |
| 13 | // |
| 14 | // PROVED AGAINST A SHORT DISPATCH FIRST. `--break two` steers with two |
| 15 | // spawn_agent calls instead of three, and the dispatch check must go red. |
| 16 | // |
| 17 | // node dev/verify_agents.mjs --break two # expected to FAIL |
| 18 | // node dev/verify_agents.mjs # and then, clean |
| 19 | import { open, shot, errors } from './harness.mjs'; |
| 20 | |
| 21 | const ok = [], bad = []; |
| 22 | const check = (name, pass, detail) => { |
| 23 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 24 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 25 | }; |
| 26 | |
| 27 | const BREAK = (() => { |
| 28 | const i = process.argv.indexOf('--break'); |
| 29 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 30 | })(); |
| 31 | if (BREAK && BREAK !== 'two') { |
| 32 | console.error(`unknown break '${BREAK}'; known: two`); |
| 33 | process.exit(2); |
| 34 | } |
| 35 | |
| 36 | /// One row per worker card: what it is called, what state it is in, whether it |
| 37 | /// carries the folded mark and whether it is still being offered a fold. |
| 38 | const cards = (page) => page.evaluate(() => [...document.querySelectorAll('#panel-agents .acard')].map(c => ({ |
| 39 | name: ((c.querySelector('.an') || {}).textContent || '').trim().slice(0, 24), |
| 40 | status: ((c.querySelector('.pill') || {}).textContent || '').trim(), |
| 41 | folded: !!c.querySelector('.afolded'), |
| 42 | offer: [...c.querySelectorAll('.abtn')].some(b => /Fold in/.test(b.textContent)), |
| 43 | }))); |
| 44 | |
| 45 | const s = await open({ name: 'agents' }); |
| 46 | if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`); |
| 47 | |
| 48 | // New Diamond. |
| 49 | await s.page.click('#new-diamond-btn'); |
| 50 | await s.page.waitForSelector('.dlg-input', { timeout: 8000 }); |
| 51 | await s.page.fill('.dlg-input', 'Dispatch Test'); |
| 52 | await s.page.click('.dlg-ok'); |
| 53 | await s.page.waitForTimeout(1000); |
| 54 | |
| 55 | // Steer with three spawn_agent calls in one turn. |
| 56 | const spawns = ['{"name":"alpha","task":"do A"}', '{"name":"beta","task":"do B"}', '{"name":"gamma","task":"do C"}'] |
| 57 | .slice(0, BREAK === 'two' ? 2 : 3); |
| 58 | await s.page.fill('#chat-input', '@tools ' + spawns.map(a => 'spawn_agent ' + a).join(' ;; ')); |
| 59 | await s.page.keyboard.press('Enter'); |
| 60 | await s.page.waitForTimeout(1500); |
| 61 | |
| 62 | // Open the Agents dock panel. |
| 63 | await s.page.evaluate(() => window.DaimondPanels.show('agents')); |
| 64 | await s.page.waitForTimeout(500); |
| 65 | |
| 66 | // Wait for the three workers to finish (each runs a mock turn). |
| 67 | await s.page.waitForTimeout(6000); |
| 68 | await shot(s, 'agents-1-dispatched'); |
| 69 | |
| 70 | const before = await cards(s.page); |
| 71 | const clearVisible = await s.page.evaluate(() => { |
| 72 | const b = document.getElementById('agents-clear'); |
| 73 | return !!(b && b.style.display !== 'none'); |
| 74 | }); |
| 75 | console.log('after dispatch:', JSON.stringify(before)); |
| 76 | |
| 77 | check('THREE WORKERS WERE DISPATCHED FROM ONE TURN', before.length === 3, |
| 78 | `${before.length} cards: ${before.map(c => c.name).join(', ') || '(none)'}`); |
| 79 | // Printed and never judged before: a panel of three workers stuck at "running" |
| 80 | // read exactly like three that had finished. |
| 81 | check('and all three finished', before.length > 0 && before.every(c => /done/i.test(c.status)), |
| 82 | `statuses ${JSON.stringify(before.map(c => c.status))}`); |
| 83 | check('each finished worker is offered a fold', before.length > 0 && before.every(c => c.offer), |
| 84 | `${before.filter(c => c.offer).length}/${before.length} offered`); |
| 85 | check('the Clear control is on the panel', clearVisible, '#agents-clear'); |
| 86 | |
| 87 | // Fold the first foldable worker in. |
| 88 | const foldBtn = s.page.locator('#panel-agents .acard').first().locator('.abtn', { hasText: 'Fold in' }).first(); |
| 89 | const offered = await foldBtn.count(); |
| 90 | check('there is a fold to press', !!offered, `${offered} "Fold in" on the first card`); |
| 91 | let after = before; |
| 92 | if (offered) { |
| 93 | await foldBtn.click(); |
| 94 | await s.page.waitForTimeout(2500); // the propose (reducer) turn |
| 95 | // Accept the proposed fold diff in the Centre. |
| 96 | const accept = await s.page.$('.diff-accept'); |
| 97 | if (accept) { await accept.click(); await s.page.waitForTimeout(2000); } |
| 98 | after = await cards(s.page); |
| 99 | } |
| 100 | console.log('after folding:', JSON.stringify(after)); |
| 101 | await shot(s, 'agents-2-folded'); |
| 102 | |
| 103 | const folded = after.filter(c => c.folded); |
| 104 | check('THE WORKER THAT WAS FOLDED IN IS MARKED AS FOLDED', folded.length === 1, |
| 105 | `${folded.length} cards carry .afolded`); |
| 106 | // "Once, not repeatedly": the card that folded is no longer offered the fold, |
| 107 | // and nobody else lost theirs. |
| 108 | check('AND IS NOT OFFERED THE FOLD A SECOND TIME', |
| 109 | folded.length === 1 && !folded[0].offer, folded.length ? `offer=${folded[0].offer}` : 'nothing folded'); |
| 110 | check('while every other worker keeps its own offer', |
| 111 | after.filter(c => c.offer).length === before.filter(c => c.offer).length - 1, |
| 112 | `${after.filter(c => c.offer).length} offers left of ${before.filter(c => c.offer).length}`); |
| 113 | |
| 114 | // Clear finished. |
| 115 | const afterClear = await s.page.evaluate(async () => { |
| 116 | const b = document.getElementById('agents-clear'); |
| 117 | if (b) b.click(); |
| 118 | await new Promise(r => setTimeout(r, 400)); |
| 119 | return document.querySelectorAll('#panel-agents .acard').length; |
| 120 | }); |
| 121 | check('CLEARING FINISHED WORKERS EMPTIES THE PANEL', afterClear === 0, `${afterClear} cards left`); |
| 122 | |
| 123 | const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e)); |
| 124 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 125 | |
| 126 | await s.close(); |
| 127 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 128 | if (bad.length) { bad.forEach(x => console.log(' FAILED: ' + x)); process.exit(1); } |