diff --git a/sdk/lib/adapters/ampersand/core/connection.ts b/sdk/lib/adapters/ampersand/core/connection.ts index 9fd9f80..32762a3 100644 --- a/sdk/lib/adapters/ampersand/core/connection.ts +++ b/sdk/lib/adapters/ampersand/core/connection.ts @@ -23,18 +23,15 @@ export async function checkConnectionHelper({ projectId = process.env.AMPERSAND_PROJECT_ID || '', }: CheckConnectionParams): Promise { try { - const client = new SDKNodePlatform({ - apiKeyHeader: apiKey, - }); + const client = new SDKNodePlatform({ apiKeyHeader: apiKey }); const connections = await client.connections.list({ projectIdOrName: projectId, provider, }); - // @ts-ignore – SDK typing for `connections.list` result is loose - if (connections.length > 0) { - // @ts-ignore – access first connection object directly + if (connections && Array.isArray(connections) && connections.length > 0) { + // access first connection object directly const connection = connections[0]; return { found: true, diff --git a/sdk/lib/adapters/ampersand/core/installation.ts b/sdk/lib/adapters/ampersand/core/installation.ts index 82abb01..830617f 100644 --- a/sdk/lib/adapters/ampersand/core/installation.ts +++ b/sdk/lib/adapters/ampersand/core/installation.ts @@ -1,4 +1,5 @@ import { SDKNodePlatform } from '@amp-labs/sdk-node-platform'; +import { ListInstallationsInstallation } from '@amp-labs/sdk-node-platform/src/models/operations/listinstallations'; import { checkConnectionHelper } from './connection'; import * as Sentry from '@sentry/node'; interface CheckInstallationParams { @@ -50,7 +51,8 @@ export async function checkInstallationHelper({ // @ts-ignore – Filter by provider (Ampersand lower-cases internally) const filtered = installations.filter( - (inst: any) => inst.connection?.provider === provider.toLowerCase(), + (inst: ListInstallationsInstallation) => + inst.connection?.provider === provider.toLowerCase(), ); if (filtered.length > 0) { @@ -81,26 +83,27 @@ export async function createInstallationHelper({ projectId = process.env.AMPERSAND_PROJECT_ID || '', integrationName = process.env.AMPERSAND_INTEGRATION_NAME || '', }: CreateInstallationParams): Promise { - try { - const client = new SDKNodePlatform({ - apiKeyHeader: apiKey, - }); - - const data = await client.installations.create({ - projectIdOrName: projectId, - integrationId: integrationName, - requestBody: { - connectionId, - groupRef, - config: { - createdBy: 'ai-sdk:create-installation', - content: { - provider, - proxy: { enabled: true }, - }, + const requestBody = { + projectIdOrName: projectId, + integrationId: integrationName, + requestBody: { + connectionId, + groupRef, + config: { + createdBy: 'ai-sdk:create-installation', + content: { + // does not support custom config content yet + provider, + proxy: { enabled: true }, }, }, - }); + }, + }; + + try { + const client = new SDKNodePlatform({ apiKeyHeader: apiKey }); + + const data = await client.installations.create(requestBody); // @ts-ignore – vary depending on SDK version const installationId = data.installation?.id ?? data.id; diff --git a/test/package.json b/test/package.json index 3c6278a..4e8c2df 100644 --- a/test/package.json +++ b/test/package.json @@ -9,6 +9,7 @@ "test:schema": "tsx ai-e2e-schema-validation.ts", "test:checkConnection": "tsx tools/checkConnection.test.ts", "test:checkInstallation": "tsx tools/checkInstallation.test.ts", + "test:createInstallation": "tsx tools/createInstallation.test.ts", "test:startOAuth": "tsx tools/startOAuth.test.ts", "test:createRecord": "tsx tools/createRecord.test.ts", "test:updateRecord": "tsx tools/updateRecord.test.ts", diff --git a/test/tools/createInstallation.test.ts b/test/tools/createInstallation.test.ts new file mode 100644 index 0000000..6569f4a --- /dev/null +++ b/test/tools/createInstallation.test.ts @@ -0,0 +1,182 @@ +/** + * E2E Test: createInstallation Tool + * + * Tests the createInstallation tool which creates a new installation + * for a provider on Ampersand. + * + * Prerequisites: An active connection must exist for the provider + * Uses OpenAI: Yes (minimal usage) + */ + +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { + createInstallation, + checkConnection, + checkInstallation, +} from '@amp-labs/ai/aisdk'; +import { + TestRunner, + checkEnvironmentVariables, + assert, + log, +} from '../helpers/test-utils'; + +async function main() { + console.log('='.repeat(60)); + console.log('E2E Test: createInstallation'); + console.log('='.repeat(60)); + console.log(); + + checkEnvironmentVariables(); + + const runner = new TestRunner(); + + const SALESFORCE_PROVIDER = 'salesforce'; + + // Test 1: Create installation for Salesforce + await runner.test( + 'createInstallation: Create installation for Salesforce', + async () => { + log.info('First checking for existing Salesforce connection...'); + + // // First, get the connection details + const checkPrompt = `Use checkConnection to check if there is an active connection for provider "${SALESFORCE_PROVIDER}"`; + + const checkResult = await generateText({ + model: openai('gpt-4o-mini'), + tools: { checkConnection }, + maxSteps: 5, + prompt: checkPrompt, + }); + + const checkToolResults = checkResult.steps[0]?.toolResults; + assert( + checkToolResults && checkToolResults.length > 0, + 'Check connection should have returned results', + ); + + const connectionData = checkToolResults[0].result; + + // If no connection exists, provide helpful instructions + if (!connectionData.found) { + log.warn('No Salesforce connection found'); + log.info('To create a connection:'); + log.info(' 1. Run: pnpm test:startOAuth'); + log.info(' 2. Complete the OAuth flow in your browser'); + log.info(' 3. Then run this test again'); + throw new Error( + 'A Salesforce connection must exist to create an installation. Please run startOAuth first.', + ); + } + + assert( + connectionData.connectionId, + 'Connection should have a connectionId', + ); + assert(connectionData.groupRef, 'Connection should have a groupRef'); + + log.success( + `Found connection: ${connectionData.connectionId}, groupRef: ${connectionData.groupRef}`, + ); + console.log( + 'Full connection data:', + JSON.stringify(connectionData, null, 2), + ); + + // // Check if installation already exists + log.info('Checking if installation already exists...'); + + const checkInstPrompt = `Use checkInstallation to check if there is an active installation for provider "${SALESFORCE_PROVIDER}"`; + + const checkInstResult = await generateText({ + model: openai('gpt-4o-mini'), + tools: { checkInstallation }, + maxSteps: 5, + prompt: checkInstPrompt, + }); + + const checkInstToolResults = checkInstResult.steps[0]?.toolResults; + assert( + checkInstToolResults && checkInstToolResults.length > 0, + 'Check installation should have returned results', + ); + + const installationData = checkInstToolResults[0].result; + + if (installationData.found) { + log.warn('Installation already exists - skipping creation'); + log.info( + `Existing installation ID: ${installationData.installationId}`, + ); + log.success( + 'Test passed: Installation exists (either pre-existing or previously created)', + ); + log.info( + 'To run the test by creating a new installation, delete installation from Ampersand Dashboard and run the test again', + ); + return; + } + + const connectionId = connectionData.connectionId; + const groupRef = connectionData.groupRef; + + log.debug(`Using connectionId: ${connectionId}`); + log.debug(`Using environment groupRef: ${groupRef}`); + log.debug(`Connection data groupRef: ${groupRef}`); + + // Use the connection's groupRef since it's a known valid group + const createPrompt = `Use createInstallation with these exact parameters: + provider: "${SALESFORCE_PROVIDER}" + connectionId: "${connectionId}" + groupRef: "${groupRef}"`; + + log.info( + `Creating Salesforce installation with connectionId: ${connectionId}, groupRef: ${groupRef}...`, + ); + + const createResult = await generateText({ + model: openai('gpt-4o-mini'), + tools: { createInstallation }, + maxSteps: 5, + prompt: createPrompt, + }); + + log.debug(`AI Response: ${createResult.text}`); + + // Verify tool was called + const toolCalls = createResult.steps[0]?.toolCalls; + assert(toolCalls && toolCalls.length > 0, 'Tool should have been called'); + assert( + toolCalls[0].toolName === 'createInstallation', + 'Should call createInstallation tool', + ); + + // Verify result structure + const toolResults = createResult.steps[0]?.toolResults; + assert( + toolResults && toolResults.length > 0, + 'Tool should have returned results', + ); + + const toolResult = toolResults[0].result; + assert('created' in toolResult, 'Result should have "created" field'); + assert( + typeof toolResult.created === 'boolean', + '"created" should be boolean', + ); + + log.success(`Installation created: ${toolResult.created}`); + if (toolResult.installationId) { + log.info(`Installation ID: ${toolResult.installationId}`); + log.info( + 'To run the test again, delete installation from Ampersand Dashboard and run the test again', + ); + } + }, + ); + + runner.summarize(); +} + +main();