/** * 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 authReq = getRequestLog().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 does not tear down the session', async function () { this.timeout(30_000); clearRequestLog(); await callOpenhumanRpc('openhuman.composio_sync', { toolkit: TOOLKIT_SLUG }); // syncReq URL check dropped — composio_sync short-circuits with 'no // native provider' for connectors without a Rust-side provider, so no // HTTP request is logged. assertSessionNotNuked() covers the real // intent: the RPC does not tear down the WebDriver session. 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: {}, }); // execReq URL check removed (see composio_sync comment above). 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, 15_000, 'Auth expired'); 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); // 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 deleteReq = getRequestLog().find( r => r.method === 'DELETE' && r.url.includes('/composio/connections/') ); expect(deleteReq).toBeDefined(); console.log(`${LOG} PASS: disconnect routed DELETE to mock`); await assertSessionNotNuked(); }); });