Skip to content
Draft
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
2,412 changes: 2,075 additions & 337 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@
"author": "Strands Agents",
"license": "Apache-2.0",
"devDependencies": {
"@aws-sdk/client-s3": "^3.943.0",
"@aws-sdk/client-secrets-manager": "^3.943.0",
"@aws-sdk/client-sts": "^3.943.0",
"@aws-sdk/credential-providers": "^3.943.0",
"@types/json-schema": "^7.0.15",
"@types/node": "^24.6.0",
Expand Down
27 changes: 27 additions & 0 deletions test/integ/__fixtures__/_setup-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-sec
import type { TestProject } from 'vitest/node'
import type { ProvidedContext } from 'vitest'
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import { getS3TestResources } from './s3-test-helper.js'

/**
* Load API keys as environment variables from AWS Secrets Manager
Expand Down Expand Up @@ -70,6 +71,7 @@ export async function setup(project: TestProject): Promise<void> {
project.provide('isCI', isCI)
project.provide('provider-openai', await getOpenAITestContext(isCI))
project.provide('provider-bedrock', await getBedrockTestContext(isCI))
project.provide('s3-resources', await getS3TestContext(isCI))
}

async function getOpenAITestContext(isCI: boolean): Promise<ProvidedContext['provider-openai']> {
Expand Down Expand Up @@ -111,3 +113,28 @@ async function getBedrockTestContext(isCI: boolean): Promise<ProvidedContext['pr
}
}
}

async function getS3TestContext(isCI: boolean): Promise<ProvidedContext['s3-resources']> {
try {
const resources = await getS3TestResources()
console.log('⏭️ S3 resources uploaded - S3 location tests will run')
return {
shouldSkip: false,
imageUri: resources.imageUri,
documentUri: resources.documentUri,
videoUri: resources.videoUri,
}
} catch (error) {
console.log('⏭️ S3 resources not available - S3 location tests will be skipped')
console.log(` Error: ${error instanceof Error ? error.message : String(error)}`)
if (isCI) {
throw new Error('CI/CD should be running all tests')
}
return {
shouldSkip: true,
imageUri: undefined,
documentUri: undefined,
videoUri: undefined,
}
}
}
138 changes: 138 additions & 0 deletions test/integ/__fixtures__/s3-test-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* S3 test helper utilities for integration tests.
*
* Provides functions to upload test resources to S3 and return their URIs
* for use in media block integration tests.
*/

import { S3Client, CreateBucketCommand, HeadBucketCommand, PutObjectCommand } from '@aws-sdk/client-s3'
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import * as fs from 'fs'
import * as path from 'path'
import { fileURLToPath } from 'url'

const S3_REGION = 'us-west-2'

/**
* S3 test resources containing URIs for uploaded test files.
*/
export interface S3TestResources {
imageUri: string
documentUri: string
videoUri: string
}

/**
* Gets the current AWS account ID using STS.
*
* @returns AWS account ID
*/
async function getAccountId(): Promise<string> {
const stsClient = new STSClient({
region: S3_REGION,
credentials: fromNodeProviderChain(),
})

const response = await stsClient.send(new GetCallerIdentityCommand({}))
return response.Account!
}

/**
* Ensures the test bucket exists, creating it if necessary.
*
* @param s3Client - S3 client
* @param bucketName - Bucket name to create/verify
*/
async function ensureBucket(s3Client: S3Client, bucketName: string): Promise<void> {
try {
await s3Client.send(new HeadBucketCommand({ Bucket: bucketName }))
console.log(`Bucket ${bucketName} already exists`)
} catch {
try {
await s3Client.send(
new CreateBucketCommand({
Bucket: bucketName,
CreateBucketConfiguration: {
LocationConstraint: S3_REGION,
},
})
)
console.log(`Created test bucket: ${bucketName}`)
// Wait for bucket to be available
await new Promise((resolve) => globalThis.setTimeout(resolve, 2000))
} catch (createError) {
// Bucket may already exist if created by another run
const errorMessage = createError instanceof Error ? createError.message : String(createError)
if (!errorMessage.includes('BucketAlreadyOwnedByYou')) {
throw createError
}
console.log(`Bucket ${bucketName} already exists (owned by you)`)
}
}
}

/**
* Uploads test resources to S3.
*
* @param s3Client - S3 client
* @param bucketName - Target bucket name
* @returns S3 URIs for uploaded resources
*/
async function uploadTestResources(s3Client: S3Client, bucketName: string): Promise<S3TestResources> {
// Get the directory of this module
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const resourcesDir = path.join(__dirname, '..', '__resources__')

// Define test files to upload
const files = [
{ localPath: path.join(resourcesDir, 'yellow.png'), key: 'test-images/yellow.png', contentType: 'image/png' },
{
localPath: path.join(resourcesDir, 'letter.pdf'),
key: 'test-documents/letter.pdf',
contentType: 'application/pdf',
},
{ localPath: path.join(resourcesDir, 'blue.mp4'), key: 'test-videos/blue.mp4', contentType: 'video/mp4' },
]

// Upload each file
for (const file of files) {
const fileContent = fs.readFileSync(file.localPath)
await s3Client.send(
new PutObjectCommand({
Bucket: bucketName,
Key: file.key,
Body: fileContent,
ContentType: file.contentType,
})
)
console.log(`Uploaded test file to s3://${bucketName}/${file.key}`)
}

return {
imageUri: `s3://${bucketName}/test-images/yellow.png`,
documentUri: `s3://${bucketName}/test-documents/letter.pdf`,
videoUri: `s3://${bucketName}/test-videos/blue.mp4`,
}
}

/**
* Gets S3 test resources by uploading test files to S3.
* This is the main entry point for S3 test setup.
*
* @returns S3 test resources with URIs
* @throws Error if AWS credentials are unavailable
*/
export async function getS3TestResources(): Promise<S3TestResources> {
const accountId = await getAccountId()
const bucketName = `strands-integ-tests-resources-${accountId}`

const s3Client = new S3Client({
region: S3_REGION,
credentials: fromNodeProviderChain(),
})

await ensureBucket(s3Client, bucketName)
return await uploadTestResources(s3Client, bucketName)
}
Binary file added test/integ/__resources__/blue.mp4
Binary file not shown.
117 changes: 116 additions & 1 deletion test/integ/bedrock.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it, vi, inject } from 'vitest'
import {
Agent,
Message,
NullConversationManager,
SlidingWindowConversationManager,
TextBlock,
FunctionTool,
ImageBlock,
DocumentBlock,
VideoBlock,
} from '@strands-agents/sdk'

