mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 21:44:38 +00:00
206 lines
7.5 KiB
TypeScript
206 lines
7.5 KiB
TypeScript
/**
|
|
* 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();
|
|
});
|
|
});
|