Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_multichat.mjs

6.9 KiB, 1 run

created by r2519314175:543, 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// You must be able to work in a second chat while the first is still running.
2//
3// THE HEADER USED TO SAY "and Stop must hit the chat you are looking at -- not
4// whichever started last". Nothing below has ever pressed Stop, so the sentence
5// has gone: a claim in a comment is not a check, and this one had been read as
6// one for as long as the file has existed.
7//
8// WHAT THE SECOND CHAT'S REPLY IS LOOKED FOR IN. `bSent` used to be
9// `/Reply in B/.test(#chat-output innerText)`, and the phrase it looked for is
10// the one this file TYPES: the transcript renders the user's own message, so the
11// check passed on a turn that never got an answer at all -- the same defect
12// verify_backup had. The reply is now a nonce the mock is asked to say back, and
13// it is looked for in `.chat-msg-assistant` alone.
14//
15// PROVED AGAINST TWO BREAKS FIRST:
16// --break noreply B's message is sent with `@err 500`, so the user's line is
17// rendered and no assistant message ever arrives. The old
18// check went green on exactly that; this one must go red.
19// --break shorta A's turn is short enough to be over before B's finishes,
20// which is the state every run of this file could have been
21// in without anybody knowing.
22//
23// node dev/verify_multichat.mjs --break noreply # expected to FAIL
24// node dev/verify_multichat.mjs --break shorta # expected to FAIL
25// node dev/verify_multichat.mjs # and then, clean
26import { open, newChat, shot, errors } from './harness.mjs';
27
28const ok = [], bad = [];
29const check = (name, pass, detail) => {
30 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
31 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
32};
33
34const BREAK = (() => {
35 const i = process.argv.indexOf('--break');
36 return i > 0 ? String(process.argv[i + 1] || '') : '';
37})();
38if (BREAK && !['noreply', 'shorta'].includes(BREAK)) {
39 console.error(`unknown break '${BREAK}'; known: noreply, shorta`);
40 process.exit(2);
41}
42
43const s = await open({ name: 'multichat' });
44if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`);
45
46// Chat A: start a LONG turn (streams slowly), do not wait for it.
47await newChat(s);
48// `shorta` gives A a turn that is over before B's is: still running when the
49// first check looks (2.4s of chunks), finished long before the last one does.
50await s.page.fill('#chat-input', BREAK === 'shorta' ? '@long 20' : '@long 200');
51await s.page.click('#chat-send');
52await s.page.waitForTimeout(800);
53
54const aRunning = await s.page.evaluate(() => {
55 const b = document.getElementById('chat-send');
56 return { stopMode: /stop/i.test((b.getAttribute('title') || '') + (b.className || '')), disabled: document.getElementById('chat-input').disabled };
57});
58// The composer is NOT disabled mid-turn any more: a turn cannot be interrupted, but the next
59// thing to say can be typed while the answer arrives and is sent when the turn ends. Stop-mode on
60// the send button is the signal that a turn is running; an unusable box never was.
61check('A LONG TURN IN CHAT A IS RUNNING — stop-mode on the send button, composer still live',
62 aRunning.stopMode && !aRunning.disabled, JSON.stringify(aRunning));
63
64// Start a SECOND chat while A still streams.
65await s.page.click('#new-session-btn');
66await s.page.waitForTimeout(400);
67// Start the pending tile. `.tile-start` by class, NOT has-text("Start") —
68// the substring match also hits the BYOK form's hidden "Save & start".
69const startBtn = s.page.locator('.tile-start').first();
70const started = await startBtn.count();
71// This used to be a bare `if`: with no Start button the file went on to measure
72// chat A a second time and printed three greens about it.
73check('a second chat can be started while the first one runs', !!started, `${started} .tile-start on the rail`);
74if (started) await startBtn.click();
75await s.page.waitForTimeout(600);
76
77const bComposer = await s.page.evaluate(() => {
78 const inp = document.getElementById('chat-input');
79 const b = document.getElementById('chat-send');
80 return { inputDisabled: inp.disabled, stopMode: /stop/i.test((b.getAttribute('title') || '') + (b.className || '')) };
81});
82check('CHAT B IS USABLE WHILE A RUNS — input enabled, and its send button is in send-mode not stop-mode',
83 !bComposer.inputDisabled && !bComposer.stopMode, JSON.stringify(bComposer));
84
85// Type and send in B while A still runs. The nonce is what the mock is asked to
86// say back, so finding it in an ASSISTANT bubble is the answer arriving.
87const NONCE = 'replyB-' + Math.random().toString(36).slice(2, 8);
88let b = { user: false, asst: false, text: '' };
89if (!bComposer.inputDisabled) {
90 await s.page.fill('#chat-input', BREAK === 'noreply' ? `@err 500 ${NONCE}` : `@text ${NONCE}`);
91 await s.page.click('#chat-send');
92 await s.page.waitForTimeout(4000);
93 b = await s.page.evaluate((nonce) => {
94 const txt = (sel) => [...document.querySelectorAll(sel)].map(e => e.textContent).join('\n');
95 const asst = txt('#chat-output .chat-msg-assistant .chat-msg-content');
96 return {
97 user: txt('#chat-output .chat-msg-user .chat-msg-content').includes(nonce),
98 asst: asst.includes(nonce),
99 text: asst.slice(-120),
100 };
101 }, NONCE);
102}
103await shot(s, 'multichat-B-active');
104
105check('B\'s message was accepted into B\'s transcript', b.user, `looking for ${NONCE} in .chat-msg-user`);
106check('B\'S TURN ANSWERED WHILE A RAN — the answer is in an assistant bubble, not merely the line we typed',
107 b.asst, `looking for ${NONCE} in .chat-msg-assistant, which holds ${JSON.stringify(b.text)}`);
108
109// "WHILE A RAN" WAS NEVER CHECKED. A's turn was seen to start and was never
110// looked at again, so a run in which it had finished before B was even opened
111// read exactly the same. It streams 200 chunks at 120ms each in the mock (24s of
112// server-side sleep, whatever this machine's speed), and everything above takes
113// about eight -- so it must still be going, and the composer says so when the
114// tile is selected again.
115const idle = s.page.locator('#session-list .session-box:not(.active)');
116const others = await idle.count();
117check('chat A is still on the rail, unselected', others === 1, `${others} unselected chat tiles`);
118let back = { stopMode: null };
119if (others === 1) {
120 await idle.first().click();
121 await s.page.waitForTimeout(700);
122 back = await s.page.evaluate(() => {
123 const b = document.getElementById('chat-send');
124 return { stopMode: /stop/i.test((b.getAttribute('title') || '') + (b.className || '')) };
125 });
126}
127check('AND A WAS STILL RUNNING THROUGHOUT — its own composer is still in stop-mode',
128 back.stopMode === true, `stop-mode=${back.stopMode}`);
129
130const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
131check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
132
133await s.close();
134console.log(`\n${ok.length} passed, ${bad.length} failed`);
135if (bad.length) { bad.forEach(x => console.log(' FAILED: ' + x)); process.exit(1); }