import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
Expand Down Expand Up @@ -246,3 +249,115 @@ describe.skipIf(bedrock.skip)('BedrockModel Integration Tests', () => {
}, 30000)
})
})

describe('S3 Location Media', () => {
const s3Resources = () => inject('s3-resources')

describe.skipIf(inject('provider-bedrock').shouldSkip || inject('s3-resources').shouldSkip)(
'Bedrock S3 Location Integration Tests',
() => {
it.concurrent(
'processes image from S3 location',
async () => {
const resources = s3Resources()
const imageUri = resources.imageUri!

const messages: Message[] = [
{
type: 'message',
role: 'user',
content: [
new TextBlock('What colors do you see in this image?'),
new ImageBlock({
format: 'png',
source: { s3Location: { uri: imageUri } },
}),
],
},
]

const agent = new Agent({
model: bedrock.createModel({
modelId: 'us.amazon.nova-lite-v1:0',
maxTokens: 200,
}),
printer: false,
})

const result = await agent.invoke(messages)
expect(result.toString().length).toBeGreaterThan(0)
},
30000
)

it.concurrent(
'processes document from S3 location',
async () => {
const resources = s3Resources()
const documentUri = resources.documentUri!

const messages: Message[] = [
{
type: 'message',
role: 'user',
content: [
new TextBlock('Please summarize this document briefly.'),
new DocumentBlock({
name: 'letter',
format: 'pdf',
source: { s3Location: { uri: documentUri } },
}),
],
},
]

const agent = new Agent({
model: bedrock.createModel({
modelId: 'us.amazon.nova-lite-v1:0',
maxTokens: 200,
}),
printer: false,
})

const result = await agent.invoke(messages)
expect(result.toString().length).toBeGreaterThan(0)
},
30000
)

it.concurrent(
'processes video from S3 location',
async () => {
const resources = s3Resources()
const videoUri = resources.videoUri!

const messages: Message[] = [
{
type: 'message',
role: 'user',
content: [
new TextBlock('What colors do you see in this video?'),
new VideoBlock({
format: 'mp4',
source: { s3Location: { uri: videoUri } },
}),
],
},
]

const agent = new Agent({
model: bedrock.createModel({
modelId: 'us.amazon.nova-pro-v1:0',
maxTokens: 200,
}),
printer: false,
})

const result = await agent.invoke(messages)
expect(result.toString().length).toBeGreaterThan(0)
},
60000
)
}
)
})
6 changes: 6 additions & 0 deletions test/integ/vitest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,11 @@ declare module 'vitest' {
shouldSkip: boolean
credentials: AwsCredentialIdentity | undefined
}
['s3-resources']: {
shouldSkip: boolean
imageUri: string | undefined
documentUri: string | undefined
videoUri: string | undefined
}
}
}
Loading