Oregami
Repositories/oxedyne/daimond

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.
7import { open, connectMock, mockLog, clearMockLog, contentText, shot } from './harness.mjs';
8
9const 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
21let bad = [];
22const ok = (c, why, d) => { console.log((c ? ' ok ' : ' FAIL ') + why + (d != null ? ' — ' + d : '')); if (!c) bad.push(why); };
23
24const s = await open({ name: 'askdaimon', defaults: false });
25const { page: p } = s;
26const errs = [];
27p.on('pageerror', e => errs.push('PAGEERROR ' + String(e).slice(0, 200)));
28await p.waitForTimeout(1500);
29await connectMock(s);
30
31// A Diamond, opened on its chat face, which is where the owner types.
32await p.evaluate(() => document.getElementById('new-diamond-btn').click());
33await p.waitForSelector('.dlg-card', { timeout: 8000 });
34await 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});
41await p.waitForTimeout(1200);
42await p.click('#dview-chat');
43await p.waitForTimeout(700);
44const 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 ═══════
51clearMockLog();
52await say('/decisions');
53const sent = mockLog().flatMap(r => (r.messages || []).map(m => contentText(m.content))).join('\n');
54ok(/decisions/.test(sent) && /ONE AT A TIME/.test(sent),
55 'the skill reached the daimon', sent.length + ' chars on the wire');
56ok(/`ask` tool/.test(sent),
57 'and it tells the daimon to use the tool rather than write the question in prose');
58for (const f of ['question', 'options', 'recommend', 'why', 'if_silent']) {
59 ok(new RegExp('`' + f + '`').test(sent), `it names the field \`${f}\``);
60}
61ok(/`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.
65const schemas = mockLog().map(r => JSON.stringify(r.tools || [])).join('');
66ok(/"ask"/.test(schemas), 'and the daimon was actually handed the tool');
67
68// ══ 2. The daimon asks, he taps, and it carries on ═════════════════
69clearMockLog();
70await say('@tool ask ' + JSON.stringify(Q), 6000);
71const 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});
81ok(!!card, 'the question is drawn in the DAIMON\'s thread');
82ok(!!card && /parcel be signed/.test(card.q), 'and it is this question', card && card.q);
83ok(!!card && card.opts.length === 2 && card.opts[0].rec,
84 'with its options as buttons and the recommendation marked');
85ok(!!card && !card.modal, 'and it is not a permission dialog');
86ok(mockLog().length === 1, 'the daimon\'s turn ended on the question', mockLog().length + ' request(s)');
87await shot(s, 'askdaimon-1-question');
88
89clearMockLog();
90await p.evaluate(() => document.querySelector('#chat-output .ask-card .ask-opt.recommended').click());
91await p.waitForTimeout(5000);
92const answered = mockLog().flatMap(r => (r.messages || [])
93 .filter(m => m.role === 'user').map(m => contentText(m.content)))
94 .filter(t => /^Chose: /.test(t));
95ok(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.
99const carried = mockLog().length >= 1 && /parcel be signed/.test(
100 mockLog().map(r => JSON.stringify(r.messages || [])).join(''));
101ok(carried, 'and it carried on with the question it had asked still in its context');
102const 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});
108ok(thread.answered && /On the device/.test(thread.done),
109 'and the card says what he answered', thread.done);
110await shot(s, 'askdaimon-2-answered');
111
112ok(errs.length === 0, 'nothing was thrown', errs.join(' | '));
113console.log(bad.length ? `\nprobe_askdaimon: ${bad.length} failed.` : '\nprobe_askdaimon: all checks passed');
114await s.close();
115process.exit(bad.length ? 1 : 0);