Files
openhuman/app/test/e2e/specs/voice-mode.spec.ts
T
Mega MindandGitHub 04146bf0bf Fix/191 skills reliability upstream (#282)
* feat(skills): core RPC data stats, tool timeout env, ping state merge (#191)

- Add openhuman.skills_data_stats and SkillDataDirectoryStats (disk usage).
- Centralize tool execution timeout via OPENHUMAN_TOOL_TIMEOUT_SECS (tool_timeout).
- Apply timeout to skill event loop, agent tool loop, harness default, delegate.
- Ping scheduler: merge connection_error into published_state via registry.
- JSON-RPC e2e: assert skills_data_stats.

Closes #214
Closes #218
Part of #213 (backend)

Made-with: Cursor

* feat(app): skills sync stats UI, reconnect resync, FE timeouts, chat errors (#191)

- useSkillDataDirectoryStats + Skills.tsx merge disk stats with skill state.
- resyncRunningSkillsAfterReconnect on socket connect; disconnectSkill OAuth cleanup in finally.
- withTimeout for callTool/triggerSync; VITE_TOOL_TIMEOUT_SECS in app .env.example.
- Structured ChatSendError in Conversations (data-chat-send-error-code).

Closes #213
Closes #215
Closes #216
Closes #217
Closes #219

Made-with: Cursor

* test(e2e): onboarding helpers and skills smoke specs (#191)

- shared-flows: 5-step onboarding, completeOnboardingIfVisible.
- auth-access-control + voice-mode use shared helpers.
- skills-registry: named skill assertion; new skill-oauth, multi-round, reconnect, lifecycle specs.

Closes #220
Closes #221
Closes #222
Closes #223
Closes #224
Part of #189 (E2E helpers)

Made-with: Cursor

* style: rustfmt + Prettier for CI (PR #282)

Fix Type Check workflow: prettier --check and cargo fmt --check.

Made-with: Cursor

* fix: PR #282 review — tool timeout config, tool_timeout module, CI diagnostics

- Centralize VITE_TOOL_TIMEOUT_SECS in config.ts (TOOL_TIMEOUT_SECS)
- Move tool_timeout to folder module; merge_published_state broadcasts SKILL_STATE_CHANGED
- Resync guard after socket reconnect; qjs_engine logs data dir stat errors
- E2E: registry/lifecycle/multi-round failure diagnostics; onboarding label constant
- chatSendError arrow export; rustfmt/import grouping in event_loop

Made-with: Cursor

* test(e2e): add Gmail skill end-to-end tests

- Introduced a comprehensive end-to-end test suite for the Gmail skill, covering the full lifecycle from discovery to OAuth completion and email management.
- Implemented two modes: a lifecycle-only mode that validates the skill's event loop without real credentials, and a live mode that interacts with the Gmail API using actual credentials.
- Added detailed environment variable requirements and usage instructions for both testing modes.

Closes #XXX (replace with relevant issue number if applicable)
2026-04-02 15:25:31 -07:00

169 lines
5.5 KiB
TypeScript

// @ts-nocheck
/**
* E2E test: Voice mode integration
*
* Covers:
* - Navigating to conversations page
* - Switching to voice input mode
* - Voice status check fires and displays availability message
* - Voice input/reply mode toggle buttons render
* - Voice recording button renders in voice mode
* - Switching back to text mode restores text input
*
* The mock server runs on http://127.0.0.1:18473
*/
import { waitForApp, waitForAppReady } from '../helpers/app-helpers';
import { triggerAuthDeepLink } from '../helpers/deep-link-helpers';
import {
clickText,
dumpAccessibilityTree,
textExists,
waitForWebView,
waitForWindowVisible,
} from '../helpers/element-helpers';
import { completeOnboardingIfVisible } from '../helpers/shared-flows';
import { clearRequestLog, getRequestLog, startMockServer, stopMockServer } from '../mock-server';
async function waitForRequest(method, urlFragment, timeout = 15_000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const log = getRequestLog();
const match = log.find(r => r.method === method && r.url.includes(urlFragment));
if (match) return match;
await browser.pause(500);
}
return undefined;
}
async function waitForHome(timeout = 20_000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
if (await textExists('Message OpenHuman')) return true;
await browser.pause(700);
}
return false;
}
async function waitForAnyText(candidates, timeout = 20_000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
for (const t of candidates) {
if (await textExists(t)) return t;
}
await browser.pause(600);
}
return null;
}
describe('Voice mode integration', () => {
before(async () => {
await startMockServer();
await waitForApp();
clearRequestLog();
});
after(async () => {
await stopMockServer();
});
it('can switch to voice input mode, see status message, and switch back to text', async () => {
// --- Authenticate and reach conversations ---
await triggerAuthDeepLink('e2e-voice-token');
await waitForWindowVisible(25_000);
await waitForWebView(15_000);
await waitForAppReady(15_000);
const consume = await waitForRequest('POST', '/telegram/login-tokens/');
expect(consume).toBeDefined();
await completeOnboardingIfVisible('[VoiceModeE2E]');
const onHome = await waitForHome(20_000);
if (!onHome) {
const tree = await dumpAccessibilityTree();
console.log('[VoiceModeE2E] Home not reached. Tree:\n', tree.slice(0, 4000));
}
expect(onHome).toBe(true);
// --- Verify we see the text input area (default mode) ---
const hasTextInput = await waitForAnyText(['Message OpenHuman', 'Type a message'], 10_000);
expect(hasTextInput).not.toBeNull();
// --- Verify voice toggle buttons are visible ---
// The Input toggle group should show "Text" and "Voice" buttons
const hasInputLabel = await textExists('Input');
expect(hasInputLabel).toBe(true);
// --- Switch to voice input mode ---
// There are two "Voice" buttons (Input toggle and Reply toggle).
// We click the first one which is the Input mode toggle.
await clickText('Voice', 10_000);
await browser.pause(2_000);
// --- Voice status check should fire ---
// Since whisper-cli is not installed in the E2E environment,
// we expect the unavailability message or the ready message.
const voiceStatusMessage = await waitForAnyText(
[
'Speech-to-text unavailable',
'whisper-cli binary',
'STT model not found',
'Ready',
'Start Talking',
'Could not check voice availability',
],
15_000
);
if (!voiceStatusMessage) {
const tree = await dumpAccessibilityTree();
console.log('[VoiceModeE2E] No voice status message seen. Tree:\n', tree.slice(0, 5000));
}
expect(voiceStatusMessage).not.toBeNull();
// --- Verify the voice recording button or unavailability message is visible ---
const hasVoiceButton = await waitForAnyText(
['Start Talking', 'Transcribing', 'Stop & Send'],
10_000
);
if (!hasVoiceButton) {
const hasStatus = await textExists('Speech-to-text unavailable');
expect(hasStatus).toBe(true);
}
// --- Switch back to text mode ---
// Click the "Text" button in the Input toggle group
await clickText('Text', 10_000);
await browser.pause(1_500);
// --- Verify text input is restored ---
const textRestored = await waitForAnyText(['Message OpenHuman', 'Type a message'], 10_000);
expect(textRestored).not.toBeNull();
});
it('shows reply mode toggle with text and voice options', async () => {
// Ensure conversations page is loaded (re-authenticate if state was lost).
const onConversations = await waitForAnyText(
['Message OpenHuman', 'Type a message', 'Reply'],
5_000
);
if (!onConversations) {
await triggerAuthDeepLink('e2e-voice-token');
await waitForWindowVisible(25_000);
await waitForWebView(15_000);
await waitForAppReady(15_000);
await completeOnboardingIfVisible('[VoiceModeE2E]');
await waitForHome(20_000);
}
// The Reply toggle should be visible on the conversations page
const hasReplyLabel = await textExists('Reply');
expect(hasReplyLabel).toBe(true);
// Verify both reply mode options exist
// (There are multiple "Text" and "Voice" buttons — Input + Reply groups)
const hasText = await textExists('Text');
expect(hasText).toBe(true);
});
});