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
111 changes: 111 additions & 0 deletions test/integ/__fixtures__/_setup-global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Global setup that runs once before all integration tests and possibly runs in the *parent* process
*/

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
import type { TestProject } from 'vitest/node'
import type { ProvidedContext } from 'vitest'
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'

/**
* Load API keys as environment variables from AWS Secrets Manager
*/
async function loadApiKeysFromSecretsManager(): Promise<void> {
const client = new SecretsManagerClient({
region: process.env.AWS_REGION || 'us-east-1',
})

try {
const secretName = 'model-provider-api-key'
const command = new GetSecretValueCommand({
SecretId: secretName,
})
const response = await client.send(command)

if (response.SecretString) {
const secret = JSON.parse(response.SecretString)
// Only add API keys for currently supported providers
const supportedProviders = ['openai']
Object.entries(secret).forEach(([key, value]) => {
if (supportedProviders.includes(key.toLowerCase())) {
process.env[`${key.toUpperCase()}_API_KEY`] = String(value)
}
})
}
} catch (e) {
console.warn('Error retrieving secret', e)
}

/*
* Validate that required environment variables are set when running in GitHub Actions.
* This prevents tests from being unintentionally skipped due to missing credentials.
*/
if (process.env.GITHUB_ACTIONS !== 'true') {
console.warn('Tests running outside GitHub Actions, skipping required provider validation')
return
}

const requiredProviders: Set<string> = new Set(['OPENAI_API_KEY'])

for (const provider of requiredProviders) {
if (!process.env[provider]) {
throw new Error(`Missing required environment variables for ${provider}`)
}
}
}

/**
* Perform shared setup for the integration tests unless it's already been.
*/
export async function setup(project: TestProject): Promise<void> {
console.log('Global setup: Loading API keys from Secrets Manager...')
await loadApiKeysFromSecretsManager()
console.log('Global setup: API keys loaded into environment')

const isCI = !!globalThis.process.env.CI

project.provide('isBrowser', project.isBrowserEnabled())
project.provide('isCI', isCI)
project.provide('provider-openai', await getOpenAITestContext(isCI))
project.provide('provider-bedrock', await getBedrockTestContext(isCI))
}

async function getOpenAITestContext(isCI: boolean): Promise<ProvidedContext['provider-openai']> {
const apiKey = process.env.OPENAI_API_KEY
const shouldSkip = !apiKey

if (shouldSkip) {
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
if (isCI) {
throw new Error('CI/CD should be running all tests')
}
} else {
console.log('⏭️ OpenAI API key available - integration tests will run')
}

return {
apiKey: apiKey,
shouldSkip: shouldSkip,
}
}

async function getBedrockTestContext(isCI: boolean): Promise<ProvidedContext['provider-bedrock']> {
try {
const credentialProvider = fromNodeProviderChain()
const credentials = await credentialProvider()
console.log('⏭️ Bedrock credentials available - integration tests will run')
return {
shouldSkip: false,
credentials: credentials,
}
} catch {
console.log('⏭️ Bedrock credentials not available - integration tests will be skipped')
if (isCI) {
throw new Error('CI/CD should be running all tests')
}
return {
shouldSkip: true,
credentials: undefined,
}
}
}
19 changes: 19 additions & 0 deletions test/integ/__fixtures__/_setup-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Global setup that runs once before all integration tests and possibly runs in the *parent* process
*/

import { beforeAll } from 'vitest'
import { configureLogging } from '$/sdk/logging/index.js'
import { isCI } from './test-helpers.js'

beforeAll(() => {
// When running under CI/CD, preserve all logs including debug
if (isCI()) {
configureLogging({
debug: (...args: unknown[]) => console.debug(...args),
info: (...args: unknown[]) => console.info(...args),
warn: (...args: unknown[]) => console.warn(...args),
error: (...args: unknown[]) => console.error(...args),
})
}
})
53 changes: 53 additions & 0 deletions test/integ/__fixtures__/model-providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Contains helpers for creating various model providers that work both in node & the browser
*/

import { isCI } from './test-helpers.js'
import { inject } from 'vitest'
import { BedrockModel, type BedrockModelOptions } from '$/sdk/models/bedrock.js'
import { OpenAIModel, type OpenAIModelOptions } from '$/sdk/models/openai.js'

export const bedrock = {
name: 'BedrockModel',
get skip() {
return !isCI() && inject('provider-bedrock').shouldSkip
},
createModel: (options: BedrockModelOptions = {}): BedrockModel => {
const credentials = inject('provider-bedrock').credentials
if (!credentials) {
throw new Error('No Bedrock credentials provided')
}

return new BedrockModel({
...options,
clientConfig: {
...(options.clientConfig ?? {}),
credentials: credentials,
},
})
},
}

