Skip to content
Merged
10 changes: 10 additions & 0 deletions .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/e2e-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions app/test/e2e/helpers/shared-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () =>
Expand All @@ -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);

Expand Down
12 changes: 6 additions & 6 deletions app/test/e2e/specs/auth-access-control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:',
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
},
Expand All @@ -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');
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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;
},
Expand Down
5 changes: 4 additions & 1 deletion app/test/e2e/specs/card-payment-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
20 changes: 11 additions & 9 deletions app/test/e2e/specs/chat-harness-scroll-render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,19 @@ const STREAM_SCRIPT = [
{ finish: 'stop' },
];

// The message column is the `<div ref={messagesContainerRef} className="flex-1 overflow-y-auto ...">`
// 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 `<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;
}> {
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,
Expand All @@ -87,9 +88,10 @@ async function scrollMetrics(): Promise<{

async function scrollMessageColumn(top: number): Promise<void> {
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);
}
Expand Down
4 changes: 3 additions & 1 deletion app/test/e2e/specs/composio-triggers-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 14 additions & 2 deletions app/test/e2e/specs/connector-jira.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
Comment on lines +83 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for the mounted Jira state to clear

When this test runs after the preceding Jira card test in the shared WDIO session, the app is already on /connections with the ACTIVE connection held in React state. Clearing only localStorage does not update the mounted useComposioIntegrations state until a later poll/refetch, and navigateToSkills() can be a same-route no-op, so opening the modal immediately can still latch the connected phase and fail to render the subdomain input. Force a remount/refresh or wait for the disconnected state before opening the modal.

Useful? React with 👍 / 👎.

await navigateToSkills();
await waitForText(CONNECTOR_NAME, 10_000);
const modal = await openConnectorModal(CONNECTOR_NAME);
Expand Down
4 changes: 3 additions & 1 deletion app/test/e2e/specs/cron-jobs-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ async function openCronJobsPanel(): Promise<void> {
}

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);
Expand Down
2 changes: 1 addition & 1 deletion app/test/e2e/specs/local-model-runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]');
Expand Down
10 changes: 5 additions & 5 deletions app/test/e2e/specs/login-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down Expand Up @@ -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:',
Expand Down Expand Up @@ -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)');

Expand All @@ -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)');

Expand Down Expand Up @@ -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)');
Expand Down
2 changes: 1 addition & 1 deletion app/test/e2e/specs/logout-relogin-onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading