Unify bedrock & openai client creation for integ tests (#340)

In order to better share code between browser and node integ tests, use a single implementation for creating clients and inside of the factory methods, do the conditional check for what environment we're running in.

As part of this I refactored the setup to take advantage of vitest features of having the global setup inject data into the running tests - including whether we're running in the browser and if we're running in CI/CD.

---------

Co-authored-by: Mackenzie Zastrow <zastrowm@users.noreply.github.com>
This commit is contained in:
Mackenzie Zastrow
2025-12-24 12:55:54 -05:00
committed by GitHub
co-authored by Mackenzie Zastrow
parent 7fe52cbd5f
commit 2fb71db7c1
21 changed files with 330 additions and 329 deletions
+113
View File
@@ -0,0 +1,113 @@
/**
* Global setup that runs once before all integration tests and possibly runs in the *parent* process.
*
* _setup-test on the other hand runs in the *child* 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.
*/
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,
}
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Test setup that runs once before all integration tests, but in the *child* process.
*
* _setup-global on the other hand 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),
})
}
})
@@ -0,0 +1,52 @@
/**
* Contains helpers for creating various model providers that work both in node & the browser
*/
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 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 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]
@@ -1,4 +1,3 @@
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import type { ContentBlock, Message } from '$/sdk/types/messages.js'
/**
@@ -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
}
}
+7 -1
View File
@@ -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')
}
/**
+4 -20
View File
@@ -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({
@@ -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 () => {
@@ -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,
})
+44 -47
View File
@@ -1,52 +1,49 @@
import { describe, it, expect } from 'vitest'
import { Agent, BedrockModel } from '$/sdk/index.js'
import { describe, expect, it } from 'vitest'
import { Agent } from '$/sdk/index.js'
import { bash } from '$/sdk/vended-tools/bash/index.js'
import { getMessageText, shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'
import { getMessageText } from './__fixtures__/model-test-helpers.js'
import { bedrock } from './__fixtures__/model-providers.js'
describe.skipIf((await shouldSkipBedrockTests()) || process.platform === 'win32')(
'Bash Tool Integration',
{ timeout: 60000 },
() => {
// Shared agent configuration for all tests
const createAgent = () =>
new Agent({
model: new BedrockModel({
region: 'us-east-1',
}),
tools: [bash],
})
describe('basic execution', () => {
it('captures stdout streams correctly', async () => {
const agent = createAgent()
const stdoutResult = await agent.invoke('Use bash to echo "Hello from bash"')
expect(getMessageText(stdoutResult.lastMessage)).toContain('Hello from bash')
})
it('captures stderr streams correctly', async () => {
const agent = createAgent()
const stderrResult = await agent.invoke('Use bash to run: echo "error" >&2')
expect(getMessageText(stderrResult.lastMessage)).toContain('error')
})
it('handles complex command patterns', async () => {
const agent = createAgent()
// Test command sequencing
const seqResult = await agent.invoke('Use bash to: create a variable TEST=hello, then echo it')
expect(getMessageText(seqResult.lastMessage).toLowerCase()).toContain('hello')
})
describe.skipIf(bedrock.skip || process.platform === 'win32')('Bash Tool Integration', () => {
// Shared agent configuration for all tests
const createAgent = () =>
new Agent({
model: bedrock.createModel({
region: 'us-east-1',
}),
tools: [bash],
})
describe('error handling', () => {
it('handles command errors gracefully', async () => {
const agent = createAgent()
const result = await agent.invoke('Use bash to run: nonexistent_command_xyz')
// Should indicate command not found or error
const lastMessage = getMessageText(result.lastMessage).toLowerCase()
expect(lastMessage).toMatch(/not found|error|command/)
})
describe('basic execution', () => {
it('captures stdout streams correctly', async () => {
const agent = createAgent()
const stdoutResult = await agent.invoke('Use bash to echo "Hello from bash"')
expect(getMessageText(stdoutResult.lastMessage)).toContain('Hello from bash')
})
}
)
it('captures stderr streams correctly', async () => {
const agent = createAgent()
const stderrResult = await agent.invoke('Use bash to run: echo "error" >&2')
expect(getMessageText(stderrResult.lastMessage)).toContain('error')
})
it('handles complex command patterns', async () => {
const agent = createAgent()
// Test command sequencing
const seqResult = await agent.invoke('Use bash to: create a variable TEST=hello, then echo it')
expect(getMessageText(seqResult.lastMessage).toLowerCase()).toContain('hello')
})
})
describe('error handling', () => {
it('handles command errors gracefully', async () => {
const agent = createAgent()
const result = await agent.invoke('Use bash to run: nonexistent_command_xyz')
// Should indicate command not found or error
const lastMessage = getMessageText(result.lastMessage).toLowerCase()
expect(lastMessage).toMatch(/not found|error|command/)
})
})
})
+17 -18
View File
@@ -1,22 +1,21 @@
import { describe, it, expect, vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
BedrockModel,
Message,
Agent,
TextBlock,
Message,
NullConversationManager,
SlidingWindowConversationManager,
TextBlock,
FunctionTool,
} from '@strands-agents/sdk'
import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'
import { bedrock } from './__fixtures__/model-providers.js'
describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests', () => {
describe.skipIf(bedrock.skip)('BedrockModel Integration Tests', () => {
describe('Streaming', () => {
describe('Configuration', () => {
it.concurrent('respects maxTokens configuration', async () => {
const provider = new BedrockModel({ maxTokens: 20 })
const provider = bedrock.createModel({ maxTokens: 20 })
const messages: Message[] = [
{
type: 'message',
@@ -35,7 +34,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
})
it.concurrent('uses system prompt cache on subsequent requests', async () => {
const provider = new BedrockModel({ maxTokens: 100 })
const provider = bedrock.createModel({ maxTokens: 100 })
const largeContext = `Context information: ${'hello '.repeat(2000)} [test-${Date.now()}-${Math.random()}]`
const cachedSystemPrompt = [
{ type: 'textBlock' as const, text: 'You are a helpful assistant.' },
@@ -63,7 +62,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
})
it.concurrent('uses message cache points on subsequent requests', async () => {
const provider = new BedrockModel({ maxTokens: 100 })
const provider = bedrock.createModel({ maxTokens: 100 })
const largeContext = `Context information: ${'hello '.repeat(2000)} [test-${Date.now()}-${Math.random()}]`
const messagesWithCachePoint = (text: string): Message[] => [
{
@@ -91,7 +90,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
describe('Error Handling', () => {
it.concurrent('handles invalid model ID gracefully', async () => {
const provider = new BedrockModel({ modelId: 'invalid-model-id-that-does-not-exist' })
const provider = bedrock.createModel({ modelId: 'invalid-model-id-that-does-not-exist' })
const messages: Message[] = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }]
await expect(collectIterator(provider.stream(messages))).rejects.toThrow()
})
@@ -101,7 +100,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
describe('Agent with Conversation Manager', () => {
it('manages conversation history with SlidingWindowConversationManager', async () => {
const agent = new Agent({
model: new BedrockModel({ maxTokens: 100 }),
model: bedrock.createModel({ maxTokens: 100 }),
conversationManager: new SlidingWindowConversationManager({ windowSize: 4 }),
})
@@ -122,7 +121,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
it('throws ContextWindowOverflowError with NullConversationManager', async () => {
const agent = new Agent({
model: new BedrockModel({ maxTokens: 50 }),
model: bedrock.createModel({ maxTokens: 50 }),
conversationManager: new NullConversationManager(),
})
@@ -136,7 +135,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
describe('Region Configuration', () => {
it('uses explicit region when provided', async () => {
const provider = new BedrockModel({
const provider = bedrock.createModel({
region: 'us-east-1',
maxTokens: 50,
})
@@ -152,7 +151,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
vi.stubEnv('AWS_REGION', undefined)
vi.stubEnv('AWS_DEFAULT_REGION', undefined)
const provider = new BedrockModel({
const provider = bedrock.createModel({
maxTokens: 50,
})
@@ -176,7 +175,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
// Use vitest to stub the environment variable
vi.stubEnv('AWS_REGION', 'eu-central-1')
const provider = new BedrockModel({
const provider = bedrock.createModel({
maxTokens: 50,
})
@@ -190,7 +189,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
// Use vitest to stub the environment variable
vi.stubEnv('AWS_REGION', 'eu-west-1')
const provider = new BedrockModel({
const provider = bedrock.createModel({
region: 'ap-southeast-2',
maxTokens: 50,
})
@@ -202,7 +201,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
})
it('uses region from clientConfig when provided', async () => {
const provider = new BedrockModel({
const provider = bedrock.createModel({
clientConfig: { region: 'ap-northeast-1' },
maxTokens: 50,
})
@@ -239,7 +238,7 @@ describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests'
describe('Thinking Mode with Tools', () => {
it('handles thinking mode with tool use', async () => {
const bedrockModel = new BedrockModel({
const bedrockModel = bedrock.createModel({
modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0',
additionalRequestFields: {
thinking: {
+7 -35
View File
@@ -1,8 +1,5 @@
import { describe, it, expect } from 'vitest'
import { commands } from 'vitest/browser'
import { Agent, DocumentBlock, ImageBlock, Message, TextBlock, tool } from '@strands-agents/sdk'
import { BedrockModel } from '@strands-agents/sdk/bedrock'
import { OpenAIModel } from '@strands-agents/sdk/openai'
import { notebook } from '@strands-agents/sdk/vended_tools/notebook'
import { httpRequest } from '@strands-agents/sdk/vended_tools/http_request'
import { z } from 'zod'
@@ -12,6 +9,7 @@ import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
// Import fixtures
import yellowPngUrl from '../__resources__/yellow.png?url'
import { loadFixture } from '../__fixtures__/test-helpers.js'
import { allProviders } from '../__fixtures__/model-providers.js'
// Calculator tool for testing
const calculatorTool = tool({
@@ -33,36 +31,10 @@ const calculatorTool = tool({
},
})
// Provider configurations with browser credential handling
const providers = [
{
name: 'BedrockModel',
createModel: async () => {
const credentials = await commands.getAwsCredentials()
return new BedrockModel({
region: 'us-east-1',
clientConfig: {
credentials,
},
})
},
},
{
name: 'OpenAIModel',
createModel: async () =>
new OpenAIModel({
apiKey: await commands.getOpenAIAPIKey(),
clientConfig: {
dangerouslyAllowBrowser: true,
},
}),
},
]
describe.each(providers)('Agent Browser Tests with $name', async ({ name, createModel }) => {
describe(`${name} Browser Integration`, () => {
describe.each(allProviders)('Agent Browser Tests with $name', async ({ name, skip, createModel }) => {
describe.skipIf(skip)(`${name} Browser Integration`, () => {
it('handles basic invocation', async () => {
const agent = new Agent({ model: await createModel(), printer: false })
const agent = new Agent({ model: createModel(), printer: false })
const result = await agent.invoke('Say hello in one word')
expect(result.stopReason).toBe('endTurn')
@@ -72,7 +44,7 @@ describe.each(providers)('Agent Browser Tests with $name', async ({ name, create
it('handles tool use', async () => {
const agent = new Agent({
model: await createModel(),
model: createModel(),
printer: false,
systemPrompt: 'Use the calculator tool to solve math problems. Respond with only the numeric result.',
tools: [calculatorTool],
@@ -103,7 +75,7 @@ describe.each(providers)('Agent Browser Tests with $name', async ({ name, create
})
const agent = new Agent({
model: await createModel(),
model: createModel(),
messages: [
new Message({
role: 'user',
@@ -126,7 +98,7 @@ describe.each(providers)('Agent Browser Tests with $name', async ({ name, create
it('handles tool invocation', async () => {
const agent = new Agent({
model: await createModel(),
model: createModel(),
tools: [notebook, httpRequest],
printer: false,
})
+5 -12
View File
@@ -1,8 +1,7 @@
import { describe, it, expect } from 'vitest'
import { BedrockModel } from '@strands-agents/sdk/bedrock'
import { Message, TextBlock } from '@strands-agents/sdk'
import { commands } from 'vitest/browser'
import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { bedrock } from '../__fixtures__/model-providers.js'
describe('Region Configuration', () => {
const sayHighMessage = Message.fromMessageData({
@@ -11,12 +10,9 @@ describe('Region Configuration', () => {
})
it('uses explicit region when provided', async () => {
const provider = new BedrockModel({
const provider = bedrock.createModel({
region: 'us-east-1',
maxTokens: 50,
clientConfig: {
credentials: await commands.getAwsCredentials(),
},
})
// Validate region configuration by checking config.region() directly
@@ -29,11 +25,8 @@ describe('Region Configuration', () => {
})
it('defaults to us-west-2 when no region provided and AWS SDK does not resolve one', async () => {
const provider = new BedrockModel({
const provider = bedrock.createModel({
maxTokens: 50,
clientConfig: {
credentials: await commands.getAwsCredentials(),
},
})
// Validate region defaults to us-west-2
@@ -46,8 +39,8 @@ describe('Region Configuration', () => {
})
it('uses region from clientConfig when provided', async () => {
const provider = new BedrockModel({
clientConfig: { region: 'ap-northeast-1', credentials: await commands.getAwsCredentials() },
const provider = bedrock.createModel({
clientConfig: { region: 'ap-northeast-1' },
maxTokens: 50,
})
-8
View File
@@ -1,8 +0,0 @@
import type { AwsCredentialIdentity } from '@aws-sdk/types'
declare module 'vitest/browser' {
interface BrowserCommands {
getAwsCredentials: () => Promise<AwsCredentialIdentity>
getOpenAIAPIKey: () => Promise<string>
}
}
+5 -5
View File
@@ -1,19 +1,19 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Agent, BedrockModel } from '$/sdk/index.js'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { Agent } from '$/sdk/index.js'
import { fileEditor } from '$/sdk/vended-tools/file_editor/index.js'
import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'
import { promises as fs } from 'fs'
import * as path from 'path'
import { tmpdir } from 'os'
import { bedrock } from './__fixtures__/model-providers.js'
describe.skipIf(await shouldSkipBedrockTests())('FileEditor Tool Integration', () => {
describe.skipIf(bedrock.skip)('FileEditor Tool Integration', () => {
let testDir: string
// Shared agent configuration for all tests
const createAgent = () =>
new Agent({
model: new BedrockModel({
model: bedrock.createModel({
region: 'us-east-1',
}),
tools: [fileEditor],
+5 -5
View File
@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest'
import { describe, expect, it } from 'vitest'
import { httpRequest } from '@strands-agents/sdk/vended_tools/http_request'
import { Agent, BedrockModel } from '@strands-agents/sdk'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'
import { Agent } from '@strands-agents/sdk'
import { bedrock } from './__fixtures__/model-providers.js'
describe.skipIf(await shouldSkipBedrockTests())('httpRequest tool (integration)', () => {
describe.skipIf(bedrock.skip)('httpRequest tool (integration)', () => {
it('agent uses http_request tool to fetch weather from Open-Meteo', async () => {
const agent = new Agent({
model: new BedrockModel({ maxTokens: 500 }),
model: bedrock.createModel({ maxTokens: 500 }),
tools: [httpRequest],
printer: false,
})
-63
View File
@@ -1,63 +0,0 @@
/**
* Global setup that runs once before all integration tests
* Loads API keys from AWS Secrets Manager into environment variables
*/
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
async function loadApiKeysFromSecretsManager(): Promise<void> {
// Load API keys as environment variables from AWS Secrets Manager
const client = new SecretsManagerClient({
region: process.env.AWS_REGION || 'us-east-1',
})
console.log('Loading API keys from Secrets Manager')
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}`)
}
}
}
export async function setup(): Promise<void> {
console.log('Global setup: Loading API keys from Secrets Manager...')
try {
await loadApiKeysFromSecretsManager()
console.log('Global setup complete: API keys loaded into environment')
} catch (error) {
console.error('Global setup failed:', error)
}
}
+3 -3
View File
@@ -7,13 +7,13 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { McpClient, Agent } from '@strands-agents/sdk'
import { BedrockModel } from '@strands-agents/sdk/bedrock'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { resolve } from 'node:path'
import { URL } from 'node:url'
import { startHTTPServer, type HttpServerInfo } from './__fixtures__/test-mcp-server.js'
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { bedrock } from './__fixtures__/model-providers.js'
type TransportConfig = {
name: string
@@ -64,7 +64,7 @@ describe('MCP Integration Tests', () => {
describe.each(transports)('$name transport', ({ createClient }) => {
it('agent can use multiple MCP tools in a conversation', async () => {
const client = await createClient()
const model = new BedrockModel({ maxTokens: 300 })
const model = bedrock.createModel({ maxTokens: 300 })
const agent = new Agent({
systemPrompt:
@@ -97,7 +97,7 @@ describe('MCP Integration Tests', () => {
it('agent handles MCP tool errors gracefully', async () => {
const client = await createClient()
const model = new BedrockModel({ maxTokens: 200 })
const model = bedrock.createModel({ maxTokens: 200 })
const agent = new Agent({
systemPrompt: 'You are a helpful assistant. If asked to test errors, use the error_tool.',
+6 -6
View File
@@ -1,14 +1,14 @@
import { describe, it, expect } from 'vitest'
import { Agent, BedrockModel } from '$/sdk/index.js'
import type { AgentStreamEvent, AgentResult } from '$/sdk/index.js'
import { describe, expect, it } from 'vitest'
import type { AgentResult, AgentStreamEvent } from '$/sdk/index.js'
import { Agent } from '$/sdk/index.js'
import { notebook } from '$/sdk/vended-tools/notebook/index.js'
import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'
import { bedrock } from './__fixtures__/model-providers.js'
describe.skipIf(await shouldSkipBedrockTests())('Notebook Tool Integration', () => {
describe.skipIf(bedrock.skip)('Notebook Tool Integration', () => {
// Shared agent configuration for all tests
const agentParams = {
model: new BedrockModel({
model: bedrock.createModel({
region: 'us-east-1',
}),
tools: [notebook],
+11 -12
View File
@@ -1,16 +1,15 @@
import { describe, it, expect } from 'vitest'
import { OpenAIModel } from '@strands-agents/sdk/openai'
import { Message } from '@strands-agents/sdk'
import { describe, expect, it } from 'vitest'
import type { ToolSpec } from '@strands-agents/sdk'
import { Message } from '@strands-agents/sdk'
import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipOpenAITests } from './__fixtures__/model-test-helpers.js'
import { openai } from './__fixtures__/model-providers.js'
describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () => {
describe.skipIf(openai.skip)('OpenAIModel Integration Tests', () => {
describe('Configuration', () => {
it.concurrent('respects maxTokens configuration', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
maxTokens: 20, // Very small limit
})
@@ -34,7 +33,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
})
it.concurrent('respects temperature configuration', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
temperature: 0, // Deterministic
maxTokens: 50,
@@ -77,7 +76,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
describe('Error Handling', () => {
it.concurrent('handles invalid model ID gracefully', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'invalid-model-id-that-does-not-exist-xyz',
})
@@ -99,7 +98,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
describe('Content Block Lifecycle', () => {
it.concurrent('emits complete content block lifecycle events', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
maxTokens: 50,
})
@@ -139,7 +138,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
describe('Stop Reasons', () => {
it.concurrent('returns endTurn stop reason for natural completion', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
maxTokens: 100,
})
@@ -159,7 +158,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
})
it.concurrent('returns maxTokens stop reason when token limit reached', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
maxTokens: 10, // Very small limit to force cutoff
})
@@ -179,7 +178,7 @@ describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () =>
})
it.concurrent('returns toolUse stop reason when requesting tool use', async () => {
const provider = new OpenAIModel({
const provider = openai.createModel({
modelId: 'gpt-4o-mini',
maxTokens: 200,
})
+1 -2
View File
@@ -3,8 +3,7 @@
"compilerOptions": {
"paths": {
"$/sdk/*": ["../../src/*"]
},
"types": ["vite/client", "vitest/importMeta", "@types/node"]
}
},
"references": [{ "path": "../../src/tsconfig.json" }]
}
+17
View File
@@ -0,0 +1,17 @@
import 'vitest'
import type { AwsCredentialIdentity } from '@aws-sdk/types'
declare module 'vitest' {
export interface ProvidedContext {
isCI: boolean
isBrowser: boolean
['provider-openai']: {
shouldSkip: boolean
apiKey: string | undefined
}
['provider-bedrock']: {
shouldSkip: boolean
credentials: AwsCredentialIdentity | undefined
}
}
}
+2 -2
View File
@@ -27,6 +27,6 @@
"verbatimModuleSyntax": true,
"sourceMap": true,
"removeComments": false,
"types": ["vitest/importMeta"]
"types": ["vite/client", "vitest/importMeta", "@types/node"]
}
}
}
+10 -24
View File
@@ -1,9 +1,6 @@
import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import type { AwsCredentialIdentity } from '@aws-sdk/types'
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import type { BrowserCommand } from 'vitest/node'
import path from 'path'
import * as path from 'node:path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -15,21 +12,12 @@ if (process.platform === 'win32') {
coverageExclude.push('src/vended-tools/bash/**')
}
const getAwsCredentials: BrowserCommand<[], AwsCredentialIdentity> = async ({ testPath, provider }) => {
const credentialProvider = fromNodeProviderChain()
return await credentialProvider()
}
const getOpenAIAPIKey: BrowserCommand<[], string | undefined> = async ({ testPath, provider }) => {
return process.env.OPENAI_API_KEY
}
export default defineConfig({
test: {
unstubEnvs: true,
reporters: [
'default',
['junit', { outputFile: 'test/.artifacts/test-report/junit/report.xml' }],
['junit', { outputFile: 'test/.artifacts/test-report/junit/report.xml', includeConsoleOutput: true }],
['json', { outputFile: 'test/.artifacts/test-report/json/report.json' }],
],
projects: [
@@ -53,6 +41,7 @@ export default defineConfig({
browser: {
enabled: true,
provider: playwright(),
headless: true,
screenshotDirectory: 'test/.artifacts/browser-screenshots/',
instances: [
{
@@ -71,9 +60,10 @@ export default defineConfig({
include: ['test/integ/**/*.test.ts'],
exclude: ['test/integ/**/*.browser.test.ts'],
name: { label: 'integ-node', color: 'magenta' },
testTimeout: 30000,
testTimeout: 60 * 1000,
retry: 1,
globalSetup: './test/integ/integ-setup.ts',
globalSetup: './test/integ/__fixtures__/_setup-global.ts',
setupFiles: './test/integ/__fixtures__/_setup-test.ts',
sequence: {
concurrent: true,
},
@@ -87,24 +77,20 @@ export default defineConfig({
},
include: ['test/integ/**/*.browser.test.ts'],
name: { label: 'integ-browser', color: 'yellow' },
testTimeout: 30000,
testTimeout: 60 * 1000,
browser: {
enabled: true,
provider: playwright(),
headless: true,
screenshotDirectory: 'test/.artifacts/browser-screenshots/',
instances: [
{
browser: 'chromium',
},
],
// These act as passthrough commands that browser tests can use to communicate with the test server running in node.
// This allows browsers to get access to credential secrets
commands: {
getAwsCredentials,
getOpenAIAPIKey,
},
},
globalSetup: './test/integ/integ-setup.ts',
globalSetup: './test/integ/__fixtures__/_setup-global.ts',
setupFiles: './test/integ/__fixtures__/_setup-test.ts',
sequence: {
concurrent: true,
},