diff --git a/electron-builder.yml b/electron-builder.yml index f0336c3a..e6f2c2ef 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -8,6 +8,9 @@ files: - out/**/* - package.json - resources/icon.png +extraResources: + - from: cred.txt + to: cred.txt asar: true mac: category: public.app-category.business diff --git a/src/main/app/ipc-handlers.ts b/src/main/app/ipc-handlers.ts index fb8fa0f2..71d71c4c 100644 --- a/src/main/app/ipc-handlers.ts +++ b/src/main/app/ipc-handlers.ts @@ -321,7 +321,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { } }); - const composioConnections = new ComposioConnectionService(deps.connection.db, process.cwd()); + const composioConnections = new ComposioConnectionService(deps.connection.db); const connectorGrants = new ConnectorGrantsService(deps.connection.db); ipcMain.handle(IPC_CHANNELS.listComposioToolkits, async (_event, input) => { @@ -362,19 +362,6 @@ export function registerIpcHandlers(deps: IpcDeps): void { toolkitSlug: input.toolkitSlug, }))); - ipcMain.handle(IPC_CHANNELS.saveComposioSettings, (_event, input) => mutate(() => { - const now = new Date().toISOString(); - const db = deps.connection.db; - const upsert = db.prepare(` - insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?) - on conflict(org_id, key) do update set value = excluded.value, updated_at = excluded.updated_at - `); - upsert.run(input.orgId, "api_key", input.apiKey, now); - upsert.run(input.orgId, "toolkits", JSON.stringify(input.toolkits), now); - upsert.run(input.orgId, "entity_id", input.orgId, now); - return null; - })); - ipcMain.handle(IPC_CHANNELS.windowMinimize, () => deps.mainWindow.minimize()); ipcMain.handle(IPC_CHANNELS.windowMaximize, () => deps.mainWindow.isMaximized() ? deps.mainWindow.unmaximize() : deps.mainWindow.maximize()); ipcMain.handle(IPC_CHANNELS.windowClose, () => deps.mainWindow.close()); diff --git a/src/main/services/composio/composio-connection-service.ts b/src/main/services/composio/composio-connection-service.ts index 703e08d5..6eaeac4f 100644 --- a/src/main/services/composio/composio-connection-service.ts +++ b/src/main/services/composio/composio-connection-service.ts @@ -16,7 +16,7 @@ let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | nu * Connectors page read local state instead of calling out on every frame. */ export class ComposioConnectionService { - constructor(private readonly db: DatabaseSync, private readonly credentialFileDir: string) {} + constructor(private readonly db: DatabaseSync) {} listToolkits(orgId: string): Promise { const client = this.requireClient(orgId); @@ -81,7 +81,7 @@ export class ComposioConnectionService { } private requireClient(orgId: string): ComposioClient { - const apiKey = resolveComposioApiKey(this.db, orgId, this.credentialFileDir); + const apiKey = resolveComposioApiKey(this.db, orgId); if (!apiKey) throw new Error("Composio API key is not configured for this company."); return new ComposioClient(apiKey); } diff --git a/src/main/services/composio/composio-credentials.ts b/src/main/services/composio/composio-credentials.ts index bc81a0ea..78464ece 100644 --- a/src/main/services/composio/composio-credentials.ts +++ b/src/main/services/composio/composio-credentials.ts @@ -3,18 +3,19 @@ import { join } from "node:path"; import type { DatabaseSync } from "node:sqlite"; /** - * The org's Composio API key. A developer credential file next to the app - * (cred.txt, first line "Composio apikey: ak_...", gitignored) is the source - * of truth whenever it exists: rotating the key in the file takes effect on - * the next resolution, replacing whatever the database held. Packaged - * installs have no credential file and manage the key in Settings instead. + * The Composio integration key ships with the product: cred.txt (first line + * "Composio apikey: ak_...", gitignored) sits at the project root in + * development and inside the packaged app's resources directory in + * production. Wherever the file exists it is the source of truth — rotating + * the key in the file replaces whatever the database held on the next + * resolution. It is not user-configurable. */ -export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDir: string = process.cwd()): string | null { +export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDirs: string[] = defaultCredentialDirs()): string | null { const stored = db.prepare( "select value from composio_settings where org_id = ? and key = 'api_key' limit 1", ).get(orgId) as { value: string } | undefined; - const fileKey = readCredentialFile(credentialFileDir); + const fileKey = readCredentialFile(credentialFileDirs); if (!fileKey) return stored?.value ?? null; if (stored?.value === fileKey) return fileKey; @@ -28,10 +29,20 @@ export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentia return fileKey; } -function readCredentialFile(dir: string): string | null { - const path = join(dir, "cred.txt"); - if (!existsSync(path)) return null; - const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? ""; - const key = firstLine.split(":").slice(1).join(":").trim(); - return key || null; +/** Project root in dev; the app's resources directory when packaged. */ +export function defaultCredentialDirs(): string[] { + const dirs = [process.cwd()]; + if (process.resourcesPath) dirs.push(process.resourcesPath); + return dirs; +} + +function readCredentialFile(dirs: string[]): string | null { + for (const dir of dirs) { + const path = join(dir, "cred.txt"); + if (!existsSync(path)) continue; + const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? ""; + const key = firstLine.split(":").slice(1).join(":").trim(); + if (key) return key; + } + return null; } diff --git a/src/preload/index.ts b/src/preload/index.ts index b2617bd1..df52f88b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -82,7 +82,6 @@ const api: DesktopApi = { setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input), createRetentionFreeze: (input) => invoke(IPC_CHANNELS.createRetentionFreeze, input), setRetentionFreezeState: (input) => invoke(IPC_CHANNELS.setRetentionFreezeState, input), - saveComposioSettings: (input) => invoke(IPC_CHANNELS.saveComposioSettings, input), listComposioToolkits: (input) => invoke(IPC_CHANNELS.listComposioToolkits, input), connectComposioToolkit: (input) => invoke(IPC_CHANNELS.connectComposioToolkit, input), refreshComposioAccounts: (input) => invoke(IPC_CHANNELS.refreshComposioAccounts, input), diff --git a/src/renderer/src/pages/ConnectorsPage.tsx b/src/renderer/src/pages/ConnectorsPage.tsx index a4ae9b7f..3c34b4f6 100644 --- a/src/renderer/src/pages/ConnectorsPage.tsx +++ b/src/renderer/src/pages/ConnectorsPage.tsx @@ -66,6 +66,12 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { if (pollTimer.current) clearTimeout(pollTimer.current); }, []); + // Account statuses sync from Composio when the page opens; a failure here + // is already expressed by the catalog error state. + useEffect(() => { + void window.agentCompany.refreshComposioAccounts({ orgId }); + }, [orgId]); + const refreshAccounts = () => actions.run(() => window.agentCompany.refreshComposioAccounts({ orgId })); @@ -104,17 +110,7 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { return (
-
-
-

Connectors

-
- Company connections your agents can be granted to use. -
-
-
- -
-
+

Connectors

diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx index c06ef15a..e51bc948 100644 --- a/src/renderer/src/pages/HomePage.tsx +++ b/src/renderer/src/pages/HomePage.tsx @@ -49,7 +49,6 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) { return (
-

{greeting}, {firstName}

diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index fb38abb2..575141ca 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -43,8 +43,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) { const workspaces = useMemo(() => snapshot.workspaces.filter((item) => item.orgId === orgId), [snapshot.workspaces, orgId]); const providerRequests = useMemo(() => snapshot.providerRequests.filter((item) => item.orgId === orgId), [snapshot.providerRequests, orgId]); const hiringPolicies = useMemo(() => snapshot.hiringPolicies.filter((item) => item.orgId === orgId), [snapshot.hiringPolicies, orgId]); - const migrationReports = useMemo(() => snapshot.migrationReports.filter((report) => report.orgId === orgId), [snapshot.migrationReports, orgId]); - const rollbackSnapshots = useMemo(() => snapshot.rollbackSnapshots.filter((item) => item.orgId === orgId), [snapshot.rollbackSnapshots, orgId]); return (
@@ -83,17 +81,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) { {tab === "advanced" && ( item.orgId === orgId)} actions={actions} /> )} diff --git a/src/renderer/src/pages/settings/AdvancedPanel.tsx b/src/renderer/src/pages/settings/AdvancedPanel.tsx index 901c8bcc..5122da34 100644 --- a/src/renderer/src/pages/settings/AdvancedPanel.tsx +++ b/src/renderer/src/pages/settings/AdvancedPanel.tsx @@ -1,49 +1,33 @@ import type { AgentRecord, HiringPolicyRecord, - MigrationReportRecord, ProviderCatalogEntry, ProviderRecord, ProviderRequestRecord, - RollbackSnapshotRecord, WorkspaceRecord, } from "../../../../shared/domain"; -import type { MigrationReconciliationItemRecord } from "../../../../shared/rollback-domain"; -import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain"; import type { SnapshotActions } from "../../app/useSnapshot"; import { BillingPanel } from "./BillingPanel"; -import { ComposioPanel } from "./ComposioPanel"; -import { MigrationPanel } from "./MigrationPanel"; import { ProviderRequestPanel } from "./ProviderRequestPanel"; /** The low-frequency operator surfaces, one section each. */ export function AdvancedPanel({ orgId, - employeeId, agents, providers, catalog, workspaces, providerRequests, hiringPolicies, - composioSettings, - migrationReports, - rollbackSnapshots, - reconciliationItems, actions, }: { orgId: string; - employeeId: string; agents: AgentRecord[]; providers: ProviderRecord[]; catalog: ProviderCatalogEntry[]; workspaces: WorkspaceRecord[]; providerRequests: ProviderRequestRecord[]; hiringPolicies: HiringPolicyRecord[]; - composioSettings: ComposioSettingRecord[]; - migrationReports: MigrationReportRecord[]; - rollbackSnapshots: RollbackSnapshotRecord[]; - reconciliationItems: MigrationReconciliationItemRecord[]; actions: SnapshotActions; }) { const connected = providers.filter((provider) => provider.enabled); @@ -51,11 +35,8 @@ export function AdvancedPanel({

Advanced

-
Composio
- - {connected.length > 0 && ( -
+
- -
- -
); } diff --git a/src/renderer/src/pages/settings/ComposioPanel.tsx b/src/renderer/src/pages/settings/ComposioPanel.tsx deleted file mode 100644 index 756b6ca9..00000000 --- a/src/renderer/src/pages/settings/ComposioPanel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useMemo, useState, type FormEvent } from "react"; -import { Button } from "../../shared/ui"; -import type { SnapshotActions } from "../../app/useSnapshot"; -import type { ComposioSettingRecord } from "../../../../shared/snapshot-domain"; - -function hasSavedKey(settings: ComposioSettingRecord[], orgId: string): boolean { - return settings.some((s) => s.orgId === orgId && s.key === "api_key" && s.value.length > 0); -} - -/** - * The Composio project API key. Which toolkits agents may use is decided by - * connections and per-agent grants on the Connectors page, not here. - */ -export function ComposioPanel({ - orgId, - settings, - actions, -}: { - orgId: string; - settings: ComposioSettingRecord[]; - actions: SnapshotActions; -}) { - const keyConfigured = useMemo(() => hasSavedKey(settings, orgId), [settings, orgId]); - const [apiKey, setApiKey] = useState(""); - - const handleSave = (event: FormEvent) => { - event.preventDefault(); - const key = apiKey.trim(); - if (!key) return; - void actions.run(() => window.agentCompany.saveComposioSettings({ - orgId, - apiKey: key, - toolkits: [], - })).then(() => setApiKey("")); - }; - - return ( -
- setApiKey(event.target.value)} - placeholder={keyConfigured ? "••••••••" : "ak_..."} - autoComplete="off" - style={{ flex: 1 }} - /> - -
- ); -} diff --git a/src/renderer/src/pages/settings/MigrationPanel.tsx b/src/renderer/src/pages/settings/MigrationPanel.tsx deleted file mode 100644 index 3ff57501..00000000 --- a/src/renderer/src/pages/settings/MigrationPanel.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { useState, type FormEvent } from "react"; -import { Button, Field, Icon, Pill } from "../../shared/ui"; -import { Dialog } from "../../shared/Dialog"; -import type { MigrationReconciliationItemRecord, MigrationReportRecord, RollbackSnapshotRecord } from "../../../../shared/domain"; -import type { SnapshotActions } from "../../app/useSnapshot"; -import { formatRelative } from "../types"; - -interface ParsedReport { - tableCounts: Record; - tableChecksums: Record; - idMaps: Record; - failures: Array<{ table: string; reason: string }>; - rollbackPack?: { sourceDatabasePath: string; capturedAt: string; restoreStrategy: string }; -} - -export function MigrationPanel({ - orgId, - employeeId, - reports, - rollbackSnapshots, - reconciliationItems, - actions, -}: { - orgId: string; - employeeId: string; - reports: MigrationReportRecord[]; - rollbackSnapshots: RollbackSnapshotRecord[]; - reconciliationItems: MigrationReconciliationItemRecord[]; - actions: SnapshotActions; -}) { - const [open, setOpen] = useState(false); - - const submit = (event: FormEvent) => { - event.preventDefault(); - const form = new FormData(event.currentTarget); - void actions.run(() => window.agentCompany.createMigrationReport({ - orgId, - sourceDatabasePath: String(form.get("sourceDatabasePath") ?? "").trim(), - })).then((result) => { if (result) setOpen(false); }); - }; - - return ( -
-
-
Migration
-
- -
-
- {reports.length > 0 && ( -
- {reports.map((report, index) => ( - - ))} -
- )} -
-
- -
Rollback snapshots
-
- {rollbackSnapshots.length > 0 ? rollbackSnapshots.map((snapshot, index) => ( - - )) :
0 rollback snapshots
} -
-
-
- -
Reconciliation
-
- {reconciliationItems.length > 0 ? reconciliationItems.map((item, index) => ( - - )) :
0 reconciliation items
} -
- setOpen(false)} onSubmit={submit}> - - -
- ); -} - -function ReportRow({ - orgId, - employeeId, - report, - actions, - last, -}: { - orgId: string; - employeeId: string; - report: MigrationReportRecord; - actions: SnapshotActions; - last: boolean; -}) { - const body = parseReport(report.reportJson); - const idCount = Object.values(body.idMaps).reduce((sum, ids) => sum + ids.length, 0); - return ( -
-
-
{report.sourceRef}
-
{formatRelative(report.createdAt)}
-
-
- {Object.entries(body.tableCounts).map(([table, count]) => ( - {table}: {count} - ))} -
- {Object.keys(body.tableChecksums).length} checksums -
- {idCount} ids - {body.failures.length > 0 ? {body.failures.length} failures : null} -
- -
- ); -} - -function RollbackRow({ - orgId, - employeeId, - snapshot, - actions, - last, -}: { - orgId: string; - employeeId: string; - snapshot: RollbackSnapshotRecord; - actions: SnapshotActions; - last: boolean; -}) { - const manifest = JSON.parse(snapshot.workspaceManifestJson) as { workspaces: unknown[]; files: unknown[] }; - return ( -
-
-
{snapshot.sourceDatabasePath}
-
{formatRelative(snapshot.createdAt)}
-
- {snapshot.sourceDatabaseSha256.slice(0, 12)} - {manifest.workspaces.length} workspaces - {manifest.files.length} files - -
- ); -} - -function ReconciliationRow({ - orgId, - employeeId, - item, - actions, - last, -}: { - orgId: string; - employeeId: string; - item: MigrationReconciliationItemRecord; - actions: SnapshotActions; - last: boolean; -}) { - const resolved = item.state === "resolved"; - return ( -
-
-
{item.targetType} · {item.targetId}
-
{formatRelative(item.createdAt)}
-
-
{item.reason}
- {item.state} - {item.sourceEventId.slice(0, 12)} - -
- ); -} - -function parseReport(reportJson: string): ParsedReport { - const parsed = JSON.parse(reportJson) as ParsedReport; - if (!parsed.tableCounts || !parsed.tableChecksums || !parsed.idMaps || !Array.isArray(parsed.failures)) { - throw new Error("Migration report is invalid."); - } - return parsed; -} diff --git a/src/shared/commands/composio.ts b/src/shared/commands/composio.ts index dcbd4a21..365065be 100644 --- a/src/shared/commands/composio.ts +++ b/src/shared/commands/composio.ts @@ -1,9 +1,3 @@ -export interface SaveComposioSettingsInput { - orgId: string; - apiKey: string; - toolkits: string[]; -} - export interface ComposioToolkitSummaryDto { slug: string; name: string; diff --git a/src/shared/desktop-api.ts b/src/shared/desktop-api.ts index e512dade..c9d5bc94 100644 --- a/src/shared/desktop-api.ts +++ b/src/shared/desktop-api.ts @@ -63,7 +63,6 @@ import type { RunTaskInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, - SaveComposioSettingsInput, ComposioToolkitSummaryDto, ConnectComposioToolkitInput, ConnectComposioToolkitResult, @@ -179,7 +178,6 @@ export interface DesktopApi { setLegalHoldState(input: SetLegalHoldStateInput): Promise>; createRetentionFreeze(input: CreateRetentionFreezeInput): Promise>; setRetentionFreezeState(input: SetRetentionFreezeStateInput): Promise>; - saveComposioSettings(input: SaveComposioSettingsInput): Promise>; listComposioToolkits(input: ListComposioToolkitsInput): Promise>; connectComposioToolkit(input: ConnectComposioToolkitInput): Promise>; refreshComposioAccounts(input: RefreshComposioAccountsInput): Promise>; diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index bec9277c..c35fb109 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -74,7 +74,6 @@ export const IPC_CHANNELS = { deleteMeeting: "agentcompany:delete-meeting", createSprint: "agentcompany:create-sprint", deleteSprint: "agentcompany:delete-sprint", - saveComposioSettings: "agentcompany:save-composio-settings", listComposioToolkits: "agentcompany:list-composio-toolkits", connectComposioToolkit: "agentcompany:connect-composio-toolkit", refreshComposioAccounts: "agentcompany:refresh-composio-accounts", diff --git a/tests/e2e/desktop.spec.ts b/tests/e2e/desktop.spec.ts index 26bcb72a..666b4fdc 100644 --- a/tests/e2e/desktop.spec.ts +++ b/tests/e2e/desktop.spec.ts @@ -3,7 +3,6 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { randomUUID } from "node:crypto"; -import { DatabaseSync } from "node:sqlite"; test("onboards, hires, and creates work in the Electron shell", async () => { const profileDir = join(tmpdir(), `agentcompany-new-e2e-${randomUUID()}`); @@ -115,7 +114,7 @@ test("onboards, hires, and creates work in the Electron shell", async () => { // is reachable from the test machine. await page.getByRole("button", { name: "Connectors", exact: true }).click(); await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Refresh" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Refresh" })).toHaveCount(0); await page.getByRole("button", { name: "Settings" }).click(); await expect(page.getByText("AI CEO", { exact: true })).toBeVisible(); @@ -130,7 +129,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await expect(page.getByRole("button", { name: "Background" })).toBeVisible(); await page.getByLabel("Request prompt").fill("Draft a provider control memo."); await page.getByLabel("Request instructions").fill("Use direct language."); - await expect(page.getByLabel("Composio API key")).toBeVisible(); await page.getByRole("button", { name: "Providers" }).click(); await page.getByLabel("OpenAI data policy").selectOption("zero_data_retention"); @@ -147,17 +145,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await expect(page.getByText("Never run").first()).toBeVisible(); await page.getByRole("button", { name: "Run tests" }).click(); await expect(page.getByText("1/1 passed").first()).toBeVisible(); - const legacyPath = createLegacyDatabase(); - await page.getByRole("button", { name: "Advanced" }).click(); - await page.getByRole("button", { name: "Report" }).click(); - await page.getByLabel("Source database path").fill(legacyPath); - await page.locator(".modal").getByRole("button", { name: "Create" }).click(); - await expect(page.getByText("organizations: 1").first()).toBeVisible(); - await page.getByRole("button", { name: "Snapshot" }).click(); - await expect(page.getByText("Rollback snapshots", { exact: true })).toBeVisible(); - await expect(page.getByText(/\d+ workspaces/).first()).toBeVisible(); - await page.getByRole("button", { name: "Set", exact: true }).click(); - await expect(page.getByRole("button", { name: "active", exact: true })).toBeDisabled(); // Virtual office page await page.getByRole("button", { name: "Office" }).click(); @@ -176,14 +163,6 @@ test("onboards, hires, and creates work in the Electron shell", async () => { } }); -function createLegacyDatabase(): string { - const legacyPath = join(tmpdir(), `agentcompany-legacy-${randomUUID()}.db`); - const legacy = new DatabaseSync(legacyPath); - legacy.exec("create table organizations(id text primary key); insert into organizations(id) values ('legacy-org');"); - legacy.close(); - return legacyPath; -} - function createSkillDirectory(): string { const root = join(tmpdir(), `agentcompany-skill-${randomUUID()}`); mkdirSync(join(root, "checks"), { recursive: true }); diff --git a/tests/unit/composio-credentials.test.ts b/tests/unit/composio-credentials.test.ts index e32ae05f..4ab67350 100644 --- a/tests/unit/composio-credentials.test.ts +++ b/tests/unit/composio-credentials.test.ts @@ -40,30 +40,39 @@ describe("composio credential resolution", () => { test("seeds the database from the credential file", () => { const h = harness(); writeCred(h, "ak_first"); - expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_first"); + expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_first"); expect(storedKey(h)).toBe("ak_first"); }); test("a rotated credential file replaces the stale stored key", () => { const h = harness(); writeCred(h, "ak_old"); - resolveComposioApiKey(h.connection.db, h.orgId, h.credDir); + resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir]); writeCred(h, "ak_rotated"); - expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_rotated"); + expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_rotated"); expect(storedKey(h)).toBe("ak_rotated"); }); + test("finds the credential file in a later directory when the first has none", () => { + const h = harness(); + writeCred(h, "ak_resources"); + const emptyDir = join(tmpdir(), `ac-cred-empty-${randomUUID()}`); + mkdirSync(emptyDir, { recursive: true }); + expect(resolveComposioApiKey(h.connection.db, h.orgId, [emptyDir, h.credDir])).toBe("ak_resources"); + expect(storedKey(h)).toBe("ak_resources"); + }); + test("without a credential file the stored key stands", () => { const h = harness(); const now = new Date().toISOString(); h.connection.db.prepare( "insert into composio_settings(org_id, key, value, updated_at) values (?, 'api_key', 'ak_settings', ?)", ).run(h.orgId, now); - expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBe("ak_settings"); + expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_settings"); }); test("no file and no stored key yields null", () => { const h = harness(); - expect(resolveComposioApiKey(h.connection.db, h.orgId, h.credDir)).toBeNull(); + expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBeNull(); }); });