Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions sdk/lib/adapters/ampersand/core/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,15 @@ export async function checkConnectionHelper({
projectId = process.env.AMPERSAND_PROJECT_ID || '',
}: CheckConnectionParams): Promise<CheckConnectionResult> {
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,
Expand Down
41 changes: 22 additions & 19 deletions sdk/lib/adapters/ampersand/core/installation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SDKNodePlatform } from '@amp-labs/sdk-node-platform';
import { ListInstallationsInstallation } from '@amp-labs/sdk-node-platform/src/models/operations/listinstallations';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this import path uses src/ which I think won't exist in the published package.

either import from models/operations/listinstallations or re-export these types from the main entry point at the SDK

import { checkConnectionHelper } from './connection';
import * as Sentry from '@sentry/node';
interface CheckInstallationParams {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -81,26 +83,27 @@ export async function createInstallationHelper({
projectId = process.env.AMPERSAND_PROJECT_ID || '',
integrationName = process.env.AMPERSAND_INTEGRATION_NAME || '',
}: CreateInstallationParams): Promise<CreateInstallationResult> {
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;
Expand Down
1 change: 1 addition & 0 deletions test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
182 changes: 182 additions & 0 deletions test/tools/createInstallation.test.ts
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we assert toolResult.created === true? looks likeit passes even if the installation failed to create

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();