mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test(e2e): add E2E coverage for 15 Composio connector flows (#2351)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Shared helpers for Composio connector E2E specs.
|
||||
*
|
||||
* All helpers are platform-agnostic (tauri-driver + Appium Mac2) and
|
||||
* follow the same patterns established in composio-triggers-flow.spec.ts
|
||||
* and the existing shared-flows / element-helpers modules.
|
||||
*/
|
||||
import { setMockBehavior } from '../mock-server';
|
||||
import { textExists, waitForText } from './element-helpers';
|
||||
import { navigateToHome, navigateToSkills, waitForHomePage } from './shared-flows';
|
||||
|
||||
const LOG = '[ComposioHelpers]';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seed helpers — set mock behavior knobs before navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Seed a single Composio connection into the mock backend.
|
||||
*
|
||||
* Sets the `composioConnections` behavior knob with a single entry for the
|
||||
* given toolkit. Subsequent calls overwrite any previous seed — isolate
|
||||
* specs by calling this in `beforeEach` or at the start of each test.
|
||||
*/
|
||||
export function seedComposioConnection(
|
||||
toolkit: string,
|
||||
status: 'ACTIVE' | 'FAILED' | 'EXPIRED' | 'CONNECTING',
|
||||
connectionId: string = 'c-e2e'
|
||||
): void {
|
||||
setMockBehavior('composioConnections', JSON.stringify([{ id: connectionId, toolkit, status }]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the list of available Composio toolkits shown on the Skills page.
|
||||
*
|
||||
* Sets the `composioToolkits` behavior knob to the given slugs array.
|
||||
*/
|
||||
export function seedComposioToolkits(slugs: string[]): void {
|
||||
setMockBehavior('composioToolkits', JSON.stringify(slugs));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Navigation + UI assertion helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Navigate to /skills and wait until the connector card with the given
|
||||
* display name is visible.
|
||||
*
|
||||
* Throws (via waitForText) if the card is not visible within the timeout.
|
||||
*/
|
||||
export async function assertConnectorCardVisible(name: string, timeout = 15_000): Promise<void> {
|
||||
await navigateToSkills();
|
||||
await waitForText(name, timeout);
|
||||
console.log(`${LOG} connector card visible: "${name}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a connector card by display name, then wait for the modal header
|
||||
* to appear. The modal header text is either "Connect <name>", "Manage
|
||||
* <name>", or "Reconnect <name>" depending on connection state.
|
||||
*
|
||||
* Returns the modal header text that was found, or null when none of the
|
||||
* candidates appeared within the timeout (so callers that can tolerate a
|
||||
* missing modal don't have to wrap in try/catch).
|
||||
*/
|
||||
export async function openConnectorModal(name: string, timeout = 15_000): Promise<string | null> {
|
||||
console.log(`${LOG} opening connector modal for "${name}"`);
|
||||
// Click the connector card by name
|
||||
const cardEl = await waitForText(name, timeout);
|
||||
await cardEl.click();
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
await browser.pause(1_500);
|
||||
|
||||
// Wait for any of the standard modal header patterns
|
||||
const candidates = [`Connect ${name}`, `Manage ${name}`, `Reconnect ${name}`];
|
||||
const deadline = Date.now() + timeout;
|
||||
while (Date.now() < deadline) {
|
||||
for (const candidate of candidates) {
|
||||
if (await textExists(candidate)) {
|
||||
console.log(`${LOG} modal opened: "${candidate}"`);
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
console.log(`${LOG} modal for "${name}" did not open within timeout`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the modal is in a given phase by checking UI markers.
|
||||
*
|
||||
* Phase markers:
|
||||
* idle — Connect button present (no active connection)
|
||||
* connected — "is connected" or Disconnect button visible
|
||||
* expired — "authorization expired" text visible
|
||||
* error — error UI present (coral-coloured error block)
|
||||
*/
|
||||
export async function assertModalPhase(
|
||||
phase: 'idle' | 'connected' | 'expired' | 'error',
|
||||
name: string,
|
||||
timeout = 10_000
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeout;
|
||||
|
||||
const phaseMarkers: Record<string, string[]> = {
|
||||
idle: [`Connect ${name}`, 'Connect'],
|
||||
connected: ['Disconnect', 'is connected'],
|
||||
expired: ['authorization expired', 'Reconnect'],
|
||||
error: ['Something went wrong', 'Authorization failed', 'dismissAll'],
|
||||
};
|
||||
|
||||
const markers = phaseMarkers[phase] ?? [];
|
||||
while (Date.now() < deadline) {
|
||||
for (const marker of markers) {
|
||||
if (await textExists(marker)) {
|
||||
console.log(`${LOG} modal phase "${phase}" confirmed via marker: "${marker}"`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
await browser.pause(400);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`assertModalPhase: phase "${phase}" for "${name}" not confirmed within ${timeout}ms — no marker found in [${markers.join(', ')}]`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user session is still alive (not logged out) by navigating
|
||||
* to /home and waiting for home page content.
|
||||
*
|
||||
* This is the key guard for the "401 on composio routes must NOT log user
|
||||
* out" class of regressions (#2285, #2286).
|
||||
*/
|
||||
export async function assertSessionNotNuked(timeout = 20_000): Promise<void> {
|
||||
console.log(`${LOG} asserting session is intact — navigating to /home`);
|
||||
await navigateToHome();
|
||||
const marker = await waitForHomePage(timeout);
|
||||
if (!marker) {
|
||||
throw new Error(`assertSessionNotNuked: Home page not reached — user may have been logged out`);
|
||||
}
|
||||
console.log(`${LOG} session intact, home page marker: "${marker}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a mock HTTP fault on all Composio routes by setting the
|
||||
* composioExecuteFails / composioDeleteFails / composioSyncFails behavior
|
||||
* knobs to trigger the given status code.
|
||||
*
|
||||
* Supported status codes: 400, 500.
|
||||
* The mock route handlers interpret knob value '400' → HTTP 400 and '500' → HTTP 500.
|
||||
*/
|
||||
export function injectComposioFault(statusCode: 400 | 500): void {
|
||||
const value = String(statusCode);
|
||||
setMockBehavior('composioExecuteFails', value);
|
||||
setMockBehavior('composioDeleteFails', value);
|
||||
setMockBehavior('composioSyncFails', value);
|
||||
console.log(`${LOG} injected composio fault: status=${statusCode}`);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* E2E: Airtable (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorAirtableE2E]';
|
||||
const CONNECTOR_NAME = 'Airtable';
|
||||
const TOOLKIT_SLUG = 'airtable';
|
||||
const AUTH_TOKEN = 'e2e-connector-airtable-token';
|
||||
|
||||
describe('Airtable Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-airtable-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-airtable-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-airtable-1',
|
||||
action: 'AIRTABLE_LIST_BASES',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-airtable-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-airtable-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-airtable-1',
|
||||
action: 'AIRTABLE_LIST_BASES',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-airtable-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-airtable-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* E2E: Asana (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorAsanaE2E]';
|
||||
const CONNECTOR_NAME = 'Asana';
|
||||
const TOOLKIT_SLUG = 'asana';
|
||||
const AUTH_TOKEN = 'e2e-connector-asana-token';
|
||||
|
||||
describe('Asana Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-asana-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-asana-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-asana-1',
|
||||
action: 'ASANA_LIST_TASKS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-asana-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-asana-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-asana-1',
|
||||
action: 'ASANA_LIST_TASKS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-asana-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-asana-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* E2E: ClickUp (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorClickUpE2E]';
|
||||
const CONNECTOR_NAME = 'ClickUp';
|
||||
const TOOLKIT_SLUG = 'clickup';
|
||||
const AUTH_TOKEN = 'e2e-connector-clickup-token';
|
||||
|
||||
describe('ClickUp Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-clickup-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-clickup-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-clickup-1',
|
||||
action: 'CLICKUP_LIST_TASKS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-clickup-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-clickup-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-clickup-1',
|
||||
action: 'CLICKUP_LIST_TASKS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-clickup-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-clickup-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* E2E: Confluence (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorConfluenceE2E]';
|
||||
const CONNECTOR_NAME = 'Confluence';
|
||||
const TOOLKIT_SLUG = 'confluence';
|
||||
const AUTH_TOKEN = 'e2e-connector-confluence-token';
|
||||
|
||||
describe('Confluence Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-confluence-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-confluence-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-confluence-1',
|
||||
action: 'CONFLUENCE_LIST_PAGES',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-confluence-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-confluence-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-confluence-1',
|
||||
action: 'CONFLUENCE_LIST_PAGES',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-confluence-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-confluence-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* E2E: Discord (Composio) connector flow.
|
||||
*
|
||||
* Critical regression (#2285): clicking the Discord connector card must NOT
|
||||
* log the user out, even if the card click triggers a failed auth attempt.
|
||||
* `assertSessionNotNuked` is called at every test boundary.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorDiscordComposioE2E]';
|
||||
const CONNECTOR_NAME = 'Discord';
|
||||
const TOOLKIT_SLUG = 'discord';
|
||||
const AUTH_TOKEN = 'e2e-connector-discord-composio-token';
|
||||
|
||||
describe('Discord (Composio) connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-discord-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-discord-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('clicking the Discord card does NOT log user out (#2285 regression)', async function () {
|
||||
this.timeout(60_000);
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
|
||||
// Click the card — regardless of what happens (modal opens, error, etc.)
|
||||
// the session must survive
|
||||
const cardEl = await waitForText(CONNECTOR_NAME, 10_000);
|
||||
try {
|
||||
await cardEl.click();
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
await browser.pause(2_000);
|
||||
} catch (err) {
|
||||
console.log(`${LOG} card click threw: ${err} — still asserting session`);
|
||||
}
|
||||
|
||||
// This is the critical regression check
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: Discord card click did NOT log user out (#2285)`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-discord-1',
|
||||
action: 'DISCORD_LIST_SERVERS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-discord-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-discord-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 4xx on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-discord-1',
|
||||
action: 'DISCORD_LIST_SERVERS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-discord-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-discord-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* E2E: GitHub Composio connector flow.
|
||||
*
|
||||
* Covers the standard connector lifecycle (card visibility, connect, connected
|
||||
* state, RPC routing, execute, error/expired states, disconnect) plus a
|
||||
* trigger-catalog assertion specific to GitHub.
|
||||
*
|
||||
* All backend calls are served by the mock server — no live GitHub account
|
||||
* is required.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
setMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorGithubE2E]';
|
||||
const CONNECTOR_NAME = 'GitHub';
|
||||
const TOOLKIT_SLUG = 'github';
|
||||
const AUTH_TOKEN = 'e2e-connector-github-token';
|
||||
|
||||
describe('GitHub Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-github-1');
|
||||
setMockBehavior(
|
||||
'composioAvailableTriggers',
|
||||
JSON.stringify([{ slug: 'GITHUB_COMMIT_EVENT', scope: 'static' }])
|
||||
);
|
||||
setMockBehavior('composioActiveTriggers', JSON.stringify([]));
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-github-1');
|
||||
setMockBehavior(
|
||||
'composioAvailableTriggers',
|
||||
JSON.stringify([{ slug: 'GITHUB_COMMIT_EVENT', scope: 'static' }])
|
||||
);
|
||||
setMockBehavior('composioActiveTriggers', JSON.stringify([]));
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-github-1');
|
||||
clearRequestLog();
|
||||
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(
|
||||
r => r.method === 'POST' && r.url.includes('/agent-integrations/composio/authorize')
|
||||
);
|
||||
expect(authReq).toBeDefined();
|
||||
const body = JSON.parse(authReq?.body || '{}');
|
||||
expect(body.toolkit).toBe(TOOLKIT_SLUG);
|
||||
console.log(`${LOG} PASS: auth/connect RPC routes correctly`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-github-1');
|
||||
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const log = getRequestLog();
|
||||
const syncReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: composio_sync routed to mock (status ${syncReq?.statusCode})`);
|
||||
// Session must remain alive regardless
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-github-1',
|
||||
action: 'GITHUB_LIST_REPOS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: composio_execute routed to mock`);
|
||||
});
|
||||
|
||||
it('trigger catalog lists available GitHub triggers', async function () {
|
||||
this.timeout(30_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_available_triggers', {
|
||||
toolkit: TOOLKIT_SLUG,
|
||||
connection_id: 'c-github-1',
|
||||
});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const triggers = (result as { triggers?: unknown[] })?.triggers ?? [];
|
||||
const slugs = (triggers as { slug?: string }[]).map(t => t.slug);
|
||||
expect(slugs).toContain('GITHUB_COMMIT_EVENT');
|
||||
console.log(`${LOG} PASS: trigger catalog contains GITHUB_COMMIT_EVENT`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-github-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
// App must remain responsive — skills page should not be blank
|
||||
const alive = await textExists(CONNECTOR_NAME);
|
||||
expect(alive).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed connection does not show blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-github-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) {
|
||||
await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
} else {
|
||||
console.log(`${LOG} modal not opened for expired state — asserting session only`);
|
||||
}
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
// Inject a fault that returns 400 on execute (simulates a scoped 4xx)
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-github-1',
|
||||
action: 'GITHUB_LIST_REPOS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class composio error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-github-1');
|
||||
clearRequestLog();
|
||||
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-github-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE to mock`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* E2E: Gmail (Composio) connector flow.
|
||||
*
|
||||
* Covers the standard lifecycle plus a regression test for
|
||||
* GMAIL_FETCH_EMAILS returning 400 (#1296) — the app must show a
|
||||
* user-friendly error, not crash or blank screen.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
setMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorGmailComposioE2E]';
|
||||
const CONNECTOR_NAME = 'Gmail';
|
||||
const TOOLKIT_SLUG = 'gmail';
|
||||
const AUTH_TOKEN = 'e2e-connector-gmail-composio-token';
|
||||
|
||||
describe('Gmail (Composio) connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gmail-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gmail-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(
|
||||
r => r.method === 'POST' && r.url.includes('/agent-integrations/composio/authorize')
|
||||
);
|
||||
expect(authReq).toBeDefined();
|
||||
const body = JSON.parse(authReq?.body || '{}');
|
||||
expect(body.toolkit).toBe(TOOLKIT_SLUG);
|
||||
console.log(`${LOG} PASS: auth/connect routed correctly`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gmail-1');
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const log = getRequestLog();
|
||||
const syncReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: composio_sync routed (status ${syncReq?.statusCode})`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gmail-1',
|
||||
action: 'GMAIL_FETCH_EMAILS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: composio_execute routed`);
|
||||
});
|
||||
|
||||
it('GMAIL_FETCH_EMAILS returning 400 shows user-friendly error, not blank screen (#1296)', async function () {
|
||||
this.timeout(60_000);
|
||||
// Inject a 400 response on execute
|
||||
setMockBehavior('composioExecuteFails', '1');
|
||||
clearRequestLog();
|
||||
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gmail-1',
|
||||
action: 'GMAIL_FETCH_EMAILS',
|
||||
params: {},
|
||||
});
|
||||
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
if (execReq) {
|
||||
// The mock returns 400 — the RPC layer should surface a safe error, not crash
|
||||
console.log(`${LOG} execute returned status: ${execReq.statusCode}`);
|
||||
}
|
||||
|
||||
// Critical: app must remain responsive — session not nuked
|
||||
await assertSessionNotNuked();
|
||||
|
||||
// Navigate to skills; the page must not be blank
|
||||
await navigateToSkills();
|
||||
const gmailVisible = await textExists(CONNECTOR_NAME);
|
||||
expect(gmailVisible).toBe(true);
|
||||
console.log(`${LOG} PASS: 400 on GMAIL_FETCH_EMAILS does not blank the screen`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-gmail-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const alive = await textExists(CONNECTOR_NAME);
|
||||
expect(alive).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-gmail-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) {
|
||||
await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
}
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gmail-1',
|
||||
action: 'GMAIL_FETCH_EMAILS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gmail-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-gmail-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* E2E: Google Calendar (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorGoogleCalendarE2E]';
|
||||
const CONNECTOR_NAME = 'Google Calendar';
|
||||
const TOOLKIT_SLUG = 'googlecalendar';
|
||||
const AUTH_TOKEN = 'e2e-connector-googlecalendar-token';
|
||||
|
||||
describe('Google Calendar Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gcal-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gcal-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(
|
||||
r => r.method === 'POST' && r.url.includes('/agent-integrations/composio/authorize')
|
||||
);
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed correctly`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gcal-1');
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gcal-1',
|
||||
action: 'GOOGLECALENDAR_LIST_EVENTS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-gcal-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-gcal-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) {
|
||||
await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
}
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gcal-1',
|
||||
action: 'GOOGLECALENDAR_LIST_EVENTS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gcal-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-gcal-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* E2E: Google Drive (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorGoogleDriveE2E]';
|
||||
const CONNECTOR_NAME = 'Google Drive';
|
||||
const TOOLKIT_SLUG = 'googledrive';
|
||||
const AUTH_TOKEN = 'e2e-connector-googledrive-token';
|
||||
|
||||
describe('Google Drive Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gdrive-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gdrive-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gdrive-1',
|
||||
action: 'GOOGLEDRIVE_LIST_FILES',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-gdrive-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-gdrive-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gdrive-1',
|
||||
action: 'GOOGLEDRIVE_LIST_FILES',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gdrive-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-gdrive-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* E2E: Google Sheets (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorGoogleSheetsE2E]';
|
||||
const CONNECTOR_NAME = 'Google Sheets';
|
||||
const TOOLKIT_SLUG = 'googlesheets';
|
||||
const AUTH_TOKEN = 'e2e-connector-googlesheets-token';
|
||||
|
||||
describe('Google Sheets Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gsheets-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gsheets-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gsheets-1',
|
||||
action: 'GOOGLESHEETS_LIST_SPREADSHEETS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-gsheets-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-gsheets-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-gsheets-1',
|
||||
action: 'GOOGLESHEETS_LIST_SPREADSHEETS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-gsheets-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-gsheets-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* E2E: Jira (Composio) connector flow.
|
||||
*
|
||||
* Includes an extra test verifying the subdomain required-field validation:
|
||||
* the Connect button must be disabled (or show an inline error) when no
|
||||
* valid Atlassian subdomain is entered.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
setMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorJiraE2E]';
|
||||
const CONNECTOR_NAME = 'Jira';
|
||||
const TOOLKIT_SLUG = 'jira';
|
||||
const AUTH_TOKEN = 'e2e-connector-jira-token';
|
||||
|
||||
describe('Jira Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-jira-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-jira-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
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');
|
||||
setMockBehavior('composioConnections', JSON.stringify([]));
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
expect(modal).toBeTruthy();
|
||||
// The Jira connect modal should render a subdomain input per toolkitRequiredFields.ts
|
||||
// It uses data-testid="composio-required-subdomain"
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
const hasSubdomainInput = await browser
|
||||
.execute(() => {
|
||||
return (
|
||||
document.querySelector('[data-testid="composio-required-subdomain"]') !== null ||
|
||||
document.querySelector('input[placeholder*="subdomain"]') !== null ||
|
||||
// fallback: any .atlassian.net suffix label
|
||||
Array.from(document.querySelectorAll('*')).some(el =>
|
||||
(el.textContent ?? '').includes('.atlassian.net')
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(() => false);
|
||||
expect(hasSubdomainInput).toBe(true);
|
||||
console.log(`${LOG} PASS: subdomain input field visible in Jira modal`);
|
||||
// Close modal by pressing Escape
|
||||
// @ts-expect-error -- browser global is injected by WDIO at runtime, not typed in this env
|
||||
await browser.keys(['Escape']).catch(() => {});
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
|
||||
it('auth/connect flow with subdomain extra_params routes correctly', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', {
|
||||
toolkit: TOOLKIT_SLUG,
|
||||
extra_params: { subdomain: 'myteam' },
|
||||
});
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
const body = JSON.parse(authReq?.body || '{}');
|
||||
expect(body.toolkit).toBe(TOOLKIT_SLUG);
|
||||
console.log(`${LOG} PASS: authorize with subdomain extra_params routed correctly`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-jira-1');
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-jira-1',
|
||||
action: 'JIRA_LIST_ISSUES',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-jira-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-jira-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-jira-1',
|
||||
action: 'JIRA_LIST_ISSUES',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-jira-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-jira-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* E2E: Notion (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorNotionE2E]';
|
||||
const CONNECTOR_NAME = 'Notion';
|
||||
const TOOLKIT_SLUG = 'notion';
|
||||
const AUTH_TOKEN = 'e2e-connector-notion-token';
|
||||
|
||||
describe('Notion Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-notion-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-notion-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-notion-1',
|
||||
action: 'NOTION_LIST_PAGES',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-notion-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-notion-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-notion-1',
|
||||
action: 'NOTION_LIST_PAGES',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-notion-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-notion-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* E2E: Cross-cutting session guard for Composio connector routes.
|
||||
*
|
||||
* Regression coverage for:
|
||||
* #2286 — a 401 on any /agent-integrations/composio/* route must NOT clear
|
||||
* the user session / log the user out.
|
||||
* #2285 — clicking a connector card in a degraded state must NOT log user out.
|
||||
*
|
||||
* These tests exercise the fault-injection paths against multiple toolkits
|
||||
* and multiple error scenarios to ensure the session-guard holds broadly, not
|
||||
* just for a single connector.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import { waitForWebView, waitForWindowVisible } from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
resetMockBehavior,
|
||||
setMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorSessionGuardE2E]';
|
||||
const AUTH_TOKEN = 'e2e-connector-session-guard-token';
|
||||
|
||||
// Toolkits tested in the cross-cutting sweep
|
||||
const GUARD_TOOLKITS = ['github', 'gmail', 'slack', 'notion', 'discord'];
|
||||
|
||||
describe('Composio connector session guard (cross-cutting, #2286)', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits(GUARD_TOOLKITS);
|
||||
// Seed all toolkits as ACTIVE
|
||||
setMockBehavior(
|
||||
'composioConnections',
|
||||
JSON.stringify(
|
||||
GUARD_TOOLKITS.map((slug, i) => ({ id: `c-guard-${i}`, toolkit: slug, status: 'ACTIVE' }))
|
||||
)
|
||||
);
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits(GUARD_TOOLKITS);
|
||||
setMockBehavior(
|
||||
'composioConnections',
|
||||
JSON.stringify(
|
||||
GUARD_TOOLKITS.map((slug, i) => ({ id: `c-guard-${i}`, toolkit: slug, status: 'ACTIVE' }))
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('400 on composio/execute does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
|
||||
// Fire execute against every guard toolkit
|
||||
for (const slug of GUARD_TOOLKITS) {
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: `c-guard-${GUARD_TOOLKITS.indexOf(slug)}`,
|
||||
action: `${slug.toUpperCase()}_TEST_ACTION`,
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
// Session must survive all of these
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 400 on execute does not log user out for any toolkit`);
|
||||
});
|
||||
|
||||
it('500 on composio/execute does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(500);
|
||||
|
||||
for (const slug of GUARD_TOOLKITS) {
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: `c-guard-${GUARD_TOOLKITS.indexOf(slug)}`,
|
||||
action: `${slug.toUpperCase()}_TEST_ACTION`,
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 500 on execute does not log user out for any toolkit`);
|
||||
});
|
||||
|
||||
it('500 on composio/connections delete does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
setMockBehavior('composioDeleteFails', '1');
|
||||
|
||||
for (const slug of GUARD_TOOLKITS) {
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: `c-guard-${GUARD_TOOLKITS.indexOf(slug)}`,
|
||||
});
|
||||
}
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 500 on delete does not log user out`);
|
||||
});
|
||||
|
||||
it('500 on composio/sync does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
setMockBehavior('composioSyncFails', '1');
|
||||
|
||||
for (const slug of GUARD_TOOLKITS) {
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: slug });
|
||||
}
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 500 on sync does not log user out`);
|
||||
});
|
||||
|
||||
it('navigating to Skills page with FAILED connections does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
// Set all connections as FAILED
|
||||
setMockBehavior(
|
||||
'composioConnections',
|
||||
JSON.stringify(
|
||||
GUARD_TOOLKITS.map((slug, i) => ({ id: `c-guard-${i}`, toolkit: slug, status: 'FAILED' }))
|
||||
)
|
||||
);
|
||||
|
||||
await navigateToSkills();
|
||||
await waitForWebView(15_000);
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: FAILED connections on Skills page do not log user out`);
|
||||
});
|
||||
|
||||
it('navigating to Skills page with EXPIRED connections does NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
setMockBehavior(
|
||||
'composioConnections',
|
||||
JSON.stringify(
|
||||
GUARD_TOOLKITS.map((slug, i) => ({ id: `c-guard-${i}`, toolkit: slug, status: 'EXPIRED' }))
|
||||
)
|
||||
);
|
||||
|
||||
await navigateToSkills();
|
||||
await waitForWebView(15_000);
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: EXPIRED connections on Skills page do not log user out`);
|
||||
});
|
||||
|
||||
it('rapid authorize failures across toolkits do NOT log user out (#2286)', async function () {
|
||||
this.timeout(60_000);
|
||||
// Make authorize return 400 (via execute fault — authorize itself doesn't
|
||||
// have a fault knob but the pattern is the same at the session layer)
|
||||
setMockBehavior('composioExecuteFails', '1');
|
||||
setMockBehavior('composioDeleteFails', '1');
|
||||
|
||||
for (const slug of GUARD_TOOLKITS) {
|
||||
await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: slug });
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: `c-guard-${GUARD_TOOLKITS.indexOf(slug)}`,
|
||||
action: `${slug.toUpperCase()}_TEST_ACTION`,
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: rapid failures across toolkits do not log user out`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* E2E: Slack (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorSlackComposioE2E]';
|
||||
const CONNECTOR_NAME = 'Slack';
|
||||
const TOOLKIT_SLUG = 'slack';
|
||||
const AUTH_TOKEN = 'e2e-connector-slack-composio-token';
|
||||
|
||||
describe('Slack (Composio) connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-slack-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-slack-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-slack-1',
|
||||
action: 'SLACK_LIST_CHANNELS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-slack-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-slack-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-slack-1',
|
||||
action: 'SLACK_LIST_CHANNELS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-slack-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', { connection_id: 'c-slack-1' });
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* E2E: Todoist (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorTodoistE2E]';
|
||||
const CONNECTOR_NAME = 'Todoist';
|
||||
const TOOLKIT_SLUG = 'todoist';
|
||||
const AUTH_TOKEN = 'e2e-connector-todoist-token';
|
||||
|
||||
describe('Todoist Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-todoist-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-todoist-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-todoist-1',
|
||||
action: 'TODOIST_LIST_PROJECTS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-todoist-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-todoist-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
if (modal) await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-todoist-1',
|
||||
action: 'TODOIST_LIST_PROJECTS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-todoist-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-todoist-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* E2E: YouTube (Composio) connector flow.
|
||||
*/
|
||||
import { waitForApp } from '../helpers/app-helpers';
|
||||
import {
|
||||
assertConnectorCardVisible,
|
||||
assertModalPhase,
|
||||
assertSessionNotNuked,
|
||||
injectComposioFault,
|
||||
openConnectorModal,
|
||||
seedComposioConnection,
|
||||
seedComposioToolkits,
|
||||
} from '../helpers/composio-helpers';
|
||||
import { callOpenhumanRpc } from '../helpers/core-rpc';
|
||||
import { triggerAuthDeepLinkBypass } from '../helpers/deep-link-helpers';
|
||||
import {
|
||||
textExists,
|
||||
waitForText,
|
||||
waitForWebView,
|
||||
waitForWindowVisible,
|
||||
} from '../helpers/element-helpers';
|
||||
import { completeOnboardingIfVisible, navigateToSkills } from '../helpers/shared-flows';
|
||||
import {
|
||||
clearRequestLog,
|
||||
getRequestLog,
|
||||
resetMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from '../mock-server';
|
||||
|
||||
const LOG = '[ConnectorYouTubeE2E]';
|
||||
const CONNECTOR_NAME = 'YouTube';
|
||||
const TOOLKIT_SLUG = 'youtube';
|
||||
const AUTH_TOKEN = 'e2e-connector-youtube-token';
|
||||
|
||||
describe('YouTube Composio connector flow', () => {
|
||||
before(async function () {
|
||||
this.timeout(90_000);
|
||||
await startMockServer();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-youtube-1');
|
||||
await waitForApp();
|
||||
clearRequestLog();
|
||||
await triggerAuthDeepLinkBypass(AUTH_TOKEN);
|
||||
await waitForWindowVisible(25_000);
|
||||
await waitForWebView(15_000);
|
||||
await completeOnboardingIfVisible(LOG);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
resetMockBehavior();
|
||||
seedComposioToolkits([TOOLKIT_SLUG]);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-youtube-1');
|
||||
});
|
||||
|
||||
it('card is visible and selectable', async function () {
|
||||
this.timeout(60_000);
|
||||
await assertConnectorCardVisible(CONNECTOR_NAME);
|
||||
console.log(`${LOG} PASS: card visible`);
|
||||
});
|
||||
|
||||
it('auth/connect flow succeeds with mocked backend', async function () {
|
||||
this.timeout(60_000);
|
||||
clearRequestLog();
|
||||
const out = await callOpenhumanRpc('openhuman.composio_authorize', { toolkit: TOOLKIT_SLUG });
|
||||
expect(out.ok).toBe(true);
|
||||
const log = getRequestLog();
|
||||
const authReq = log.find(r => r.method === 'POST' && r.url.includes('/composio/authorize'));
|
||||
expect(authReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: auth/connect routed`);
|
||||
});
|
||||
|
||||
it('connected state persists after reconnect/reload', async function () {
|
||||
this.timeout(60_000);
|
||||
const out = await callOpenhumanRpc('openhuman.composio_list_connections', {});
|
||||
expect(out.ok).toBe(true);
|
||||
const result = (out.result as { result?: unknown })?.result ?? out.result;
|
||||
const connections = (result as { connections?: unknown[] })?.connections ?? [];
|
||||
const hit = (connections as { toolkit?: string; status?: string }[]).find(
|
||||
c => c.toolkit?.toLowerCase() === TOOLKIT_SLUG
|
||||
);
|
||||
expect(hit).toBeDefined();
|
||||
expect(hit?.status).toBe('ACTIVE');
|
||||
console.log(`${LOG} PASS: connected state persists`);
|
||||
});
|
||||
|
||||
it('composio_sync RPC routes to mock backend', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG });
|
||||
const syncLog = getRequestLog();
|
||||
const syncReq = syncLog.find(r => r.method === 'POST' && r.url.includes('/composio/sync'));
|
||||
expect(syncReq).toBeDefined();
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: sync does not nuke session`);
|
||||
});
|
||||
|
||||
it('composio_execute routes a basic task', async function () {
|
||||
this.timeout(30_000);
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-youtube-1',
|
||||
action: 'YOUTUBE_LIST_PLAYLISTS',
|
||||
params: {},
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const execReq = log.find(r => r.url.includes('/composio/execute'));
|
||||
expect(execReq).toBeDefined();
|
||||
expect(execReq!.method).toBe('POST');
|
||||
console.log(`${LOG} PASS: execute routed`);
|
||||
});
|
||||
|
||||
it('failed connection shows error state, not blank screen', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'FAILED', 'c-youtube-fail');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
expect(await textExists(CONNECTOR_NAME)).toBe(true);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: failed state does not blank screen`);
|
||||
});
|
||||
|
||||
it('expired auth shows Reconnect button and does not log user out', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-youtube-expired');
|
||||
await navigateToSkills();
|
||||
await waitForText(CONNECTOR_NAME, 10_000);
|
||||
const modal = await openConnectorModal(CONNECTOR_NAME);
|
||||
expect(modal).toBeTruthy();
|
||||
await assertModalPhase('expired', CONNECTOR_NAME);
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: expired auth does not log user out`);
|
||||
});
|
||||
|
||||
it('unrelated 401 on composio route does not nuke session', async function () {
|
||||
this.timeout(60_000);
|
||||
injectComposioFault(400);
|
||||
await callOpenhumanRpc('openhuman.composio_execute', {
|
||||
connection_id: 'c-youtube-1',
|
||||
action: 'YOUTUBE_LIST_PLAYLISTS',
|
||||
params: {},
|
||||
});
|
||||
await assertSessionNotNuked();
|
||||
console.log(`${LOG} PASS: 401-class error does not nuke session`);
|
||||
});
|
||||
|
||||
it('disconnect flow removes connection', async function () {
|
||||
this.timeout(60_000);
|
||||
seedComposioConnection(TOOLKIT_SLUG, 'ACTIVE', 'c-youtube-1');
|
||||
clearRequestLog();
|
||||
await callOpenhumanRpc('openhuman.composio_delete_connection', {
|
||||
connection_id: 'c-youtube-1',
|
||||
});
|
||||
const log = getRequestLog();
|
||||
const deleteReq = log.find(
|
||||
r => r.method === 'DELETE' && r.url.includes('/composio/connections/')
|
||||
);
|
||||
expect(deleteReq).toBeDefined();
|
||||
console.log(`${LOG} PASS: disconnect routed DELETE`);
|
||||
await assertSessionNotNuked();
|
||||
});
|
||||
});
|
||||
@@ -161,7 +161,13 @@ export function handleIntegrations(ctx) {
|
||||
const connections = parseBehaviorJson("composioConnections", [
|
||||
{ id: "c1", toolkit: "gmail", status: "ACTIVE" },
|
||||
]);
|
||||
json(res, 200, { success: true, data: { connections } });
|
||||
// Apply per-toolkit status overrides via composioConnectionStatus_<slug>
|
||||
const overridden = connections.map((c) => {
|
||||
const statusKey = `composioConnectionStatus_${c.toolkit}`;
|
||||
const overrideStatus = mockBehavior[statusKey];
|
||||
return overrideStatus ? { ...c, status: overrideStatus } : c;
|
||||
});
|
||||
json(res, 200, { success: true, data: { connections: overridden } });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -305,6 +311,39 @@ export function handleIntegrations(ctx) {
|
||||
: typeof parsedBody?.tool === "string"
|
||||
? parsedBody.tool
|
||||
: "";
|
||||
// composioExecuteFails → inject error response
|
||||
// Knob values: '400' or '1' → HTTP 400; '500' → HTTP 500
|
||||
if (mockBehavior.composioExecuteFails === "400" || mockBehavior.composioExecuteFails === "1") {
|
||||
json(res, 400, {
|
||||
success: false,
|
||||
error: "Mock execute failure",
|
||||
data: { successful: false, data: null, error: "Mock execute failure" },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (mockBehavior.composioExecuteFails === "500") {
|
||||
json(res, 500, {
|
||||
success: false,
|
||||
error: "Mock execute server error",
|
||||
data: { successful: false, data: null, error: "Mock execute server error" },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Per-action override: composioExecuteResponse_<ACTION>
|
||||
const actionKey = `composioExecuteResponse_${action}`;
|
||||
if (mockBehavior[actionKey]) {
|
||||
let overrideData;
|
||||
try {
|
||||
overrideData = JSON.parse(mockBehavior[actionKey]);
|
||||
} catch {
|
||||
overrideData = { ok: true };
|
||||
}
|
||||
json(res, 200, {
|
||||
success: true,
|
||||
data: { successful: true, data: overrideData, error: null },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const data =
|
||||
action === "GMAIL_FETCH_EMAILS"
|
||||
? {
|
||||
@@ -324,6 +363,80 @@ export function handleIntegrations(ctx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Composio connection delete ─────────────────────────────
|
||||
if (
|
||||
method === "DELETE" &&
|
||||
/^\/agent-integrations\/composio\/connections\/[^/]+\/?$/.test(url)
|
||||
) {
|
||||
if (mockBehavior.composioDeleteFails === "400") {
|
||||
json(res, 400, { success: false, error: "Mock connection delete failure" });
|
||||
return true;
|
||||
}
|
||||
if (mockBehavior.composioDeleteFails === "500" || mockBehavior.composioDeleteFails === "1") {
|
||||
json(res, 500, { success: false, error: "Mock connection delete failure" });
|
||||
return true;
|
||||
}
|
||||
let connId = url.split("/").filter(Boolean).pop() ?? "";
|
||||
connId = connId.split("?")[0];
|
||||
try {
|
||||
connId = decodeURIComponent(connId);
|
||||
} catch {
|
||||
json(res, 400, { success: false, error: "Invalid connection id encoding" });
|
||||
return true;
|
||||
}
|
||||
// Remove the connection from the seeded list if present
|
||||
const conns = parseBehaviorJson("composioConnections", [
|
||||
{ id: "c1", toolkit: "gmail", status: "ACTIVE" },
|
||||
]);
|
||||
const next = conns.filter((c) => c.id !== connId);
|
||||
const deleted = next.length !== conns.length;
|
||||
setMockBehavior("composioConnections", JSON.stringify(next));
|
||||
json(res, 200, { success: true, data: { deleted } });
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Composio sync ──────────────────────────────────────────
|
||||
if (
|
||||
method === "POST" &&
|
||||
/^\/agent-integrations\/composio\/sync\/?$/.test(url)
|
||||
) {
|
||||
if (mockBehavior.composioSyncFails === "400") {
|
||||
json(res, 400, { success: false, error: "Mock sync failure" });
|
||||
return true;
|
||||
}
|
||||
if (mockBehavior.composioSyncFails === "500" || mockBehavior.composioSyncFails === "1") {
|
||||
json(res, 500, { success: false, error: "Mock sync failure" });
|
||||
return true;
|
||||
}
|
||||
json(res, 200, { success: true, data: { items_synced: 3 } });
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Composio user-scopes ───────────────────────────────────
|
||||
if (
|
||||
method === "GET" &&
|
||||
/^\/agent-integrations\/composio\/user-scopes\/?(\?.*)?$/.test(url)
|
||||
) {
|
||||
const scopes = parseBehaviorJson("composioUserScopes", {
|
||||
read: true,
|
||||
write: true,
|
||||
admin: false,
|
||||
});
|
||||
json(res, 200, { success: true, data: scopes });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
method === "POST" &&
|
||||
/^\/agent-integrations\/composio\/user-scopes\/?$/.test(url)
|
||||
) {
|
||||
// Echo back the posted preferences and persist them as the new scopes
|
||||
const pref = parsedBody ?? {};
|
||||
setMockBehavior("composioUserScopes", JSON.stringify(pref));
|
||||
json(res, 200, { success: true, data: pref });
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Apify ──────────────────────────────────────────────────
|
||||
// Gap fill — minimal stubs for run polling.
|
||||
const apifyMatch = url.match(
|
||||
|
||||
@@ -9,10 +9,11 @@ use std::path::PathBuf;
|
||||
/// Standard model identifiers matching the backend model registry.
|
||||
pub const MODEL_AGENTIC_V1: &str = "agentic-v1";
|
||||
pub const MODEL_REASONING_V1: &str = "reasoning-v1";
|
||||
/// Conversational tier — the orchestrator (user-facing chat agent) rides on
|
||||
/// this by default. Backend maps it to Kimi K2.6 Turbo on Fireworks (128k
|
||||
/// context, `supportsThinking: false`) — tuned for time-to-first-token so
|
||||
/// chat responses feel snappy.
|
||||
/// Conversational tier (deprecated — retired from the backend strict model
|
||||
/// registry in migration 2→3). Do not use for new sessions; the backend now
|
||||
/// returns 400 for threads that send `chat-v1`. Retained here only for
|
||||
/// migration code that needs to identify and replace the old model identifier.
|
||||
/// Use [`MODEL_REASONING_QUICK_V1`] or [`DEFAULT_MODEL`] instead.
|
||||
pub const MODEL_CHAT_V1: &str = "chat-v1";
|
||||
/// Low-latency chat tier. Backend maps this to Kimi K2.6 Turbo on
|
||||
/// Fireworks (128k context, `supportsThinking: false`) — tuned for
|
||||
|
||||
Reference in New Issue
Block a user