candidates and board ui agent issue
Some checks failed
CI / check (push) Failing after 4m58s

This commit is contained in:
2026-09-05 10:46:06 +05:30
parent e02a0c23d4
commit 6249e00a3a
78 changed files with 15071 additions and 1998 deletions

256
scripts/browser-flows.js Normal file
View File

@@ -0,0 +1,256 @@
/**
* The Owliver UI-editing journey, as a script that runs *inside a real page*.
*
* Unit and SSR tests render the same modules this file drives, and they were
* not enough: every defect this suite now guards was found in a browser, not in
* a test run. So the acceptance test is the browser, and this is what it runs.
*
* It is deliberately dependency-free and framework-free — one function, no
* imports, no build step — because it has to be evaluatable in any authenticated
* session: through the puppeteer driver beside it, or pasted into a console.
*
* A full journey spans reloads, and nothing in a page survives one. So the
* journey is split into **phases** and the *driver* owns the reloads:
*
* phase 1 inspect → hide → preview → discard → hide → Apply
* ── reload ──
* phase 2 verify hidden → hidden node still listed → unhide → Apply
* ── reload ──
* phase 3 verify restored
*
* Every assertion is about what the browser actually shows — the composed
* nodes in the document and the bytes in `preferences.uiLayouts` — never about
* a module's return value.
*/
/* global window, document, fetch, HTMLTextAreaElement, Event */
(function attach() {
if (typeof window === 'undefined') return;
/** The nodes the page is actually drawing, in document order. */
const nodes = () => [...document.querySelectorAll('[data-ui-node]')]
.map((el) => el.getAttribute('data-ui-node'));
/** What the panel is showing. The transcript, not a component's state. */
const panel = () => document.querySelector('[class*="assistant"], aside')?.innerText || '';
/** The saved layouts, read back over the wire like any other client would. */
const saved = async () => {
const response = await fetch('/api/v1/me/preferences', { credentials: 'include' });
const body = await response.json();
const prefs = (body.data || body).preferences || (body.data || body);
return { uiLayouts: prefs.uiLayouts || {}, disabledSkills: prefs.disabledSkills || [] };
};
const sleep = (ms) => new Promise((resolve) => { window.setTimeout(resolve, ms); });
/**
* Wait until the panel is genuinely idle.
*
* Not cosmetic: a message submitted while the panel is streaming is dropped,
* and a suite that does not wait reports a phantom failure for a request that
* was never sent. That is exactly what happened during the manual audit.
*/
const idle = async () => {
for (let attempt = 0; attempt < 60; attempt += 1) {
const box = document.querySelector('textarea');
if (box && !box.disabled && !/Thinking/.test(document.body.innerText)) return true;
await sleep(500);
}
return false;
};
/**
* Ask Owliver, the way a person does.
*
* Through the composer and the form's own submit — not by calling a handler —
* so the routing, the gate and the panel are all really exercised.
*/
const ask = async (question) => {
await idle();
const box = document.querySelector('textarea');
const setValue = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
setValue.call(box, question);
box.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(150);
box.closest('form').requestSubmit();
await sleep(1500);
await idle();
await sleep(600);
return panel();
};
/**
* Every request that left the page, so "did this reach the model?" is
* answered by the network rather than by reading the reply and guessing.
*/
const spy = () => {
if (window.__uiFlowSpy) { window.__uiFlowCalls = []; return; }
window.__uiFlowCalls = [];
const original = window.fetch;
window.fetch = function spied(...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
const method = (args[1]?.method || 'GET').toUpperCase();
if (/\/api\/v1\/(agents|runs)/.test(url) || method !== 'GET') {
window.__uiFlowCalls.push(`${method} ${url}`);
}
return original.apply(this, args);
};
window.__uiFlowSpy = true;
};
const modelCalls = () => (window.__uiFlowCalls || []).filter((c) => /\/agents\//.test(c));
const writeCalls = () => (window.__uiFlowCalls || []).filter((c) => /^PATCH/.test(c));
/** One assertion. `detail` is what a reader needs to debug a failure. */
const check = (results, name, pass, detail) => {
results.push({ name, pass: Boolean(pass), detail: String(detail ?? '') });
return Boolean(pass);
};
/**
* Phase 1 — the whole preview contract, before anything is kept.
*
* `target` is a node id; `phrase` is how a person would name it. Both are
* given by the caller so this file names no page and no section.
*/
async function phase1({ page, target, phrase }) {
const results = [];
spy();
const inventory = await ask('What is on this page?');
check(results, `${page}: inspect answers locally`, modelCalls().length === 0,
modelCalls().join(', ') || 'no agent run');
check(results, `${page}: inspect lists ${target}`, inventory.includes(`(${target})`),
inventory.slice(-300));
const before = nodes();
check(results, `${page}: ${target} is on the page`, before.includes(target), before.join(', '));
/* What was stored before anything was previewed. Compared against rather
than assumed empty: a page that has been customised before still has a
patch, and the claim under test is that a *preview* does not change it. */
const storedBefore = JSON.stringify((await saved()).uiLayouts[page] ?? null);
window.__uiFlowCalls = [];
await ask(`Hide ${phrase}`);
const hidden = nodes();
check(results, `${page}: hide removes it from the page`, !hidden.includes(target), hidden.join(', '));
check(results, `${page}: hide does not reach the model`, modelCalls().length === 0,
modelCalls().join(', ') || 'no agent run');
const storedDuring = JSON.stringify((await saved()).uiLayouts[page] ?? null);
check(results, `${page}: preview writes nothing`,
storedDuring === storedBefore && writeCalls().length === 0,
`before ${storedBefore} · during ${storedDuring}`);
await ask('Discard the layout change');
check(results, `${page}: discard restores it`, nodes().includes(target), nodes().join(', '));
await ask(`Hide ${phrase}`);
check(results, `${page}: hide again`, !nodes().includes(target), nodes().join(', '));
window.__uiFlowCalls = [];
await ask('Apply the layout change');
await sleep(1200);
const persisted = await saved();
const patch = persisted.uiLayouts[page];
check(results, `${page}: apply writes preferences`, writeCalls().length > 0, writeCalls().join(', '));
check(results, `${page}: apply stores an operation, not a tree`,
Boolean(patch) && patch.ops.some((op) => op.op === 'hide' && op.target === target && op.hidden === true),
JSON.stringify(patch ?? null));
check(results, `${page}: apply does not reach the model`, modelCalls().length === 0,
modelCalls().join(', ') || 'no agent run');
return results;
}
/** Phase 2 — after a cold reload: still hidden, still addressable, put back. */
async function phase2({ page, target, phrase }) {
const results = [];
spy();
check(results, `${page}: reload reconstructs the hidden state`, !nodes().includes(target), nodes().join(', '));
const inventory = await ask('What is on this page?');
check(results, `${page}: a hidden node stays in the inventory`, inventory.includes(`(${target})`),
inventory.slice(-300));
window.__uiFlowCalls = [];
await ask(`Show ${phrase}`);
check(results, `${page}: a hidden node is addressable after a reload`, nodes().includes(target),
nodes().join(', '));
check(results, `${page}: unhide does not reach the model`, modelCalls().length === 0,
modelCalls().join(', ') || 'no agent run');
await ask('Apply the layout change');
await sleep(1200);
const patch = (await saved()).uiLayouts[page];
check(results, `${page}: the unhide is persisted`,
Boolean(patch) && patch.ops.some((op) => op.op === 'hide' && op.target === target && op.hidden === false),
JSON.stringify(patch ?? null));
return results;
}
/** Phase 3 — after a second cold reload, the page is itself again. */
async function phase3({ page, target }) {
const results = [];
check(results, `${page}: reload reconstructs the restored state`, nodes().includes(target), nodes().join(', '));
return results;
}
/**
* The regression this suite exists for.
*
* Applying or discarding with nothing previewed used to fall through to the
* model, which answered — reasonably, for an agent scoped to open roles —
* that layout changes were not in its scope. A request about the interface
* must never be answered by something that does not know the interface
* exists. The second half is as important: ordinary "apply" must still be an
* ordinary word.
*/
async function nothingPreviewed({ page, ordinaryQuestion }) {
const results = [];
spy();
for (const verb of ['Discard', 'Apply']) {
window.__uiFlowCalls = [];
const reply = await ask(`${verb} the layout change`);
check(results, `${page}: "${verb} the layout change" with nothing previewed stays local`,
modelCalls().length === 0, modelCalls().join(', ') || 'no agent run');
check(results, `${page}: it says there is nothing to ${verb.toLowerCase()}`,
new RegExp(`nothing to ${verb.toLowerCase()}`, 'i').test(reply), reply.slice(-240));
}
if (ordinaryQuestion) {
window.__uiFlowCalls = [];
const reply = await ask(ordinaryQuestion);
check(results, `${page}: an ordinary "apply" is not swallowed`,
modelCalls().length > 0 && !/nothing to apply/i.test(reply.slice(-260)),
modelCalls().join(', ') || 'no agent run');
}
return results;
}
/** A data question must stay a data question, however it is worded. */
async function dataStaysData({ page, questions }) {
const results = [];
spy();
for (const question of questions) {
window.__uiFlowCalls = [];
const before = nodes();
await ask(question);
check(results, `${page}: "${question}" is answered as data`,
modelCalls().length > 0
&& !/Previewing/.test(document.body.innerText)
&& JSON.stringify(before) === JSON.stringify(nodes()),
`${modelCalls().join(', ') || 'no agent run'} · nodes ${nodes().join(', ')}`);
}
return results;
}
window.__uiFlows = { phase1, phase2, phase3, nothingPreviewed, dataStaysData, nodes, saved, ask, panel };
}());