mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 06:32:24 +00:00
* 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)
169 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|