oxedyne/daimond/dev/probe_askdaimon.mjs
5.7 KiB, 1 run
created by r2519314175:35, 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 | // probe_askdaimon.mjs — the errand as the owner runs it: `/decisions` at a daimon, |
| 2 | // a question with options, one tap, and the daimon carries on with the answer. |
| 3 | // |
| 4 | // Two legs, split at the one seam a mock cannot cross. A mock cannot DECIDE to call a |
| 5 | // tool, so leg 1 asks what `/decisions` actually put in front of the model, and leg 2 |
| 6 | // makes the model call `ask` and follows the card, the tap and what came back. |
| 7 | import { open, connectMock, mockLog, clearMockLog, contentText, shot } from './harness.mjs'; |
| 8 | |
| 9 | const Q = { |
| 10 | question: 'Where should the parcel be signed?', |
| 11 | options: [ |
| 12 | { label: 'On the device', means: 'The key never leaves this laptop. Losing it loses the ability to sign, and nothing else.' }, |
| 13 | { label: 'In the gateway', means: 'Any device can sign. Oxedyne holds a key that can speak in your name.' }, |
| 14 | ], |
| 15 | recommend: 'On the device', |
| 16 | why: 'You have said the whole point of Daimond is that Oxedyne cannot read or speak for you.', |
| 17 | if_silent: 'I will keep signing on the device and note the choice in the crystal.', |
| 18 | n: 1, of: 2, |
| 19 | }; |
| 20 | |
| 21 | let bad = []; |
| 22 | const ok = (c, why, d) => { console.log((c ? ' ok ' : ' FAIL ') + why + (d != null ? ' — ' + d : '')); if (!c) bad.push(why); }; |
| 23 | |
| 24 | const s = await open({ name: 'askdaimon', defaults: false }); |
| 25 | const { page: p } = s; |
| 26 | const errs = []; |
| 27 | p.on('pageerror', e => errs.push('PAGEERROR ' + String(e).slice(0, 200))); |
| 28 | await p.waitForTimeout(1500); |
| 29 | await connectMock(s); |
| 30 | |
| 31 | // A Diamond, opened on its chat face, which is where the owner types. |
| 32 | await p.evaluate(() => document.getElementById('new-diamond-btn').click()); |
| 33 | await p.waitForSelector('.dlg-card', { timeout: 8000 }); |
| 34 | await p.evaluate(() => { |
| 35 | const card = [...document.querySelectorAll('.dlg-card')].filter(c => c.getClientRects().length).pop(); |
| 36 | const inp = card.querySelector('input.dlg-input'); |
| 37 | inp.value = 'Sync'; |
| 38 | inp.dispatchEvent(new Event('input', { bubbles: true })); |
| 39 | card.querySelector('.dlg-ok').click(); |
| 40 | }); |
| 41 | await p.waitForTimeout(1200); |
| 42 | await p.click('#dview-chat'); |
| 43 | await p.waitForTimeout(700); |
| 44 | const say = async (text, ms = 5000) => { |
| 45 | await p.fill('#chat-input', text); |
| 46 | await p.click('#chat-send'); |
| 47 | await p.waitForTimeout(ms); |
| 48 | }; |
| 49 | |
| 50 | // ══ 1. `/decisions` reaches the daimon, and it names the tool ═══════ |
| 51 | clearMockLog(); |
| 52 | await say('/decisions'); |
| 53 | const sent = mockLog().flatMap(r => (r.messages || []).map(m => contentText(m.content))).join('\n'); |
| 54 | ok(/decisions/.test(sent) && /ONE AT A TIME/.test(sent), |
| 55 | 'the skill reached the daimon', sent.length + ' chars on the wire'); |
| 56 | ok(/`ask` tool/.test(sent), |
| 57 | 'and it tells the daimon to use the tool rather than write the question in prose'); |
| 58 | for (const f of ['question', 'options', 'recommend', 'why', 'if_silent']) { |
| 59 | ok(new RegExp('`' + f + '`').test(sent), `it names the field \`${f}\``); |
| 60 | } |
| 61 | ok(/`Chose:`/.test(sent) && /`Other:`/.test(sent), |
| 62 | 'and says how the answer comes back, including a rejection of every option'); |
| 63 | // And the tool is really in this daimon's belt, not only in its instructions. A skill |
| 64 | // that names a tool the registry does not carry teaches a call that will be refused. |
| 65 | const schemas = mockLog().map(r => JSON.stringify(r.tools || [])).join(''); |
| 66 | ok(/"ask"/.test(schemas), 'and the daimon was actually handed the tool'); |
| 67 | |
| 68 | // ══ 2. The daimon asks, he taps, and it carries on ═════════════════ |
| 69 | clearMockLog(); |
| 70 | await say('@tool ask ' + JSON.stringify(Q), 6000); |
| 71 | const card = await p.evaluate(() => { |
| 72 | const c = document.querySelector('#chat-output .ask-card'); |
| 73 | return c ? { |
| 74 | q: (c.querySelector('.ask-q') || {}).textContent || '', |
| 75 | opts: [...c.querySelectorAll('.ask-opt')].map(b => ({ |
| 76 | label: (b.querySelector('.ask-label') || {}).textContent || '', |
| 77 | rec: b.classList.contains('recommended') })), |
| 78 | modal: !!document.querySelector('.modal.dlg'), |
| 79 | } : null; |
| 80 | }); |
| 81 | ok(!!card, 'the question is drawn in the DAIMON\'s thread'); |
| 82 | ok(!!card && /parcel be signed/.test(card.q), 'and it is this question', card && card.q); |
| 83 | ok(!!card && card.opts.length === 2 && card.opts[0].rec, |
| 84 | 'with its options as buttons and the recommendation marked'); |
| 85 | ok(!!card && !card.modal, 'and it is not a permission dialog'); |
| 86 | ok(mockLog().length === 1, 'the daimon\'s turn ended on the question', mockLog().length + ' request(s)'); |
| 87 | await shot(s, 'askdaimon-1-question'); |
| 88 | |
| 89 | clearMockLog(); |
| 90 | await p.evaluate(() => document.querySelector('#chat-output .ask-card .ask-opt.recommended').click()); |
| 91 | await p.waitForTimeout(5000); |
| 92 | const answered = mockLog().flatMap(r => (r.messages || []) |
| 93 | .filter(m => m.role === 'user').map(m => contentText(m.content))) |
| 94 | .filter(t => /^Chose: /.test(t)); |
| 95 | ok(answered.length === 1 && answered[0] === 'Chose: On the device', |
| 96 | 'one tap steered the daimon with his answer', JSON.stringify(answered)); |
| 97 | // AND THE DAIMON CARRIED ON: a fresh turn ran on the answer, in the same conversation, |
| 98 | // with the question it asked still in front of it. |
| 99 | const carried = mockLog().length >= 1 && /parcel be signed/.test( |
| 100 | mockLog().map(r => JSON.stringify(r.messages || [])).join('')); |
| 101 | ok(carried, 'and it carried on with the question it had asked still in its context'); |
| 102 | const thread = await p.evaluate(() => { |
| 103 | const c = document.querySelector('#chat-output .ask-card'); |
| 104 | return { answered: !!(c && c.dataset.answered), |
| 105 | done: c ? ((c.querySelector('.ask-done') || {}).textContent || '') : '', |
| 106 | msgs: [...document.querySelectorAll('#chat-output .chat-msg-user')].map(n => n.textContent.trim()).slice(-2) }; |
| 107 | }); |
| 108 | ok(thread.answered && /On the device/.test(thread.done), |
| 109 | 'and the card says what he answered', thread.done); |
| 110 | await shot(s, 'askdaimon-2-answered'); |
| 111 | |
| 112 | ok(errs.length === 0, 'nothing was thrown', errs.join(' | ')); |
| 113 | console.log(bad.length ? `\nprobe_askdaimon: ${bad.length} failed.` : '\nprobe_askdaimon: all checks passed'); |
| 114 | await s.close(); |
| 115 | process.exit(bad.length ? 1 : 0); |