mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test(e2e): stabilize chat release-lane tail (#4527)
This commit is contained in:
@@ -2099,6 +2099,7 @@ const Conversations = ({
|
||||
}>
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
data-testid="chat-messages-scroll"
|
||||
// Full-width scroll (scrollbar hugs the window edge); inner content is
|
||||
// centered and width-capped per branch below. `min-h-0` lets this
|
||||
// basis-0 flex child shrink to 0 so the composer footer can take the
|
||||
|
||||
@@ -64,34 +64,27 @@ const STREAM_SCRIPT = [
|
||||
{ finish: 'stop' },
|
||||
];
|
||||
|
||||
// The message column is the `<div ref={messagesContainerRef}
|
||||
// className="flex-1 min-h-0 overflow-y-auto ...">` in Conversations.tsx.
|
||||
// There's only one in /chat so the structural class predicate is enough.
|
||||
async function scrollMetrics(): Promise<{
|
||||
scrollTop: number;
|
||||
scrollHeight: number;
|
||||
clientHeight: number;
|
||||
found: boolean;
|
||||
}> {
|
||||
return (await browser.execute(() => {
|
||||
// The messages scroll container is `flex-1 min-h-0 overflow-y-auto`
|
||||
// (Conversations.tsx). The former hardcoded `bg-[#f6f6f6]` class was
|
||||
// replaced by a surface token, so match on the structural classes instead.
|
||||
const el = document.querySelector('div.flex-1.min-h-0.overflow-y-auto') as HTMLElement | null;
|
||||
if (!el) return { scrollTop: 0, scrollHeight: 0, clientHeight: 0 };
|
||||
const el = document.querySelector('[data-testid="chat-messages-scroll"]') as HTMLElement | null;
|
||||
if (!el) return { scrollTop: 0, scrollHeight: 0, clientHeight: 0, found: false };
|
||||
return {
|
||||
scrollTop: el.scrollTop,
|
||||
scrollHeight: el.scrollHeight,
|
||||
clientHeight: el.clientHeight,
|
||||
found: true,
|
||||
};
|
||||
})) as { scrollTop: number; scrollHeight: number; clientHeight: number };
|
||||
})) as { scrollTop: number; scrollHeight: number; clientHeight: number; found: boolean };
|
||||
}
|
||||
|
||||
async function scrollMessageColumn(top: number): Promise<void> {
|
||||
await browser.execute((y: number) => {
|
||||
// The messages scroll container is `flex-1 min-h-0 overflow-y-auto`
|
||||
// (Conversations.tsx). The former hardcoded `bg-[#f6f6f6]` class was
|
||||
// replaced by a surface token, so match on the structural classes instead.
|
||||
const el = document.querySelector('div.flex-1.min-h-0.overflow-y-auto') as HTMLElement | null;
|
||||
const el = document.querySelector('[data-testid="chat-messages-scroll"]') as HTMLElement | null;
|
||||
if (el) el.scrollTo({ top: y, behavior: 'auto' });
|
||||
}, top);
|
||||
}
|
||||
@@ -181,7 +174,18 @@ describe('Chat harness — scroll + markdown render', () => {
|
||||
|
||||
// ── 3. Auto-scroll anchored to the bottom after the stream ─────
|
||||
// (within 40 px to absorb sub-pixel layout drift)
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
const metrics = await scrollMetrics();
|
||||
if (!metrics.found) return false;
|
||||
return metrics.scrollHeight > metrics.clientHeight;
|
||||
},
|
||||
{ timeout: 10_000, timeoutMsg: 'chat messages scroll container never overflowed' }
|
||||
);
|
||||
const atBottom = await scrollMetrics();
|
||||
console.log(
|
||||
`[chat-harness-scroll-render] bottom metrics: scrollTop=${atBottom.scrollTop}, scrollHeight=${atBottom.scrollHeight}, clientHeight=${atBottom.clientHeight}`
|
||||
);
|
||||
expect(atBottom.scrollHeight).toBeGreaterThan(atBottom.clientHeight);
|
||||
expect(atBottom.scrollHeight - (atBottom.scrollTop + atBottom.clientHeight)).toBeLessThan(40);
|
||||
|
||||
@@ -190,6 +194,9 @@ describe('Chat harness — scroll + markdown render', () => {
|
||||
await scrollMessageColumn(targetTop);
|
||||
await browser.pause(500); // let the stick hook react
|
||||
const afterScrollUp = await scrollMetrics();
|
||||
console.log(
|
||||
`[chat-harness-scroll-render] after manual scroll: scrollTop=${afterScrollUp.scrollTop}, scrollHeight=${afterScrollUp.scrollHeight}, clientHeight=${afterScrollUp.clientHeight}, targetTop=${targetTop}`
|
||||
);
|
||||
expect(Math.abs(afterScrollUp.scrollTop - targetTop)).toBeLessThan(40);
|
||||
expect(
|
||||
afterScrollUp.scrollHeight - (afterScrollUp.scrollTop + afterScrollUp.clientHeight)
|
||||
|
||||
@@ -104,6 +104,16 @@ async function snapshotRuntime(threadId: string): Promise<RuntimeSnapshot> {
|
||||
}, threadId)) as RuntimeSnapshot;
|
||||
}
|
||||
|
||||
async function hasRenderedSubagentTimeline(): Promise<boolean> {
|
||||
return (await browser.execute(() => {
|
||||
const rows = Array.from(document.querySelectorAll('[data-testid="agent-timeline-row"]'));
|
||||
return rows.some(row => {
|
||||
const text = row.textContent ?? '';
|
||||
return /Research|Researching|subagent/i.test(text);
|
||||
});
|
||||
})) as boolean;
|
||||
}
|
||||
|
||||
describe('Chat harness — orchestrator → subagent flow', () => {
|
||||
before(async function beforeSuite() {
|
||||
this.timeout(90_000);
|
||||
@@ -114,6 +124,13 @@ describe('Chat harness — orchestrator → subagent flow', () => {
|
||||
// clean signed-in state rather than a polluted one (the source of the
|
||||
// intermittent "final canary never arrives" failures).
|
||||
await resetApp(USER_ID, { clearAuthSession: true });
|
||||
const superContext = await callOpenhumanRpc('openhuman.config_set_super_context_enabled', {
|
||||
value: false,
|
||||
});
|
||||
expect(superContext.ok).toBe(true);
|
||||
console.log(
|
||||
'[chat-harness-subagent] Disabled super context for deterministic scripted LLM calls'
|
||||
);
|
||||
|
||||
setMockBehavior('llmForcedResponses', JSON.stringify(FORCED_RESPONSES));
|
||||
// Faster streaming for non-tool-call responses so this spec doesn't
|
||||
@@ -170,10 +187,15 @@ describe('Chat harness — orchestrator → subagent flow', () => {
|
||||
sawSubagentTimeline = true;
|
||||
}
|
||||
if (sawSubagentPhase && sawSubagentTimeline) break;
|
||||
if (await textExists(CANARY_FINAL)) break;
|
||||
if (await textExists(CANARY_FINAL)) {
|
||||
sawSubagentTimeline = sawSubagentTimeline || (await hasRenderedSubagentTimeline());
|
||||
break;
|
||||
}
|
||||
await browser.pause(200);
|
||||
}
|
||||
|
||||
sawSubagentTimeline = sawSubagentTimeline || (await hasRenderedSubagentTimeline());
|
||||
|
||||
// At least ONE of the two signals must have fired — the timeline
|
||||
// entry is the more durable check (the live phase can flip back to
|
||||
// 'thinking' or 'idle' before our 200ms poll catches it).
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
waitForSocketConnected,
|
||||
} from '../helpers/chat-harness';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { clickText, clickToggle, textExists } from '../helpers/element-helpers';
|
||||
import { clickText, textExists } from '../helpers/element-helpers';
|
||||
import { resetApp } from '../helpers/reset-app';
|
||||
import { navigateViaHash } from '../helpers/shared-flows';
|
||||
import {
|
||||
@@ -53,7 +53,7 @@ const FORCED_RESPONSES = [
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'call_delegate_do_crypto_1',
|
||||
name: 'delegate_do_crypto',
|
||||
name: 'do_crypto',
|
||||
arguments: JSON.stringify({
|
||||
prompt: `Prepare a $5 EVM transfer to John at ${JOHN_ADDRESS}.`,
|
||||
}),
|
||||
@@ -87,14 +87,17 @@ const FORCED_RESPONSES = [
|
||||
];
|
||||
|
||||
async function clickRecoveryConsentCheckbox(): Promise<void> {
|
||||
const checkbox = await browser.$('input[type="checkbox"]');
|
||||
const checkbox = await browser.$('#mnemonic-confirm-checkbox');
|
||||
if (!(await checkbox.isExisting())) {
|
||||
throw new Error('Recovery phrase consent checkbox not found');
|
||||
}
|
||||
if (!(await checkbox.isSelected())) {
|
||||
await clickToggle();
|
||||
await checkbox.click();
|
||||
}
|
||||
await expect(checkbox).toBeSelected();
|
||||
await browser.waitUntil(async () => await checkbox.isSelected(), {
|
||||
timeout: 5_000,
|
||||
timeoutMsg: 'Recovery phrase consent checkbox did not become selected',
|
||||
});
|
||||
}
|
||||
|
||||
describe('Chat harness — wallet flow', () => {
|
||||
@@ -106,6 +109,13 @@ describe('Chat harness — wallet flow', () => {
|
||||
// so the crypto sub-agent run starts from a clean signed-in state (a
|
||||
// polluted session was the source of the intermittent quote-store failures).
|
||||
await resetApp(USER_ID, { clearAuthSession: true });
|
||||
const superContext = await callOpenhumanRpc('openhuman.config_set_super_context_enabled', {
|
||||
value: false,
|
||||
});
|
||||
expect(superContext.ok).toBe(true);
|
||||
console.log(
|
||||
'[chat-harness-wallet-flow] Disabled super context for deterministic scripted LLM calls'
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
||||
@@ -61,6 +61,11 @@ describe('User journey — full research task', () => {
|
||||
await startMockServer();
|
||||
await waitForApp();
|
||||
await resetApp(USER_ID);
|
||||
const superContext = await callOpenhumanRpc('openhuman.config_set_super_context_enabled', {
|
||||
value: false,
|
||||
});
|
||||
expect(superContext.ok).toBe(true);
|
||||
console.log(`${LOG_PREFIX} Disabled super context for deterministic scripted LLM calls`);
|
||||
|
||||
setMockBehavior('llmForcedResponses', JSON.stringify(FORCED_RESPONSES));
|
||||
setMockBehavior('llmStreamChunkDelayMs', '10');
|
||||
@@ -111,7 +116,8 @@ describe('User journey — full research task', () => {
|
||||
console.log(`${LOG_PREFIX} J1.1: passed — user message visible`);
|
||||
});
|
||||
|
||||
it('J1.2 — tool call timeline appears during execution', async () => {
|
||||
it('J1.2 — tool call timeline appears during execution', async function () {
|
||||
this.timeout(60_000);
|
||||
console.log(`${LOG_PREFIX} J1.2: watching for tool timeline entry`);
|
||||
let sawToolTimeline = false;
|
||||
const deadline = Date.now() + 45_000;
|
||||
@@ -142,7 +148,8 @@ describe('User journey — full research task', () => {
|
||||
console.log(`${LOG_PREFIX} J1.2: passed`);
|
||||
});
|
||||
|
||||
it('J1.3 — final answer with canary text renders', async () => {
|
||||
it('J1.3 — final answer with canary text renders', async function () {
|
||||
this.timeout(60_000);
|
||||
console.log(`${LOG_PREFIX} J1.3: waiting for canary`);
|
||||
await browser.waitUntil(async () => await textExists(CANARY_FINAL), {
|
||||
timeout: 45_000,
|
||||
@@ -151,7 +158,8 @@ describe('User journey — full research task', () => {
|
||||
console.log(`${LOG_PREFIX} J1.3: passed — canary visible`);
|
||||
});
|
||||
|
||||
it('J1.4 — after navigate away + back, thread messages still visible', async () => {
|
||||
it('J1.4 — after navigate away + back, thread messages still visible', async function () {
|
||||
this.timeout(60_000);
|
||||
console.log(`${LOG_PREFIX} J1.4: navigating away to /home`);
|
||||
|
||||
// Ensure the IN_FLIGHT map cleared (turn is fully done) before navigating.
|
||||
|
||||
Reference in New Issue
Block a user