export const openai = {
name: 'OpenAIModel',
get skip() {
return !isCI() && inject('provider-openai').shouldSkip
},
createModel: (config: OpenAIModelOptions = {}): OpenAIModel => {
const apiKey = inject('provider-openai').apiKey
if (!apiKey) {
throw new Error('No OpenAI apiKey provided')
}

return new OpenAIModel({
...config,
apiKey: apiKey,
clientConfig: {
...(config.clientConfig ?? {}),
dangerouslyAllowBrowser: true,
},
})
},
}

export const allProviders = [bedrock, openai]
66 changes: 0 additions & 66 deletions test/integ/__fixtures__/model-test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import type { ContentBlock, Message } from '$/sdk/types/messages.js'

/**
Expand All @@ -19,68 +18,3 @@ export const getMessageText = (message: Message): string => {
.map((block) => block.text)
.join('\n')
}

/**
* Determines whether AWS integration tests should run based on environment and credentials.
*
* In CI environments, tests always run (credentials are expected to be configured).
* In local environments, tests run only if AWS credentials are available.
*
* @returns Promise<boolean> - true if tests should run, false if they should be skipped
*/
export async function shouldSkipBedrockTests(): Promise<boolean> {
// In a CI environment, we ALWAYS expect credentials to be configured.
// A failure is better than a skip.
if (process.env.CI) {
console.log('✅ Running in CI environment, integration tests will run.')
return false
}

// In a local environment, we check for credentials as a convenience.
try {
const credentialProvider = fromNodeProviderChain()
await credentialProvider()
console.log('✅ AWS credentials found locally, integration tests will run.')
return false
} catch {
console.log('⏭️ AWS credentials not available locally, integration tests will be skipped.')
return true
}
}

/**
* Determines if OpenAI integration tests should be skipped.
* In CI environments, throws an error if API key is missing (tests should not be skipped).
* In local development, skips tests if API key is not available.
*
* @returns true if tests should be skipped, false if they should run
* @throws Error if running in CI and API key is missing
*/
export function shouldSkipOpenAITests(): boolean {
try {
const isCI = !!process.env.CI
const hasKey = !!process.env.OPENAI_API_KEY

if (isCI && !hasKey) {
throw new Error('OpenAI API key must be available in CI environments')
}

if (hasKey) {
if (isCI) {
console.log('✅ Running in CI environment with OpenAI API key - tests will run')
} else {
console.log('✅ OpenAI API key found for integration tests')
}
return false
} else {
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
return true
}
} catch (error) {
if (error instanceof Error && error.message.includes('CI environments')) {
throw error
}
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
return true
}
}
8 changes: 7 additions & 1 deletion test/integ/__fixtures__/test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { inject } from 'vitest'

/**
* Checks whether we're running tests in the browser.
*/
export const isInBrowser = () => {
return globalThis?.process?.env == null
return inject('isBrowser')
}

export function isCI() {
return inject('isCI')
}

/**
Expand Down
24 changes: 4 additions & 20 deletions test/integ/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { describe, it, expect } from 'vitest'
import { describe, expect, it } from 'vitest'
import { Agent, DocumentBlock, ImageBlock, Message, TextBlock, tool } from '@strands-agents/sdk'
import { BedrockModel } from '@strands-agents/sdk/bedrock'
import { notebook } from '@strands-agents/sdk/vended_tools/notebook'
import { httpRequest } from '@strands-agents/sdk/vended_tools/http_request'
import { OpenAIModel } from '@strands-agents/sdk/openai'
import { z } from 'zod'

import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests, shouldSkipOpenAITests } from './__fixtures__/model-test-helpers.js'
import { loadFixture } from './__fixtures__/test-helpers.js'

// Import fixtures using Vite's ?url suffix
import yellowPngUrl from './__resources__/yellow.png?url'
import { allProviders } from './__fixtures__/model-providers.js'

// Calculator tool for testing
const calculatorTool = tool({
Expand All @@ -33,21 +31,7 @@ const calculatorTool = tool({
},
})

// Provider configurations
const providers = [
{
name: 'BedrockModel',
skip: await shouldSkipBedrockTests(),
createModel: () => new BedrockModel(),
},
{
name: 'OpenAIModel',
skip: shouldSkipOpenAITests(),
createModel: () => new OpenAIModel(),
},
]

describe.each(providers)('Agent with $name', ({ name, skip, createModel }) => {
describe.each(allProviders)('Agent with $name', ({ name, skip, createModel }) => {
describe.skipIf(skip)(`${name} Integration Tests`, () => {
describe('Basic Functionality', () => {
it('handles invocation, streaming, system prompts, and tool use', async () => {
Expand Down Expand Up @@ -242,7 +226,7 @@ describe.each(providers)('Agent with $name', ({ name, skip, createModel }) => {

it('handles tool invocation', async () => {
const agent = new Agent({
model: await createModel(),
model: createModel(),
tools: [notebook, httpRequest],
printer: false,
})
Expand Down
Loading