From 3c292b23474ff57526952653a34a676716552d96 Mon Sep 17 00:00:00 2001
From: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
Date: Sat, 2 May 2026 12:58:27 -0700
Subject: [PATCH] Add full Composio managed-auth toolkit catalog (#1093)
---
.../composio/ComposioConnectModal.tsx | 23 +-
.../components/composio/toolkitMeta.test.tsx | 34 ++
app/src/components/composio/toolkitMeta.tsx | 530 ++++++++++--------
.../Skills.composio-catalog.test.tsx | 6 +-
...ls.third-party-notion-debug-tools.test.tsx | 3 +-
app/src/test/setup.ts | 46 ++
6 files changed, 389 insertions(+), 253 deletions(-)
create mode 100644 app/src/components/composio/toolkitMeta.test.tsx
diff --git a/app/src/components/composio/ComposioConnectModal.tsx b/app/src/components/composio/ComposioConnectModal.tsx
index bcf6a6926..36c848a4b 100644
--- a/app/src/components/composio/ComposioConnectModal.tsx
+++ b/app/src/components/composio/ComposioConnectModal.tsx
@@ -318,6 +318,9 @@ export default function ComposioConnectModal({
composio
+
+ managed auth
+
{toolkit.description}
@@ -343,9 +346,21 @@ export default function ComposioConnectModal({
{phase === 'idle' && (
<>
- Connect your {toolkit.name} account. We will open a browser window where you can
- grant access, and then this app will detect the connection automatically.
+ Connect your {toolkit.name} account through Composio's hosted authorization
+ flow. We'll open a browser window, you approve access there, and this app will
+ detect the connection automatically.
+
+
+ Provider access
+
+
+ {toolkit.name} can expose{' '}
+ {toolkit.permissionLabel} . After you connect,
+ OpenHuman's own agent permissions are controlled below as read, write, and
+ admin toggles.
+
+
void handleConnect()}
@@ -478,9 +493,9 @@ function ScopeToggles({ scopes, savingScope, onToggle, error }: ScopeTogglesProp
- Agent permissions
+ OpenHuman agent permissions
-
Read+Write enabled by default
+
Read + Write enabled by default
{SCOPE_ROWS.map(row => {
diff --git a/app/src/components/composio/toolkitMeta.test.tsx b/app/src/components/composio/toolkitMeta.test.tsx
new file mode 100644
index 000000000..2129baca7
--- /dev/null
+++ b/app/src/components/composio/toolkitMeta.test.tsx
@@ -0,0 +1,34 @@
+import { describe, expect, it } from 'vitest';
+
+import { composioToolkitMeta, KNOWN_COMPOSIO_TOOLKITS } from './toolkitMeta';
+
+describe('composioToolkitMeta', () => {
+ it('ships the full Composio managed-auth catalog fallback', () => {
+ expect(KNOWN_COMPOSIO_TOOLKITS).toHaveLength(118);
+ expect(KNOWN_COMPOSIO_TOOLKITS).toContain('gmail');
+ expect(KNOWN_COMPOSIO_TOOLKITS).toContain('discord');
+ expect(KNOWN_COMPOSIO_TOOLKITS).toContain('supabase');
+ expect(KNOWN_COMPOSIO_TOOLKITS).toContain('zoom');
+ });
+
+ it('preserves canonical names for managed-auth toolkits and renders logo URLs', () => {
+ const gmail = composioToolkitMeta('gmail');
+ const calendar = composioToolkitMeta('google_calendar');
+
+ expect(gmail.name).toBe('Gmail');
+ expect(gmail.logoUrl).toContain('/gmail');
+ expect(gmail.permissionLabel).toBe('Docs, files, tasks, and workspace data');
+
+ expect(calendar.slug).toBe('googlecalendar');
+ expect(calendar.name).toBe('Google Calendar');
+ expect(calendar.logoUrl).toContain('/googlecalendar');
+ });
+
+ it('falls back cleanly for unknown slugs', () => {
+ const meta = composioToolkitMeta('my_custom_toolkit');
+
+ expect(meta.slug).toBe('my_custom_toolkit');
+ expect(meta.name).toBe('My Custom Toolkit');
+ expect(meta.logoUrl).toContain('/my_custom_toolkit');
+ });
+});
diff --git a/app/src/components/composio/toolkitMeta.tsx b/app/src/components/composio/toolkitMeta.tsx
index e7f1f51d7..e5b17c7d9 100644
--- a/app/src/components/composio/toolkitMeta.tsx
+++ b/app/src/components/composio/toolkitMeta.tsx
@@ -1,33 +1,20 @@
/**
* Display metadata for Composio toolkits shown in the Skills grid.
*
- * The backend allowlist (`GET /agent-integrations/composio/toolkits`)
- * returns plain toolkit slugs. This table turns each slug into a
- * humanised name, description, category, and icon so the
- * `UnifiedSkillCard` can render them next to regular skills without
- * special-casing.
+ * We intentionally keep a local catalog of every Composio managed-auth
+ * toolkit so the desktop UI can render a broad connection surface even
+ * before the live backend allowlist expands further. The live toolkit
+ * list still wins for runtime availability; this file provides stable
+ * names, categories, descriptions, and logos for rendering.
*
- * Unknown slugs fall back to a generic entry with a title-cased name —
- * new backend toolkits will still render, just without custom copy.
+ * Source of truth for the managed-auth list:
+ * https://docs.composio.dev/toolkits/managed-auth (118 toolkits as of
+ * May 1, 2026).
*/
-import type { ReactNode } from 'react';
-import {
- SiFacebook,
- SiGithub,
- SiGmail,
- SiGooglecalendar,
- SiGoogledrive,
- SiGooglesheets,
- SiInstagram,
- SiLinear,
- SiNotion,
- SiReddit,
- SiSlack,
-} from 'react-icons/si';
+import { type ReactNode, useState } from 'react';
import { canonicalizeComposioToolkitSlug } from '../../lib/composio/toolkitSlug';
import type { SkillCategory } from '../skills/skillCategories';
-import { SkillIconBadge } from '../skills/skillIcons';
export interface ComposioToolkitMeta {
/** Toolkit slug as returned by the backend, e.g. `"gmail"`. */
@@ -40,128 +27,203 @@ export interface ComposioToolkitMeta {
category: SkillCategory;
/** Small branded icon rendered on the card and connect modal. */
icon: ReactNode;
+ /** Composio-hosted logo URL for richer provider branding. */
+ logoUrl: string;
+ /** Short UX hint for what the user is authorizing. */
+ permissionLabel: string;
}
-function GmailIcon() {
- return (
-
- );
+interface ManagedToolkitEntry {
+ slug: string;
+ name: string;
}
-function GoogleCalendarIcon() {
- return (
-
- );
-}
+const MANAGED_COMPOSIO_TOOLKITS: readonly ManagedToolkitEntry[] = Object.freeze([
+ { slug: 'airtable', name: 'Airtable' },
+ { slug: 'apaleo', name: 'Apaleo' },
+ { slug: 'asana', name: 'Asana' },
+ { slug: 'attio', name: 'Attio' },
+ { slug: 'basecamp', name: 'Basecamp' },
+ { slug: 'bitbucket', name: 'Bitbucket' },
+ { slug: 'blackbaud', name: 'Blackbaud' },
+ { slug: 'boldsign', name: 'Boldsign' },
+ { slug: 'box', name: 'Box' },
+ { slug: 'cal', name: 'Cal' },
+ { slug: 'calendly', name: 'Calendly' },
+ { slug: 'canva', name: 'Canva' },
+ { slug: 'capsule_crm', name: 'Capsule CRM' },
+ { slug: 'clickup', name: 'ClickUp' },
+ { slug: 'confluence', name: 'Confluence' },
+ { slug: 'contentful', name: 'Contentful' },
+ { slug: 'convex', name: 'Convex' },
+ { slug: 'crowdin', name: 'Crowdin' },
+ { slug: 'dart', name: 'Dart' },
+ { slug: 'dialpad', name: 'Dialpad' },
+ { slug: 'digital_ocean', name: 'DigitalOcean' },
+ { slug: 'discord', name: 'Discord' },
+ { slug: 'discordbot', name: 'Discord Bot' },
+ { slug: 'dropbox', name: 'Dropbox' },
+ { slug: 'dub', name: 'Dub' },
+ { slug: 'dynamics365', name: 'Dynamics 365' },
+ { slug: 'eventbrite', name: 'Eventbrite' },
+ { slug: 'excel', name: 'Excel' },
+ { slug: 'exist', name: 'Exist' },
+ { slug: 'facebook', name: 'Facebook' },
+ { slug: 'fathom', name: 'Fathom' },
+ { slug: 'figma', name: 'Figma' },
+ { slug: 'freeagent', name: 'Freeagent' },
+ { slug: 'freshbooks', name: 'FreshBooks' },
+ { slug: 'github', name: 'GitHub' },
+ { slug: 'gitlab', name: 'GitLab' },
+ { slug: 'gmail', name: 'Gmail' },
+ { slug: 'googleads', name: 'Google Ads' },
+ { slug: 'google_analytics', name: 'Google Analytics' },
+ { slug: 'googlebigquery', name: 'Google BigQuery' },
+ { slug: 'googlecalendar', name: 'Google Calendar' },
+ { slug: 'google_classroom', name: 'Google Classroom' },
+ { slug: 'googledocs', name: 'Google Docs' },
+ { slug: 'googledrive', name: 'Google Drive' },
+ { slug: 'google_maps', name: 'Google Maps' },
+ { slug: 'googlemeet', name: 'Google Meet' },
+ { slug: 'googlephotos', name: 'Google Photos' },
+ { slug: 'google_search_console', name: 'Google Search Console' },
+ { slug: 'googlesheets', name: 'Google Sheets' },
+ { slug: 'googleslides', name: 'Google Slides' },
+ { slug: 'googlesuper', name: 'Google Super' },
+ { slug: 'googletasks', name: 'Google Tasks' },
+ { slug: 'gorgias', name: 'Gorgias' },
+ { slug: 'gumroad', name: 'Gumroad' },
+ { slug: 'harvest', name: 'Harvest' },
+ { slug: 'hubspot', name: 'HubSpot' },
+ { slug: 'hugging_face', name: 'Hugging Face' },
+ { slug: 'instagram', name: 'Instagram' },
+ { slug: 'intercom', name: 'Intercom' },
+ { slug: 'jira', name: 'Jira' },
+ { slug: 'kit', name: 'Kit' },
+ { slug: 'linear', name: 'Linear' },
+ { slug: 'linkedin', name: 'LinkedIn' },
+ { slug: 'linkhut', name: 'Linkhut' },
+ { slug: 'mailchimp', name: 'Mailchimp' },
+ { slug: 'microsoft_teams', name: 'Microsoft Teams' },
+ { slug: 'miro', name: 'Miro' },
+ { slug: 'monday', name: 'Monday' },
+ { slug: 'moneybird', name: 'Moneybird' },
+ { slug: 'mural', name: 'Mural' },
+ { slug: 'notion', name: 'Notion' },
+ { slug: 'omnisend', name: 'Omnisend' },
+ { slug: 'one_drive', name: 'OneDrive' },
+ { slug: 'outlook', name: 'Outlook' },
+ { slug: 'pagerduty', name: 'PagerDuty' },
+ { slug: 'prisma', name: 'Prisma' },
+ { slug: 'productboard', name: 'Productboard' },
+ { slug: 'pushbullet', name: 'Pushbullet' },
+ { slug: 'quickbooks', name: 'QuickBooks' },
+ { slug: 'reddit', name: 'Reddit' },
+ { slug: 'reddit_ads', name: 'Reddit Ads' },
+ { slug: 'roam', name: 'Roam' },
+ { slug: 'salesforce', name: 'Salesforce' },
+ { slug: 'sentry', name: 'Sentry' },
+ { slug: 'servicem8', name: 'Servicem8' },
+ { slug: 'share_point', name: 'SharePoint' },
+ { slug: 'shippo', name: 'Shippo' },
+ { slug: 'slack', name: 'Slack' },
+ { slug: 'slackbot', name: 'Slackbot' },
+ { slug: 'splitwise', name: 'Splitwise' },
+ { slug: 'square', name: 'Square' },
+ { slug: 'stack_exchange', name: 'Stack Exchange' },
+ { slug: 'strava', name: 'Strava' },
+ { slug: 'stripe', name: 'Stripe' },
+ { slug: 'supabase', name: 'Supabase' },
+ { slug: 'ticketmaster', name: 'Ticketmaster' },
+ { slug: 'ticktick', name: 'Ticktick' },
+ { slug: 'timely', name: 'Timely' },
+ { slug: 'todoist', name: 'Todoist' },
+ { slug: 'toneden', name: 'Toneden' },
+ { slug: 'trello', name: 'Trello' },
+ { slug: 'typeform', name: 'Typeform' },
+ { slug: 'wakatime', name: 'WakaTime' },
+ { slug: 'webex', name: 'Webex' },
+ { slug: 'whatsapp', name: 'WhatsApp' },
+ { slug: 'wrike', name: 'Wrike' },
+ { slug: 'yandex', name: 'Yandex' },
+ { slug: 'ynab', name: 'YNAB' },
+ { slug: 'youtube', name: 'YouTube' },
+ { slug: 'zendesk', name: 'Zendesk' },
+ { slug: 'zoho', name: 'Zoho' },
+ { slug: 'zoho_bigin', name: 'Zoho Bigin' },
+ { slug: 'zoho_books', name: 'Zoho Books' },
+ { slug: 'zoho_desk', name: 'Zoho Desk' },
+ { slug: 'zoho_inventory', name: 'Zoho Inventory' },
+ { slug: 'zoho_invoice', name: 'Zoho Invoice' },
+ { slug: 'zoho_mail', name: 'Zoho Mail' },
+ { slug: 'zoom', name: 'Zoom' },
+]);
-function GoogleDriveIcon() {
- return (
-
- );
-}
+const MANAGED_TOOLKIT_NAME_BY_SLUG = new Map(
+ MANAGED_COMPOSIO_TOOLKITS.map(entry => [entry.slug, entry.name])
+);
-function NotionIcon() {
- return (
-
- );
-}
-
-function GitHubIcon() {
- return (
-
- );
-}
-
-function SlackIcon() {
- return (
-
- );
-}
-
-function LinearIcon() {
- return (
-
- );
-}
-
-function FacebookIcon() {
- return (
-
- );
-}
-
-function GoogleSheetsIcon() {
- return (
-
- );
-}
-
-function InstagramIcon() {
- return (
-
- );
-}
-
-function RedditIcon() {
- return (
-
- );
-}
+const CHAT_KEYWORDS = ['discord', 'slack', 'teams', 'webex', 'whatsapp', 'dialpad'];
+const SOCIAL_KEYWORDS = [
+ 'facebook',
+ 'instagram',
+ 'linkedin',
+ 'reddit',
+ 'youtube',
+ 'stack_exchange',
+];
+const PRODUCTIVITY_KEYWORDS = [
+ 'gmail',
+ 'calendar',
+ 'drive',
+ 'docs',
+ 'doc',
+ 'sheets',
+ 'slides',
+ 'tasks',
+ 'todoist',
+ 'trello',
+ 'notion',
+ 'box',
+ 'dropbox',
+ 'sharepoint',
+ 'one_drive',
+ 'onedrive',
+ 'outlook',
+ 'miro',
+ 'mural',
+ 'monday',
+ 'clickup',
+ 'linear',
+ 'jira',
+ 'confluence',
+ 'asana',
+ 'basecamp',
+ 'wrike',
+ 'cal',
+ 'calendly',
+ 'typeform',
+ 'excel',
+ 'figma',
+ 'google',
+];
+const PLATFORM_KEYWORDS = [
+ 'github',
+ 'gitlab',
+ 'bitbucket',
+ 'digital_ocean',
+ 'contentful',
+ 'supabase',
+ 'convex',
+ 'prisma',
+ 'sentry',
+ 'stripe',
+ 'salesforce',
+ 'hubspot',
+ 'quickbooks',
+ 'zendesk',
+ 'zoho',
+];
function GenericIntegrationIcon() {
return (
@@ -179,124 +241,98 @@ function GenericIntegrationIcon() {
);
}
-const CATALOG: Record> = {
- gmail: {
- name: 'Gmail',
- description: 'Read, search, and send email through your Google account.',
- category: 'Productivity',
- icon: ,
- },
- googlecalendar: {
- name: 'Google Calendar',
- description: 'List and manage events across your Google calendars.',
- category: 'Productivity',
- icon: ,
- },
- google_calendar: {
- name: 'Google Calendar',
- description: 'List and manage events across your Google calendars.',
- category: 'Productivity',
- icon: ,
- },
- googledrive: {
- name: 'Google Drive',
- description: 'Browse and fetch files from your Google Drive.',
- category: 'Productivity',
- icon: ,
- },
- google_drive: {
- name: 'Google Drive',
- description: 'Browse and fetch files from your Google Drive.',
- category: 'Productivity',
- icon: ,
- },
- notion: {
- name: 'Notion',
- description: 'Read and edit pages, databases, and comments in Notion.',
- category: 'Productivity',
- icon: ,
- },
- github: {
- name: 'GitHub',
- description: 'Inspect repos, issues, and pull requests on GitHub.',
- category: 'Tools & Automation',
- icon: ,
- },
- slack: {
- name: 'Slack',
- description: 'Send messages and read channel history in Slack.',
- category: 'Social',
- icon: ,
- },
- linear: {
- name: 'Linear',
- description: 'Triage and update issues in your Linear workspace.',
- category: 'Tools & Automation',
- icon: ,
- },
- facebook: {
- name: 'Facebook',
- description: 'Create posts, manage pages, and work with Facebook social data.',
- category: 'Social',
- icon: ,
- },
- google_sheets: {
- name: 'Google Sheets',
- description: 'Read, update, and organize spreadsheets in Google Sheets.',
- category: 'Productivity',
- icon: ,
- },
- googlesheets: {
- name: 'Google Sheets',
- description: 'Read, update, and organize spreadsheets in Google Sheets.',
- category: 'Productivity',
- icon: ,
- },
- instagram: {
- name: 'Instagram',
- description: 'Manage Instagram publishing, messaging, and social content workflows.',
- category: 'Social',
- icon: ,
- },
- reddit: {
- name: 'Reddit',
- description: 'Read posts, monitor communities, and participate in Reddit discussions.',
- category: 'Social',
- icon: ,
- },
-};
+function ComposioLogoBadge({ slug, name }: { slug: string; name: string }) {
+ const [failed, setFailed] = useState(false);
+ const logoUrl = composioLogoUrl(slug);
+
+ if (failed) {
+ return ;
+ }
+
+ return (
+
+ setFailed(true)}
+ />
+
+ );
+}
+
+function composioLogoUrl(slug: string): string {
+ return `https://logos.composio.dev/api/${slug}`;
+}
+
+function guessCategory(slug: string, name: string): SkillCategory {
+ const key = `${slug} ${name}`.toLowerCase();
+ if (CHAT_KEYWORDS.some(keyword => key.includes(keyword))) return 'Chat';
+ if (SOCIAL_KEYWORDS.some(keyword => key.includes(keyword))) return 'Social';
+ if (PRODUCTIVITY_KEYWORDS.some(keyword => key.includes(keyword))) return 'Productivity';
+ if (PLATFORM_KEYWORDS.some(keyword => key.includes(keyword))) return 'Platform';
+ return 'Tools & Automation';
+}
+
+function defaultDescription(name: string, category: SkillCategory): string {
+ switch (category) {
+ case 'Chat':
+ return `Connect ${name} for messaging, inbox, and team communication workflows.`;
+ case 'Social':
+ return `Connect ${name} for social publishing, community, and audience workflows.`;
+ case 'Productivity':
+ return `Connect ${name} for documents, planning, file, and day-to-day productivity workflows.`;
+ case 'Platform':
+ return `Connect ${name} for developer, platform, CRM, and business system workflows.`;
+ default:
+ return `Connect ${name} through Composio managed auth.`;
+ }
+}
+
+function permissionLabelFor(category: SkillCategory): string {
+ switch (category) {
+ case 'Chat':
+ return 'Messages, channels, and communication data';
+ case 'Social':
+ return 'Posts, profiles, and social content';
+ case 'Productivity':
+ return 'Docs, files, tasks, and workspace data';
+ case 'Platform':
+ return 'Repos, records, tickets, and system data';
+ default:
+ return 'Connected account data';
+ }
+}
+
+function prettifyUnknownSlug(slug: string): string {
+ return slug
+ .split(/[_-]+/)
+ .filter(Boolean)
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
+ .join(' ');
+}
/**
* Canonical toolkit slugs used as the default catalog when the backend
- * allowlist hasn't loaded yet. One entry per integration — CATALOG
- * handles alternate slug variants (e.g. `google_calendar` →
- * `googlecalendar`) so they don't need to appear here.
+ * allowlist hasn't loaded yet. One entry per Composio managed-auth
+ * integration.
*/
-export const KNOWN_COMPOSIO_TOOLKITS = Object.freeze([
- 'gmail',
- 'googlecalendar',
- 'googledrive',
- 'googlesheets',
- 'notion',
- 'github',
- 'slack',
- 'linear',
- 'facebook',
- 'instagram',
- 'reddit',
-]);
+export const KNOWN_COMPOSIO_TOOLKITS = Object.freeze(
+ MANAGED_COMPOSIO_TOOLKITS.map(entry => entry.slug)
+);
export function composioToolkitMeta(slug: string): ComposioToolkitMeta {
const key = canonicalizeComposioToolkitSlug(slug);
- const hit = CATALOG[key];
- if (hit) return { slug: key, ...hit };
- // Fallback: title-case the slug and bucket it under "Other".
- const name = key.charAt(0).toUpperCase() + key.slice(1);
+ const name = MANAGED_TOOLKIT_NAME_BY_SLUG.get(key) ?? prettifyUnknownSlug(key);
+ const category = guessCategory(key, name);
return {
slug: key,
name,
- description: `Integration for ${name}.`,
- category: 'Other',
- icon: ,
+ description: defaultDescription(name, category),
+ category,
+ icon: ,
+ logoUrl: composioLogoUrl(key),
+ permissionLabel: permissionLabelFor(category),
};
}
diff --git a/app/src/pages/__tests__/Skills.composio-catalog.test.tsx b/app/src/pages/__tests__/Skills.composio-catalog.test.tsx
index 4016bb67e..292c6769f 100644
--- a/app/src/pages/__tests__/Skills.composio-catalog.test.tsx
+++ b/app/src/pages/__tests__/Skills.composio-catalog.test.tsx
@@ -43,20 +43,24 @@ describe('Skills page — Composio catalog fallback', () => {
it('shows known composio integrations in their configured category groups when the live toolkit list is empty', () => {
renderWithProviders( , { initialEntries: ['/skills'] });
+ expect(screen.getByRole('heading', { name: 'Chat' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Productivity' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Tools & Automation' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Social' })).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Platform' })).toBeInTheDocument();
+ expect(screen.getByText('Discord')).toBeInTheDocument();
expect(screen.getByText('Google Calendar')).toBeInTheDocument();
expect(screen.getByText('Google Drive')).toBeInTheDocument();
expect(screen.getByText('Gmail')).toBeInTheDocument();
expect(screen.getByText('Google Sheets')).toBeInTheDocument();
- expect(screen.getByText('Notion')).toBeInTheDocument();
expect(screen.getByText('Facebook')).toBeInTheDocument();
expect(screen.getByText('GitHub')).toBeInTheDocument();
expect(screen.getByText('Instagram')).toBeInTheDocument();
expect(screen.getByText('Linear')).toBeInTheDocument();
expect(screen.getByText('Reddit')).toBeInTheDocument();
expect(screen.getByText('Slack')).toBeInTheDocument();
+ expect(screen.getByText('Supabase')).toBeInTheDocument();
+ expect(screen.getByText('Zoom')).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Other' })).not.toBeInTheDocument();
});
diff --git a/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx b/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx
index a601861f1..d83933b6f 100644
--- a/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx
+++ b/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx
@@ -43,6 +43,7 @@ describe('Skills page — Notion composio integration', () => {
fireEvent.click(within(notionCard as HTMLElement).getByRole('button', { name: 'Connect' }));
expect(await screen.findByRole('heading', { name: 'Connect Notion' })).toBeInTheDocument();
- expect(screen.getByText(/Connect your Notion account\./i)).toBeInTheDocument();
+ expect(screen.getByText(/Connect your Notion account through Composio/i)).toBeInTheDocument();
+ expect(screen.getByText(/OpenHuman's own agent permissions/i)).toBeInTheDocument();
});
});
diff --git a/app/src/test/setup.ts b/app/src/test/setup.ts
index 314dc828a..42d54f641 100644
--- a/app/src/test/setup.ts
+++ b/app/src/test/setup.ts
@@ -24,6 +24,52 @@ import {
vi.stubEnv('DEV', true);
vi.stubEnv('MODE', 'test');
+function createStorageMock(): Storage {
+ const store = new Map();
+ return {
+ get length() {
+ return store.size;
+ },
+ clear() {
+ store.clear();
+ },
+ getItem(key: string) {
+ return store.has(key) ? store.get(key)! : null;
+ },
+ key(index: number) {
+ return Array.from(store.keys())[index] ?? null;
+ },
+ removeItem(key: string) {
+ store.delete(key);
+ },
+ setItem(key: string, value: string) {
+ store.set(String(key), String(value));
+ },
+ };
+}
+
+function ensureStorage(name: 'localStorage' | 'sessionStorage') {
+ const current = globalThis[name];
+ if (
+ current &&
+ typeof current.getItem === 'function' &&
+ typeof current.setItem === 'function' &&
+ typeof current.removeItem === 'function' &&
+ typeof current.clear === 'function'
+ ) {
+ return;
+ }
+
+ Object.defineProperty(globalThis, name, {
+ value: createStorageMock(),
+ configurable: true,
+ writable: true,
+ });
+}
+
+ensureStorage('localStorage');
+ensureStorage('sessionStorage');
+
// Polyfill ResizeObserver for cmdk/Radix components in jsdom
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class ResizeObserver {