diff --git a/src/main/services/composio/composio-connection-service.ts b/src/main/services/composio/composio-connection-service.ts index dbc6ce3d..cf9c134e 100644 --- a/src/main/services/composio/composio-connection-service.ts +++ b/src/main/services/composio/composio-connection-service.ts @@ -106,6 +106,7 @@ export class ComposioConnectionService { } const tracked = this.trackedIds(orgId); const toolkitsHere = new Set(localRows.filter((row) => liveIds.has(row.id)).map((row) => row.toolkit_slug)); + const adoptable = new Map(); for (const account of accounts) { if (!account.toolkitSlug) continue; if (tracked.has(account.id)) { @@ -115,11 +116,18 @@ export class ComposioConnectionService { } // Only a toolkit with no account here is adopted; other untracked accounts (a waiting reconnect, an orphan) never join. if (toolkitsHere.has(account.toolkitSlug)) continue; - toolkitsHere.add(account.toolkitSlug); - this.db.prepare(` - insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, label, auth_config_id, status, created_at, updated_at) - values (?, ?, ?, ?, ?, ?, ?, ?, ?) - `).run(account.id, orgId, account.toolkitSlug, connectorDisplayName(account.toolkitSlug), DEFAULT_ACCOUNT_LABEL, account.authConfigId, account.status, account.createdAt || now, now); + adoptable.set(account.toolkitSlug, [...(adoptable.get(account.toolkitSlug) ?? []), account]); + } + for (const [slug, found] of adoptable) { + for (const account of found) { + // A lone account is "main"; several each keep their own id as label, so none stands in for another. + const label = found.length === 1 ? DEFAULT_ACCOUNT_LABEL : normalizeAccountLabel(account.id); + this.db.prepare(` + insert into connector_accounts(id, org_id, toolkit_slug, toolkit_name, label, auth_config_id, status, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(account.id, orgId, slug, connectorDisplayName(slug), label, account.authConfigId, account.status, account.createdAt || now, now); + } + console.info(`[connectors] adopted ${found.length} ${slug} account(s) made outside the app`); } } diff --git a/tests/unit/composio-reconnect.test.ts b/tests/unit/composio-reconnect.test.ts index 5cb8f06f..d00e2817 100644 --- a/tests/unit/composio-reconnect.test.ts +++ b/tests/unit/composio-reconnect.test.ts @@ -123,6 +123,21 @@ describe("reconnecting a connector keeps the working account until the new one i h.connection.close(); }); + test("several accounts made outside the app for one toolkit are each adopted, none folded into another", async () => { + const h = await connectedCompany(); + for (const id of ["ca_slack_b", "ca_slack_a"]) remote.set(id, { id, toolkitSlug: "slack", status: "ACTIVE", authConfigId: "ac_slack", createdAt: new Date().toISOString() }); + remote.set("ca_notion", { id: "ca_notion", toolkitSlug: "notion", status: "ACTIVE", authConfigId: "ac_notion", createdAt: new Date().toISOString() }); + await h.service.refreshAccounts(h.orgId); + const rows = h.connection.db.prepare("select id, toolkit_slug as toolkit, label from connector_accounts where org_id = ? order by toolkit_slug, label").all(h.orgId); + expect(rows).toEqual([ + { id: h.oldId, toolkit: "github", label: "main" }, + { id: "ca_notion", toolkit: "notion", label: "main" }, + { id: "ca_slack_a", toolkit: "slack", label: "ca_slack_a" }, + { id: "ca_slack_b", toolkit: "slack", label: "ca_slack_b" }, + ]); + h.connection.close(); + }); + test("a reconnect that cannot start touches nothing", async () => { const h = await connectedCompany(); failCreate = true;