diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 64c19cfe08..b9492b33c5 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -159,6 +159,16 @@ jobs: uses: dtolnay/rust-toolchain@1.93.0 with: targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + - name: Ensure macOS cross target on the toolchain-file toolchain + if: matrix.settings.platform == 'macos-latest' + run: | + # rust-toolchain.toml pins 1.96.1, which overrides the dtolnay-installed + # 1.93.0. The action's `targets:` input added the darwin targets to + # 1.93.0, not the override toolchain the vendored tauri-cli's CEF + # universal-helper build actually uses, so add them to the active + # (override) toolchain here to avoid E0463 "can't find crate for `core`". + cargo --version + rustup target add aarch64-apple-darwin x86_64-apple-darwin - name: Install Tauri dependencies (ubuntu only) if: startsWith(matrix.settings.platform, 'ubuntu-') run: | diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index fed1444cf0..2825207186 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -470,7 +470,15 @@ jobs: run: | rustup default 1.93.0 || true which cargo + # `cargo --version` in the repo root materializes the + # rust-toolchain.toml override toolchain (1.96.1), which supersedes the + # dtolnay-installed 1.93.0. The action's `targets:` input added + # x86_64-apple-darwin to 1.93.0, NOT the override toolchain the CEF + # universal-helper build actually uses — so add it to the active + # (override) toolchain here or the cross-compile fails with E0463 + # "can't find crate for `core`". cargo --version + rustup target add x86_64-apple-darwin - name: Cache Rust build artifacts uses: Swatinem/rust-cache@v2 @@ -669,7 +677,15 @@ jobs: run: | rustup default 1.93.0 || true which cargo + # `cargo --version` in the repo root materializes the + # rust-toolchain.toml override toolchain (1.96.1), which supersedes the + # dtolnay-installed 1.93.0. The action's `targets:` input added + # x86_64-apple-darwin to 1.93.0, NOT the override toolchain the CEF + # universal-helper build actually uses — so add it to the active + # (override) toolchain here or the cross-compile fails with E0463 + # "can't find crate for `core`". cargo --version + rustup target add x86_64-apple-darwin - name: Cache Rust build artifacts uses: Swatinem/rust-cache@v2 diff --git a/app/test/e2e/helpers/shared-flows.ts b/app/test/e2e/helpers/shared-flows.ts index 9bec60cb5c..b24ca9355d 100644 --- a/app/test/e2e/helpers/shared-flows.ts +++ b/app/test/e2e/helpers/shared-flows.ts @@ -717,9 +717,13 @@ export async function walkOnboarding(logPrefix = '[E2E]', maxSteps = 12): Promis await browser.pause(1_500); } - // Wait up to 15s for the onboarding shell to actually mount. If the user is - // already onboarded (e.g. resuming an existing session) the button never - // appears and we return without firing any clicks. + // Wait for the onboarding shell to actually mount. If the user is already + // onboarded (e.g. resuming an existing session — the common case in the + // shared-workspace E2E run) the button never appears and this wait is pure + // dead time on *every* resetApp, pushing the whole bring-up toward the 30s + // Mocha hook ceiling. 8s keeps ample headroom for a cold CEF boot to paint + // the button (it appears within a few seconds when onboarding is genuinely + // needed); the rare cold-boot miss is caught by the spec-file retry. const appeared = await browser .waitUntil( async () => @@ -728,7 +732,7 @@ export async function walkOnboarding(logPrefix = '[E2E]', maxSteps = 12): Promis () => document.querySelector('[data-testid="onboarding-next-button"]') !== null ) ), - { timeout: 15_000, interval: 500, timeoutMsg: 'onboarding-next-button never appeared' } + { timeout: 8_000, interval: 500, timeoutMsg: 'onboarding-next-button never appeared' } ) .catch(() => false); diff --git a/app/test/e2e/specs/auth-access-control.spec.ts b/app/test/e2e/specs/auth-access-control.spec.ts index 6b17816f9b..0f41b02229 100644 --- a/app/test/e2e/specs/auth-access-control.spec.ts +++ b/app/test/e2e/specs/auth-access-control.spec.ts @@ -107,7 +107,7 @@ async function performFullLogin(token = 'e2e-test-token') { await waitForAppReady(15_000); await waitForAuthBootstrap(15_000); - const consumeCall = await waitForRequest('POST', '/telegram/login-tokens/', 20_000); + const consumeCall = await waitForRequest('POST', '/auth/login-token/consume', 20_000); if (!consumeCall) { console.log( '[AuthAccess] Missing consume call. Request log:', @@ -183,7 +183,7 @@ describe('Auth & Access Control', () => { const homeText = await waitForHomePage(500); if (homeText) return true; const consumed = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); return !!consumed; }, @@ -211,7 +211,7 @@ describe('Auth & Access Control', () => { await browser.waitUntil( async () => { const consumed = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); return !!consumed; }, @@ -230,7 +230,7 @@ describe('Auth & Access Control', () => { expect(finalHome).not.toBeNull(); const consumeCall = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); expect(consumeCall).toBeDefined(); console.log('[AuthAccess] Multi-device token accepted'); @@ -325,7 +325,7 @@ describe('Auth & Access Control', () => { await browser.waitUntil( async () => { const consumed = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); return !!consumed; }, @@ -465,7 +465,7 @@ describe('Auth & Access Control', () => { const homeText = await waitForHomePage(500); if (!homeText) return true; // navigated away — auto-logout happened const consumed = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); return !!consumed; }, diff --git a/app/test/e2e/specs/card-payment-flow.spec.ts b/app/test/e2e/specs/card-payment-flow.spec.ts index 5c02f1af8b..84688eebfc 100644 --- a/app/test/e2e/specs/card-payment-flow.spec.ts +++ b/app/test/e2e/specs/card-payment-flow.spec.ts @@ -18,7 +18,10 @@ import { clearRequestLog, startMockServer, stopMockServer } from '../mock-server const LOG_PREFIX = '[PaymentFlow]'; describe('Card Payment Flow', () => { - before(async () => { + before(async function () { + // resetApp bring-up can run ~25-30s and race the default 30s Mocha hook + // budget; raise it. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp('e2e-card-payment-token'); diff --git a/app/test/e2e/specs/chat-harness-scroll-render.spec.ts b/app/test/e2e/specs/chat-harness-scroll-render.spec.ts index 49b551b089..05fdda823e 100644 --- a/app/test/e2e/specs/chat-harness-scroll-render.spec.ts +++ b/app/test/e2e/specs/chat-harness-scroll-render.spec.ts @@ -64,18 +64,19 @@ const STREAM_SCRIPT = [ { finish: 'stop' }, ]; -// The message column is the `
` -// in Conversations.tsx. There's only one in /chat so a CSS predicate -// matching the unique `bg-[#f6f6f6]` class is enough. +// The message column is the `
` 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; }> { return (await browser.execute(() => { - const el = document.querySelector( - 'div.flex-1.overflow-y-auto.bg-\\[\\#f6f6f6\\]' - ) as HTMLElement | null; + // 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 }; return { scrollTop: el.scrollTop, @@ -87,9 +88,10 @@ async function scrollMetrics(): Promise<{ async function scrollMessageColumn(top: number): Promise { await browser.execute((y: number) => { - const el = document.querySelector( - 'div.flex-1.overflow-y-auto.bg-\\[\\#f6f6f6\\]' - ) as HTMLElement | null; + // 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) el.scrollTo({ top: y, behavior: 'auto' }); }, top); } diff --git a/app/test/e2e/specs/composio-triggers-flow.spec.ts b/app/test/e2e/specs/composio-triggers-flow.spec.ts index 4fc64e18d5..9106513817 100644 --- a/app/test/e2e/specs/composio-triggers-flow.spec.ts +++ b/app/test/e2e/specs/composio-triggers-flow.spec.ts @@ -23,7 +23,9 @@ import { navigateToSkills } from '../helpers/shared-flows'; import { clearRequestLog, setMockBehavior, startMockServer, stopMockServer } from '../mock-server'; describe('Composio trigger toggles (UI + core RPC)', () => { - before(async () => { + before(async function () { + // waitForApp() + resetApp() can exceed the default 30s Mocha hook budget. + this.timeout(90_000); await startMockServer(); setMockBehavior( 'composioConnections', diff --git a/app/test/e2e/specs/connector-jira.spec.ts b/app/test/e2e/specs/connector-jira.spec.ts index 34461edf67..1277dbc59f 100644 --- a/app/test/e2e/specs/connector-jira.spec.ts +++ b/app/test/e2e/specs/connector-jira.spec.ts @@ -70,9 +70,21 @@ describe('Jira Composio connector flow', () => { it('connect modal renders subdomain input field for Jira', async function () { this.timeout(60_000); - // Seed as idle (no active connection) so we see the connect flow - seedComposioConnection(TOOLKIT_SLUG, 'CONNECTING', 'c-jira-idle'); + // Seed as idle (no active connection) so we see the connect flow. The prior + // test rendered Jira ACTIVE, which persisted into the durable client-side + // connection cache (localStorage `composio:connections`). That cache + // re-seeds the tile as connected on remount — before the live `[]` fetch + // lands — and ComposioConnectModal latches its phase once at mount, so the + // modal would open stuck in the `connected` phase (no subdomain form). + // Clear the durable cache so the tile mounts disconnected and the modal + // opens in `idle`. setMockBehavior('composioConnections', JSON.stringify([])); + // @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env + await browser.execute(() => { + Object.keys(window.localStorage) + .filter(k => k.includes('composio:connections')) + .forEach(k => window.localStorage.removeItem(k)); + }); await navigateToSkills(); await waitForText(CONNECTOR_NAME, 10_000); const modal = await openConnectorModal(CONNECTOR_NAME); diff --git a/app/test/e2e/specs/cron-jobs-flow.spec.ts b/app/test/e2e/specs/cron-jobs-flow.spec.ts index 59630e298f..47c3920b87 100644 --- a/app/test/e2e/specs/cron-jobs-flow.spec.ts +++ b/app/test/e2e/specs/cron-jobs-flow.spec.ts @@ -105,7 +105,9 @@ async function openCronJobsPanel(): Promise { } describe('Cron jobs settings panel (real UI flow)', () => { - before(async () => { + before(async function () { + // waitForApp() + resetApp() can exceed the default 30s Mocha hook budget. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp(USER_ID); diff --git a/app/test/e2e/specs/local-model-runtime.spec.ts b/app/test/e2e/specs/local-model-runtime.spec.ts index 8b0663964f..1d81bc1fd6 100644 --- a/app/test/e2e/specs/local-model-runtime.spec.ts +++ b/app/test/e2e/specs/local-model-runtime.spec.ts @@ -67,7 +67,7 @@ describe.skip('Local model runtime flow', () => { await waitForWebView(15_000); await waitForAppReady(15_000); - const consume = await waitForRequest('POST', '/telegram/login-tokens/'); + const consume = await waitForRequest('POST', '/auth/login-token/consume'); expect(consume).toBeDefined(); await walkOnboarding('[LocalModel]'); diff --git a/app/test/e2e/specs/login-flow.spec.ts b/app/test/e2e/specs/login-flow.spec.ts index 1843592e58..1f54d2ebb7 100644 --- a/app/test/e2e/specs/login-flow.spec.ts +++ b/app/test/e2e/specs/login-flow.spec.ts @@ -5,7 +5,7 @@ * Verifies the full auth + onboarding journey using mock data: * Phase 1 — Deep link authentication: * 1. `openhuman://auth?token=...` deep link is triggered via __simulateDeepLink - * 2. App calls POST /telegram/login-tokens/:token/consume (mock server) + * 2. App calls POST /auth/login-token/consume (mock server) * 3. App receives JWT, dispatches to Redux authSlice * 4. UserProvider calls GET /auth/me (mock server) * @@ -143,7 +143,7 @@ describe('Login flow — complete with mock data (Linux)', () => { }); it('mock server received the token-consume call', async () => { - const call = await waitForRequest('POST', '/telegram/login-tokens/', 20_000); + const call = await waitForRequest('POST', '/auth/login-token/consume', 20_000); if (!call) { console.log( '[LoginFlow] Missing consume call. Request log:', @@ -304,7 +304,7 @@ describe('Login flow — complete with mock data (Linux)', () => { await browser.pause(5_000); // Verify the consume call was made (mock returns 401 for expired tokens) - const call = await waitForRequest('POST', '/telegram/login-tokens/', 10_000); + const call = await waitForRequest('POST', '/auth/login-token/consume', 10_000); expect(call).toBeDefined(); console.log('[LoginFlow] Expired token: consume call made (mock returns 401)'); @@ -321,7 +321,7 @@ describe('Login flow — complete with mock data (Linux)', () => { await browser.pause(5_000); // Verify the consume call was made (mock returns 401 for invalid tokens) - const call = await waitForRequest('POST', '/telegram/login-tokens/', 10_000); + const call = await waitForRequest('POST', '/auth/login-token/consume', 10_000); expect(call).toBeDefined(); console.log('[LoginFlow] Invalid token: consume call made (mock returns 401)'); @@ -350,7 +350,7 @@ describe('Login flow — complete with mock data (Linux)', () => { // Assert NO consume call was made (bypass skips it) const consumeCall = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); expect(consumeCall).toBeUndefined(); console.log('[LoginFlow] Bypass auth: no consume call (correct — token set directly)'); diff --git a/app/test/e2e/specs/logout-relogin-onboarding.spec.ts b/app/test/e2e/specs/logout-relogin-onboarding.spec.ts index fa6b626449..ffeb50c89f 100644 --- a/app/test/e2e/specs/logout-relogin-onboarding.spec.ts +++ b/app/test/e2e/specs/logout-relogin-onboarding.spec.ts @@ -107,7 +107,7 @@ describe('Logout -> re-login onboarding overlay', function () { // ── Second login (re-login) ─────────────────────────────────────────────── // Use the bypass deep-link path (key=auth) which skips the - // consumeLoginToken→/telegram/login-tokens/ exchange. After the complex + // consumeLoginToken→/auth/login-token/consume exchange. After the complex // logout→test_reset→reload cycle, the full consume flow can race against // waitForOAuthAuthReadiness timing — the bypass avoids that instability // while still exercising the core auth path (storeSession, session-token diff --git a/app/test/e2e/specs/mega-flow.spec.ts b/app/test/e2e/specs/mega-flow.spec.ts index bf6a3c01a1..4f5bc3cbc7 100644 --- a/app/test/e2e/specs/mega-flow.spec.ts +++ b/app/test/e2e/specs/mega-flow.spec.ts @@ -105,7 +105,7 @@ async function resetEverything(label: string): Promise { // scenario arrive and can be discarded — then clear AFTER the pause so // stale requests don't appear in the next scenario's waitForMockRequest // polls. (Clearing before the pause caused a race: in-flight /auth/me or - // /telegram/login-tokens/ requests from scenario N arrived during the + // /auth/login-token/consume requests from scenario N arrived during the // 800 ms window, landed in the fresh log, and were matched by scenario // N+1's waitForMockRequest — causing composio RPCs to fire before the // new deep-link's auth flow completed.) @@ -145,7 +145,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // ------------------------------------------------------------------------- // Scenario 1 — login via real token-consume deep link. - // Expectation: the app POSTs to `/telegram/login-tokens/:t/consume`, gets a + // Expectation: the app POSTs to `/auth/login-token/consume`, gets a // JWT back from the mock, and follows up with `GET /auth/me`. // ------------------------------------------------------------------------- it('login: consume deep link triggers /consume + /auth/me on the mock', async () => { @@ -154,7 +154,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await triggerDeepLink('openhuman://auth?token=mega-login-token'); - const consume = await waitForMockRequest('POST', '/telegram/login-tokens/', 20_000); + const consume = await waitForMockRequest('POST', '/auth/login-token/consume', 20_000); expect(consume).toBeDefined(); console.log(`${LOG} consume hit:`, consume?.url); @@ -187,7 +187,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { expect(me).toBeDefined(); const consume = getRequestLog().find( - r => r.method === 'POST' && r.url.includes('/telegram/login-tokens/') + r => r.method === 'POST' && r.url.includes('/auth/login-token/consume') ); expect(consume).toBeUndefined(); console.log(`${LOG} bypass: no consume call, /auth/me succeeded`); @@ -211,7 +211,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // Login first — `oauth:success` is only meaningful for an authenticated user. await triggerDeepLink('openhuman://auth?token=mega-gmail-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); expect(await waitForMockRequest('GET', '/auth/me', 10_000)).toBeDefined(); clearRequestLog(); @@ -306,7 +306,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('after Scenario 4'); await triggerDeepLink('openhuman://auth?token=mega-error-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); await triggerDeepLink('openhuman://oauth/error?provider=google&error=access_denied'); @@ -337,7 +337,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // Login so the RPC layer has an authenticated session. await triggerDeepLink('openhuman://auth?token=mega-stale-thread-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Attempt to append a message to a thread ID that does not exist. @@ -373,7 +373,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // Login so the RPC relay is authenticated. await triggerDeepLink('openhuman://auth?token=mega-unknown-method-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Call a method name that no controller has registered. @@ -401,7 +401,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('final'); await triggerDeepLink('openhuman://auth?token=mega-post-reset-token'); - const consume = await waitForMockRequest('POST', '/telegram/login-tokens/', 20_000); + const consume = await waitForMockRequest('POST', '/auth/login-token/consume', 20_000); expect(consume).toBeDefined(); const me = await waitForMockRequest('GET', '/auth/me', 15_000); expect(me).toBeDefined(); @@ -422,7 +422,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('after Scenario 6'); await triggerDeepLink('openhuman://auth?token=mega-whatsapp-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Seed two chats via the internal ingest path. @@ -504,7 +504,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // ── User A login ────────────────────────────────────────────────────── await triggerDeepLink('openhuman://auth?token=mega-acct-switch-user-a'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Create a thread as user A. @@ -530,7 +530,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('account switch to user B'); await triggerDeepLink('openhuman://auth?token=mega-acct-switch-user-b'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Verify RPC is healthy for "User B". The thread list is a valid array @@ -551,7 +551,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('account switch back to user A'); await triggerDeepLink('openhuman://auth?token=mega-acct-switch-user-a'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); const listA2 = await callOpenhumanRpc('openhuman.threads_list', {}); @@ -680,7 +680,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { // Login so the RPC relay is authenticated. await triggerDeepLink('openhuman://auth?token=mega-update-version-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); const result = await callOpenhumanRpc('openhuman.update_version', {}); @@ -741,7 +741,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('before notification-dedup scenario'); await triggerDeepLink('openhuman://auth?token=mega-notification-dedup-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); const notifPayload = { @@ -805,7 +805,7 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => { await resetEverything('before thread-crud-smoke scenario'); await triggerDeepLink('openhuman://auth?token=mega-thread-crud-token'); - await waitForMockRequest('POST', '/telegram/login-tokens/', 15_000); + await waitForMockRequest('POST', '/auth/login-token/consume', 15_000); clearRequestLog(); // Step 1 — create a fresh thread. diff --git a/app/test/e2e/specs/navigation-settings-panels.spec.ts b/app/test/e2e/specs/navigation-settings-panels.spec.ts index 847405c4f7..1e0a354d49 100644 --- a/app/test/e2e/specs/navigation-settings-panels.spec.ts +++ b/app/test/e2e/specs/navigation-settings-panels.spec.ts @@ -192,7 +192,9 @@ describe('Navigation — settings sub-panels', () => { expect(homeText).toBeTruthy(); const hash = await browser.execute(() => window.location.hash); - expect(hash).toMatch(/^#\/home/); + // Home merged into the unified chat surface: navigateToHome() lands on + // #/chat (/home redirects there). Accept either. + expect(hash).toMatch(/^#\/(chat|home)/); console.log(`${LOG_PREFIX} N2.9: passed — home content: "${homeText}"`); }); }); diff --git a/app/test/e2e/specs/navigation.spec.ts b/app/test/e2e/specs/navigation.spec.ts index 5448f6c8b3..458dc13aac 100644 --- a/app/test/e2e/specs/navigation.spec.ts +++ b/app/test/e2e/specs/navigation.spec.ts @@ -32,15 +32,15 @@ interface Route { // /home → /chat (Phase 6 — /home is now the merged chat surface) // /human → /chat (Phase 6 — back-compat redirect) // /skills → /connections (Phase 2 — back-compat redirect) -// /intelligence → /activity (Phase 3 — back-compat redirect) -// Note: /home is intentionally omitted here because AppRoutes.tsx redirects it -// to /chat — navigateViaHash('/home') settles on #/chat, which is covered by -// the /chat row. Keeping /home in ROUTES would cause the hash assertion to -// fail since the actual hash is #/chat, not #/home. +// /intelligence → /settings/notifications (Phase 3 — back-compat redirect) +// /activity → /settings/notifications (back-compat redirect) +// Note: /home and /activity are intentionally omitted here because AppRoutes.tsx +// now redirects them (/home → /chat, /activity → /settings/notifications). +// navigateViaHash settles on the redirect target, so keeping them in ROUTES +// would fail the `^#` assertion (actual hash is the redirect destination). const ROUTES: Route[] = [ { hash: '/chat' }, { hash: '/connections' }, - { hash: '/activity' }, { hash: '/rewards' }, { hash: '/settings' }, { hash: '/agent-world' }, diff --git a/app/test/e2e/specs/notifications.spec.ts b/app/test/e2e/specs/notifications.spec.ts index 6e496cf77c..bb7bbef658 100644 --- a/app/test/e2e/specs/notifications.spec.ts +++ b/app/test/e2e/specs/notifications.spec.ts @@ -70,7 +70,10 @@ async function waitForCoreSidecar(timeout = 30_000): Promise { let ingestedNotifId: string | undefined; describe('Notifications', () => { - before(async () => { + before(async function () { + // waitForApp() + waitForCoreSidecar(30s) cannot fit the default 30s Mocha + // hook budget; raise it like the other real-flow specs do. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp('e2e-notifications-user'); diff --git a/app/test/e2e/specs/onboarding-modes.spec.ts b/app/test/e2e/specs/onboarding-modes.spec.ts index f9e7774ac7..7093537af0 100644 --- a/app/test/e2e/specs/onboarding-modes.spec.ts +++ b/app/test/e2e/specs/onboarding-modes.spec.ts @@ -147,7 +147,10 @@ async function waitForHome(timeout = 20_000): Promise { const deadline = Date.now() + timeout; while (Date.now() < deadline) { const hash = await currentHash(); - if (hash.startsWith('#/home')) return true; + // Home was merged into the unified chat surface: /home redirects to /chat + // (AppRoutes.tsx). Accept either so the wizard's post-finish landing check + // survives the IA change. + if (hash.startsWith('#/home') || hash.startsWith('#/chat')) return true; await pause(400); } return false; @@ -254,7 +257,13 @@ describe('Onboarding modes — Simple (Cloud) vs Advanced (Custom)', () => { await clickTestId('onboarding-runtime-choice-custom'); await pause(300); } - await clickOnboardingNext(); + // Only advance past RuntimeChoice if we're still on that step. In local + // session mode RuntimeChoicePage auto-redirects to custom/inference, so + // an unconditional Next here over-advances inference→voice (matches the + // Phase C guard below). + if (await testIdExists('onboarding-runtime-choice-step', 500)) { + await clickOnboardingNext(); + } } } // else: local session mode — already redirected to custom/inference, continue there. @@ -395,6 +404,13 @@ describe('Onboarding modes — Simple (Cloud) vs Advanced (Custom)', () => { }, want); expect(dispatched).toBe(true); + // Voice Routing was decoupled into staged edit + explicit Save: the select's + // onChange only stages `sttProvider` (VoicePanel `onSttProviderChange`); + // persistence to config.toml happens on the always-rendered Save button + // (`save-voice-routing`, enabled once there are routing changes). Click it so + // the staged provider actually writes through. + expect(await clickTestId('save-voice-routing')).toBe(true); + // Poll config.toml for the new value. let onDisk: string | null = null; const deadline = Date.now() + 10_000; diff --git a/app/test/e2e/specs/rewards-progression-persistence.spec.ts b/app/test/e2e/specs/rewards-progression-persistence.spec.ts index a69e2ecf10..52a536a036 100644 --- a/app/test/e2e/specs/rewards-progression-persistence.spec.ts +++ b/app/test/e2e/specs/rewards-progression-persistence.spec.ts @@ -98,6 +98,9 @@ async function getRewardsMetricValue(label: string): Promise { describe('Rewards progression & persistence', () => { before(async function beforeSuite() { + // resetApp bring-up can run ~25-30s and race the default 30s Mocha hook + // budget; raise it. + this.timeout(90_000); if (!supportsExecuteScript()) { stepLog('Skipping suite on Mac2 — Rewards bottom-tab label not mapped for Appium'); this.skip(); diff --git a/app/test/e2e/specs/settings-advanced-config.spec.ts b/app/test/e2e/specs/settings-advanced-config.spec.ts index 4e8ac1d9c8..5a23384980 100644 --- a/app/test/e2e/specs/settings-advanced-config.spec.ts +++ b/app/test/e2e/specs/settings-advanced-config.spec.ts @@ -121,7 +121,23 @@ describe('Settings - Advanced Config', () => { const input = await browser.$('#autonomy-max-actions'); await input.waitForExist({ timeout: 10_000 }); - await input.setValue(String(target)); + // Drive the controlled number input via the native value setter + React + // change event. WebDriver's setValue clears the field but doesn't reliably + // fire React's synthetic onChange, so `draft`/`isChanged` wouldn't update + // and the Save button (canSave = isValid && isChanged) would stay a + // disabled no-op — "Saved." would never appear. + await browser.execute((val: string) => { + const el = document.querySelector('#autonomy-max-actions'); + if (!el) return; + const setter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set; + if (setter) setter.call(el, val); + else el.value = val; + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); + }, String(target)); await clickText('Save', 10_000); await waitForText('Saved.', 10_000); diff --git a/app/test/e2e/specs/settings-channels-permissions.spec.ts b/app/test/e2e/specs/settings-channels-permissions.spec.ts index faba179be8..99d365af5b 100644 --- a/app/test/e2e/specs/settings-channels-permissions.spec.ts +++ b/app/test/e2e/specs/settings-channels-permissions.spec.ts @@ -34,7 +34,10 @@ async function defaultMessagingChannel(): Promise { const USER_ID = 'e2e-settings-channels'; describe('Settings - Channels & Permissions', () => { - before(async () => { + before(async function () { + // resetApp bring-up can run ~25-30s and race the default 30s Mocha hook + // budget; raise it. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp(USER_ID); diff --git a/app/test/e2e/specs/settings-dev-options.spec.ts b/app/test/e2e/specs/settings-dev-options.spec.ts index d56736d7e1..91c2b523e7 100644 --- a/app/test/e2e/specs/settings-dev-options.spec.ts +++ b/app/test/e2e/specs/settings-dev-options.spec.ts @@ -35,7 +35,10 @@ describe('Settings - Developer Options', () => { this.timeout(90_000); await navigateViaHash('/settings/webhooks-debug'); - await waitForText('Webhooks Debug', 15_000); + // Panel heading comes from the route registry titleKey + // (settings.developerMenu.webhooks.title = "Webhooks"); the old + // "Webhooks Debug" (webhooks.debugTitle) is no longer rendered. + await waitForText('Webhooks', 15_000); await waitForText('Registered Webhooks', 15_000); await waitForText('Captured Requests', 15_000); expect(await textExists('Refresh')).toBe(true); @@ -43,9 +46,11 @@ describe('Settings - Developer Options', () => { it('mounts Memory Debug panel (13.4.3)', async function () { this.timeout(90_000); + // /settings/memory-debug now redirects to /brain?tab=memory-debug, where + // the relocated MemoryDebugPanel renders (no "Memory Debug" heading of its + // own). Assert on the section content that still renders there. await navigateViaHash('/settings/memory-debug'); - await waitForText('Memory Debug', 15_000); await waitForText('Documents', 15_000); await waitForText('Namespaces', 15_000); await waitForText('Query & Recall', 15_000); @@ -56,7 +61,9 @@ describe('Settings - Developer Options', () => { this.timeout(90_000); await navigateViaHash('/settings/autocomplete-debug'); - await waitForText('Autocomplete Debug', 15_000); + // Panel heading is settings.developerMenu.autocomplete.title = "Autocomplete"; + // the old "Autocomplete Debug" (autocomplete.debugTitle) is no longer used. + await waitForText('Autocomplete', 15_000); await waitForText('Live Logs', 15_000); const logsFound = (await textExists('No logs yet.')) || (await textExists('[runtime]')); diff --git a/app/test/e2e/specs/skill-execution-flow.spec.ts b/app/test/e2e/specs/skill-execution-flow.spec.ts index db7273ab42..90b984c322 100644 --- a/app/test/e2e/specs/skill-execution-flow.spec.ts +++ b/app/test/e2e/specs/skill-execution-flow.spec.ts @@ -31,7 +31,8 @@ describe('Skill discovery (UI + core RPC)', () => { it('lands the user on a logged-in shell', async () => { const atHome = (await textExists('Ask your assistant anything')) || - (await textExists('Your device is connected')); + (await textExists('Your device is connected')) || + (await textExists('Your assistant is ready when you are')); expect(atHome).toBe(true); }); diff --git a/app/test/e2e/specs/tool-browser-flow.spec.ts b/app/test/e2e/specs/tool-browser-flow.spec.ts index de3594f309..c8535d27ba 100644 --- a/app/test/e2e/specs/tool-browser-flow.spec.ts +++ b/app/test/e2e/specs/tool-browser-flow.spec.ts @@ -64,7 +64,10 @@ interface ListDefinitionsResult { } describe('System tools — Browser (open URL + automation registry)', () => { - before(async () => { + before(async function () { + // resetApp bring-up can run ~25-30s and race the default 30s Mocha hook + // budget; raise it. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp(USER_ID); diff --git a/app/test/e2e/specs/voice-mode.spec.ts b/app/test/e2e/specs/voice-mode.spec.ts index 9da14686bf..b3b5a3177f 100644 --- a/app/test/e2e/specs/voice-mode.spec.ts +++ b/app/test/e2e/specs/voice-mode.spec.ts @@ -84,7 +84,7 @@ describe.skip('Voice mode integration', () => { await waitForWebView(15_000); await waitForAppReady(15_000); - const consume = await waitForRequest('POST', '/telegram/login-tokens/'); + const consume = await waitForRequest('POST', '/auth/login-token/consume'); expect(consume).toBeDefined(); await completeOnboardingIfVisible('[VoiceModeE2E]'); diff --git a/app/test/e2e/specs/webhooks-ingress-flow.spec.ts b/app/test/e2e/specs/webhooks-ingress-flow.spec.ts index 54409b6c5e..efcf8880a7 100644 --- a/app/test/e2e/specs/webhooks-ingress-flow.spec.ts +++ b/app/test/e2e/specs/webhooks-ingress-flow.spec.ts @@ -24,7 +24,10 @@ async function openWebhooksDebugPanel(): Promise { } describe('Webhooks ingress surface (stub-level)', () => { - before(async () => { + before(async function () { + // resetApp bring-up (waitForApp + onboarding walk + home confirm) can run + // ~25-30s and race the default 30s Mocha hook budget; raise it. + this.timeout(90_000); await startMockServer(); await waitForApp(); await resetApp(USER_ID); @@ -104,7 +107,9 @@ describe('Webhooks ingress surface (stub-level)', () => { stepLog('Navigated to webhooks debug route', { currentHash }); expect(String(currentHash)).toContain('/settings/webhooks-debug'); - await waitForText('Webhooks Debug', 12_000); + // Panel heading is the route-registry title "Webhooks" now (the old + // "Webhooks Debug" webhooks.debugTitle is no longer rendered). + await waitForText('Webhooks', 12_000); await waitForText('Registered Webhooks', 12_000); await waitForText('Captured Requests', 12_000); diff --git a/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts b/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts index 9a7871cb59..b002f113e5 100644 --- a/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts +++ b/app/test/e2e/specs/webhooks-tunnel-flow.spec.ts @@ -64,7 +64,10 @@ function unwrapRpcValue(raw: unknown): T | undefined { } describe('Webhook tunnel CRUD (UI + core RPC + mock backend)', () => { - before(async () => { + before(async function () { + // resetApp bring-up can run ~25-30s and race the default 30s Mocha hook + // budget; raise it. + this.timeout(90_000); await startMockServer(); await resetMockBehavior(); await waitForApp(); diff --git a/app/test/playwright/specs/login-flow.spec.ts b/app/test/playwright/specs/login-flow.spec.ts index 7346c4229e..6c78883cb4 100644 --- a/app/test/playwright/specs/login-flow.spec.ts +++ b/app/test/playwright/specs/login-flow.spec.ts @@ -64,7 +64,7 @@ test.describe('Login Flow', () => { .toMatch(/^#\/(home|chat)(\/|$)/); const consumeCall = (await requests()).find( - request => request.method === 'POST' && request.url.includes('/telegram/login-tokens/') + request => request.method === 'POST' && request.url.includes('/auth/login-token/consume') ); expect(consumeCall).toBeUndefined(); await expect(await waitForMockRequest('GET', '/auth/me')).toBeTruthy(); diff --git a/app/test/wdio.conf.ts b/app/test/wdio.conf.ts index a6f0dfbe61..c297a5a158 100644 --- a/app/test/wdio.conf.ts +++ b/app/test/wdio.conf.ts @@ -115,6 +115,12 @@ export const config: Options.Testrunner & Record = { // run. `--bail` on e2e-run-all-flows.sh sets E2E_BAIL_ON_FAILURE=1 so we // flip this to 1 (= stop after the first failed spec). bail: process.env.E2E_BAIL_ON_FAILURE === '1' ? 1 : 0, + // Retry a failed spec file once (unless bailing) to absorb transient + // shared-session flakiness — cold-start warmups and `resetApp` bring-up + // races that sit right at the Mocha hook budget. Deterministic failures + // still fail on the retry; this only rescues the genuinely-flaky ones. + specFileRetries: process.env.E2E_BAIL_ON_FAILURE === '1' ? 0 : 1, + specFileRetriesDeferred: true, waitforTimeout: 10_000, connectionRetryTimeout: 120_000, connectionRetryCount: 3, diff --git a/scripts/mock-api/routes/auth.mjs b/scripts/mock-api/routes/auth.mjs index 93f1170afe..a81350345d 100644 --- a/scripts/mock-api/routes/auth.mjs +++ b/scripts/mock-api/routes/auth.mjs @@ -11,9 +11,15 @@ export async function handleAuth(ctx) { const { method, url, res, origin } = ctx; const mockBehavior = behavior(); + // Login-token consume. The core POSTs to `/auth/login-token/consume` with the + // token in a JSON body `{ token, audience? }` and parses `{ success, data: { jwt } }` + // (see src/api/rest.rs `consume_login_token`). The legacy path-param route + // `/telegram/login-tokens/:token/consume` was removed backend-side but is kept + // here as a harmless alias in case an older client is exercised. if ( method === "POST" && - /^\/telegram\/login-tokens\/[^/]+\/consume\/?$/.test(url) + (/^\/auth\/login-token\/consume\/?$/.test(url) || + /^\/telegram\/login-tokens\/[^/]+\/consume\/?$/.test(url)) ) { if (mockBehavior.token === "expired") { json(res, 401, { success: false, error: "Token expired or invalid" }); @@ -24,7 +30,7 @@ export async function handleAuth(ctx) { return true; } const jwt = mockBehavior.jwt ? `${MOCK_JWT}-${mockBehavior.jwt}` : MOCK_JWT; - json(res, 200, { success: true, data: { jwtToken: jwt } }); + json(res, 200, { success: true, data: { jwt } }); return true; } diff --git a/src/openhuman/tinyagents/observability.rs b/src/openhuman/tinyagents/observability.rs index 912a64e903..8ea9c9168c 100644 --- a/src/openhuman/tinyagents/observability.rs +++ b/src/openhuman/tinyagents/observability.rs @@ -1094,6 +1094,7 @@ mod tests { sink.emit(AgentEvent::ToolCompleted { call_id: "c1".into(), tool_name: "echo".to_string(), + started_at_ms: None, input: None, output: None, });