Skip to content
Merged
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
73 changes: 55 additions & 18 deletions test/integ/__fixtures__/model-test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
import type { Message, ContentBlock } from '$/sdk/types/messages.js'
import type { ContentBlock, Message } from '$/sdk/types/messages.js'

/**
* Extracts plain text content from a Message object.
*
* This helper function handles different message formats by:
* - Extracting text from Message objects by filtering for textBlock content blocks
* - Joining multiple text blocks with newlines
*
* @param message - The message to extract text from. Message object with content blocks
* @returns The extracted text content as a string, or empty string if no content is found
*/
export const getMessageText = (message: Message): string => {
if (!message.content) return ''

return message.content
.filter((block: ContentBlock) => block.type === 'textBlock')
.map((block) => block.text)
.join('\n')
}

/**
* Determines whether AWS integration tests should run based on environment and credentials.
Expand All @@ -9,41 +28,59 @@ import type { Message, ContentBlock } from '$/sdk/types/messages.js'
*
* @returns Promise<boolean> - true if tests should run, false if they should be skipped
*/
export async function shouldRunTests(): Promise<boolean> {
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 true
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 true
return false
} catch {
console.log('⏭️ AWS credentials not available locally, integration tests will be skipped.')
return false
return true
}
}

/**
* Extracts plain text content from a Message object.
* 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.
*
* This helper function handles different message formats by:
* - Extracting text from Message objects by filtering for textBlock content blocks
* - Joining multiple text blocks with newlines
*
* @param message - The message to extract text from. Message object with content blocks
* @returns The extracted text content as a string, or empty string if no content is found
* @returns true if tests should be skipped, false if they should run
* @throws Error if running in CI and API key is missing
*/
export const getMessageText = (message: Message): string => {
if (!message.content) return ''
export function shouldSkipOpenAITests(): boolean {
try {
const isCI = !!process.env.CI
const hasKey = !!process.env.OPENAI_API_KEY

return message.content
.filter((block: ContentBlock) => block.type === 'textBlock')
.map((block) => block.text)
.join('\n')
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
}
}
59 changes: 17 additions & 42 deletions test/integ/__fixtures__/test-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
/**
* Checks whether we're running tests in the browser.
*/
export const isInBrowser = () => {
return globalThis?.process?.env == null
}

/**
* Helper to load fixture files from Vite URL imports.
Expand All @@ -8,45 +12,16 @@ import { join } from 'node:path'
* @param url - The URL from a Vite ?url import
* @returns The file contents as a Uint8Array
*/
export const loadFixture = (url: string): Uint8Array => {
const relativePath = url.startsWith('/') ? url.slice(1) : url
const filePath = join(process.cwd(), relativePath)
return new Uint8Array(readFileSync(filePath))
}

/**
* 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 const 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
export async function loadFixture(url: string): Promise<Uint8Array> {
if (isInBrowser()) {
const response = await globalThis.fetch(url)
const arrayBuffer = await response.arrayBuffer()
return new Uint8Array(arrayBuffer)
} else {
const { join } = await import('node:path')
const { readFile } = await import('node:fs/promises')
const relativePath = url.startsWith('/') ? url.slice(1) : url
const filePath = join(process.cwd(), relativePath)
return new Uint8Array(await readFile(filePath))
}
}
8 changes: 4 additions & 4 deletions test/integ/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { OpenAIModel } from '@strands-agents/sdk/openai'
import { z } from 'zod'

import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldRunTests } from './__fixtures__/model-test-helpers.js'
import { loadFixture, shouldSkipOpenAITests } from './__fixtures__/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'
Expand Down Expand Up @@ -37,7 +37,7 @@ const calculatorTool = tool({
const providers = [
{
name: 'BedrockModel',
skip: !(await shouldRunTests()),
skip: await shouldSkipBedrockTests(),
createModel: () => new BedrockModel(),
},
{
Expand Down Expand Up @@ -144,7 +144,7 @@ describe.each(providers)('Agent with $name', ({ name, skip, createModel }) => {
})

// Create image block
const imageBytes = loadFixture(yellowPngUrl)
const imageBytes = await loadFixture(yellowPngUrl)
const imageBlock = new ImageBlock({
format: 'png',
source: { bytes: imageBytes },
Expand Down
4 changes: 2 additions & 2 deletions test/integ/bash.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest'
import { Agent, BedrockModel } from '$/sdk/index.js'
import { bash } from '$/sdk/vended-tools/bash/index.js'
import { getMessageText, shouldRunTests } from './__fixtures__/model-test-helpers.js'
import { getMessageText, shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'

describe.skipIf(!(await shouldRunTests()) || process.platform === 'win32')(
describe.skipIf((await shouldSkipBedrockTests()) || process.platform === 'win32')(
'Bash Tool Integration',
{ timeout: 60000 },
() => {
Expand Down
4 changes: 2 additions & 2 deletions test/integ/bedrock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
} from '@strands-agents/sdk'

import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldRunTests } from './__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'

describe.skipIf(!(await shouldRunTests()))('BedrockModel Integration Tests', () => {
describe.skipIf(await shouldSkipBedrockTests())('BedrockModel Integration Tests', () => {
describe('Streaming', () => {
describe('Configuration', () => {
it.concurrent('respects maxTokens configuration', async () => {
Expand Down
21 changes: 1 addition & 20 deletions test/integ/browser/agent.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,7 @@ import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'

// Import fixtures
import yellowPngUrl from '../__resources__/yellow.png?url'

// Environment detection for browser vs Node.js
const isNode = typeof process !== 'undefined' && typeof process.versions !== 'undefined' && !!process.versions.node

// Browser-compatible fixture loader
const loadFixture = async (url: string): Promise<Uint8Array> => {
if (isNode) {
// In Node.js, use synchronous file reading
const { readFileSync } = await import('node:fs')
const { join } = await import('node:path')
const relativePath = url.startsWith('/') ? url.slice(1) : url
const filePath = join(process.cwd(), relativePath)
return new Uint8Array(readFileSync(filePath))
} else {
// In browser, use fetch API
const response = await globalThis.fetch(url)
const arrayBuffer = await response.arrayBuffer()
return new Uint8Array(arrayBuffer)
}
}
import { loadFixture } from '../__fixtures__/test-helpers.js'

// Calculator tool for testing
const calculatorTool = tool({
Expand Down
4 changes: 2 additions & 2 deletions test/integ/file-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Agent, BedrockModel } from '$/sdk/index.js'
import { fileEditor } from '$/sdk/vended-tools/file_editor/index.js'
import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldRunTests } from './__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'

describe.skipIf(!(await shouldRunTests()))('FileEditor Tool Integration', () => {
describe.skipIf(await shouldSkipBedrockTests())('FileEditor Tool Integration', () => {
let testDir: string

// Shared agent configuration for all tests
Expand Down
4 changes: 2 additions & 2 deletions test/integ/http-request.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest'
import { httpRequest } from '@strands-agents/sdk/vended_tools/http_request'
import { Agent, BedrockModel } from '@strands-agents/sdk'
import { shouldRunTests } from './__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'

describe.skipIf(!(await shouldRunTests()))('httpRequest tool (integration)', () => {
describe.skipIf(await shouldSkipBedrockTests())('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 }),
Expand Down
4 changes: 2 additions & 2 deletions test/integ/notebook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Agent, BedrockModel } from '$/sdk/index.js'
import type { AgentStreamEvent, AgentResult } from '$/sdk/index.js'
import { notebook } from '$/sdk/vended-tools/notebook/index.js'
import { collectGenerator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldRunTests } from './__fixtures__/model-test-helpers.js'
import { shouldSkipBedrockTests } from './__fixtures__/model-test-helpers.js'

describe.skipIf(!(await shouldRunTests()))('Notebook Tool Integration', () => {
describe.skipIf(await shouldSkipBedrockTests())('Notebook Tool Integration', () => {
// Shared agent configuration for all tests
const agentParams = {
model: new BedrockModel({
Expand Down
3 changes: 2 additions & 1 deletion test/integ/openai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Message } from '@strands-agents/sdk'
import type { ToolSpec } from '@strands-agents/sdk'

import { collectIterator } from '$/sdk/__fixtures__/model-test-helpers.js'
import { shouldSkipOpenAITests } from './__fixtures__/test-helpers.js'

import { shouldSkipOpenAITests } from './__fixtures__/model-test-helpers.js'

describe.skipIf(shouldSkipOpenAITests())('OpenAIModel Integration Tests', () => {
describe('Configuration', () => {
Expand Down