Skip to content

Commit 882d743

Browse files
committed
Unify integ tests to run in browser & node
Rather than special casing the browser, run *all* integ tests in both browser & node by default, opting out when we need tests to run in only a single environment. For opting out, we now have two options: - For one off tests, use `skip: isInBrowser()` - For entire tests, use the file convention `thing.node.test.ts` and that will exclude it from browser tests This refactoring needed a bit of plumbing to make tests accommodate both environments, but I think it worked pretty well
1 parent a8223b5 commit 882d743

22 files changed

+309
-433
lines changed

.github/workflows/integration-test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ jobs:
8686
- name: Run integration tests
8787
run: npm run test:integ:all
8888

89-
- name: Upload browser test screenshots
89+
- name: Upload Artifacts
9090
if: always()
9191
uses: actions/upload-artifact@v5
9292
with:
93-
name: browser-test-screenshots
94-
path: tests_integ/browser/__screenshots__/
93+
name: test-artifacts
94+
path: ./test/.artifacts/
9595
retention-days: 4
9696
if-no-files-found: ignore

.github/workflows/test-lint.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,13 @@ jobs:
5353
run: npm run build
5454

5555
- name: Test packaging
56-
run: npm run test:package
56+
run: npm run test:package
57+
58+
- name: Upload Artifacts
59+
if: always()
60+
uses: actions/upload-artifact@v5
61+
with:
62+
name: test-artifacts
63+
path: ./test/.artifacts/
64+
retention-days: 4
65+
if-no-files-found: ignore

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ Thumbs.db
3737
.env.production.local
3838

3939
# Github workflow artifacts
40-
.artifact
40+
.artifact
41+
# Test artifacts
42+
test/.artifacts

package-lock.json

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
"./vended_tools/http_request": {
3434
"types": "./dist/vended_tools/http_request/index.d.ts",
3535
"default": "./dist/vended_tools/http_request/index.js"
36+
},
37+
"./vended_tools/bash": {
38+
"types": "./dist/vended_tools/bash/index.d.ts",
39+
"default": "./dist/vended_tools/bash/index.js"
3640
}
3741
},
3842
"scripts": {
@@ -78,6 +82,7 @@
7882
"@vitest/browser": "^4.0.15",
7983
"@vitest/browser-playwright": "^4.0.15",
8084
"@vitest/coverage-v8": "^4.0.15",
85+
"@vitest/ui": "^4.0.15",
8186
"eslint": "^9.0.0",
8287
"eslint-plugin-tsdoc": "^0.5.0",
8388
"husky": "^9.1.7",

src/models/openai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export class OpenAIModel extends Model<OpenAIModelConfig> {
265265
// In Node.js, can use OPENAI_API_KEY environment variable as fallback
266266
const hasEnvKey =
267267
typeof process !== 'undefined' && typeof process.env !== 'undefined' && process.env.OPENAI_API_KEY
268-
if (!apiKey && !hasEnvKey) {
268+
if (!apiKey && !hasEnvKey && !clientConfig?.apiKey) {
269269
throw new Error(
270270
"OpenAI API key is required. Provide it via the 'apiKey' option or set the OPENAI_API_KEY environment variable."
271271
)

tests_integ/__fixtures__/model-test-helpers.ts

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { fromNodeProviderChain } from '@aws-sdk/credential-providers'
2-
import type { Message, ContentBlock } from '../../src/types/messages.js'
1+
import { BedrockModel, type BedrockModelOptions } from '@strands-agents/sdk'
2+
import { OpenAIModel, type OpenAIModelOptions } from '@strands-agents/sdk/openai'
3+
4+
import type { Message, ContentBlock } from '@strands-agents/sdk'
5+
import { isInBrowser } from './test-helpers.js'
6+
7+
export * from '../../src/__fixtures__/model-test-helpers.js'
38

49
/**
510
* Determines whether AWS integration tests should run based on environment and credentials.
@@ -9,22 +14,71 @@ import type { Message, ContentBlock } from '../../src/types/messages.js'
914
*
1015
* @returns Promise<boolean> - true if tests should run, false if they should be skipped
1116
*/
12-
export async function shouldRunTests(): Promise<boolean> {
17+
export async function shouldSkipBedrockTests(): Promise<boolean> {
1318
// In a CI environment, we ALWAYS expect credentials to be configured.
1419
// A failure is better than a skip.
15-
if (process.env.CI) {
20+
if (globalThis.process?.env?.CI) {
1621
console.log('✅ Running in CI environment, integration tests will run.')
17-
return true
22+
return false
1823
}
1924

2025
// In a local environment, we check for credentials as a convenience.
2126
try {
22-
const credentialProvider = fromNodeProviderChain()
23-
await credentialProvider()
24-
console.log('✅ AWS credentials found locally, integration tests will run.')
25-
return true
27+
if (isInBrowser()) {
28+
const { commands } = await import('vitest/browser')
29+
await commands.getAwsCredentials()
30+
console.log('✅ credentials found via vitest, integration tests will run.')
31+
return false
32+
} else {
33+
const { fromNodeProviderChain } = await import('@aws-sdk/credential-providers')
34+
const credentialProvider = fromNodeProviderChain()
35+
await credentialProvider()
36+
console.log('✅ AWS credentials found locally, integration tests will run.')
37+
return false
38+
}
2639
} catch {
27-
console.log('⏭️ AWS credentials not available locally, integration tests will be skipped.')
40+
console.log('⏭️ AWS credentials not available, integration tests will be skipped.')
41+
return true
42+
}
43+
}
44+
45+
/**
46+
* Determines if OpenAI integration tests should be skipped.
47+
* In CI environments, throws an error if API key is missing (tests should not be skipped).
48+
* In local development, skips tests if API key is not available.
49+
*
50+
* @returns true if tests should be skipped, false if they should run
51+
* @throws Error if running in CI and API key is missing
52+
*/
53+
export const shouldSkipOpenAITests = async (): Promise<boolean> => {
54+
const getApiKey = async (): Promise<string> => {
55+
if (isInBrowser()) {
56+
const { commands } = await import('vitest/browser')
57+
return await commands.getOpenAIAPIKey()
58+
} else {
59+
return globalThis.process.env.OPENAI_API_KEY as string
60+
}
61+
}
62+
63+
// In a CI environment, we ALWAYS expect credentials to be configured.
64+
// A failure is better than a skip.
65+
if (globalThis.process?.env?.CI) {
66+
console.log('✅ Running in CI environment, integration tests will run.')
67+
const apiKey = await getApiKey()
68+
69+
if (!apiKey) {
70+
throw new Error('OpenAI API key must be available in CI environments')
71+
}
72+
73+
return false
74+
}
75+
76+
const apiKey = await getApiKey()
77+
if (!apiKey) {
78+
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
79+
return true
80+
} else {
81+
console.log('⏭️ OpenAI API key available - integration tests will run')
2882
return false
2983
}
3084
}
@@ -47,3 +101,38 @@ export const getMessageText = (message: Message): string => {
47101
.map((block) => block.text)
48102
.join('\n')
49103
}
104+
105+
export function createBedrockModel(options: BedrockModelOptions = {}) {
106+
if (isInBrowser()) {
107+
return new BedrockModel({
108+
...options,
109+
clientConfig: {
110+
...(options.clientConfig ?? {}),
111+
credentials: async () => {
112+
const { commands } = await import('vitest/browser')
113+
return await commands.getAwsCredentials()
114+
},
115+
},
116+
})
117+
} else {
118+
return new BedrockModel(options)
119+
}
120+
}
121+
122+
export function createOpenAIModel(config: OpenAIModelOptions = {}) {
123+
if (isInBrowser()) {
124+
return new OpenAIModel({
125+
...config,
126+
clientConfig: {
127+
...(config.clientConfig ?? {}),
128+
apiKey: async (): Promise<string> => {
129+
const { commands } = await import('vitest/browser')
130+
return await commands.getOpenAIAPIKey()
131+
},
132+
dangerouslyAllowBrowser: true,
133+
},
134+
})
135+
} else {
136+
return new OpenAIModel()
137+
}
138+
}
Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { readFileSync } from 'node:fs'
2-
import { join } from 'node:path'
1+
export const isInBrowser = () => {
2+
return globalThis?.process?.env == null
3+
}
34

45
/**
56
* Helper to load fixture files from Vite URL imports.
@@ -8,45 +9,16 @@ import { join } from 'node:path'
89
* @param url - The URL from a Vite ?url import
910
* @returns The file contents as a Uint8Array
1011
*/
11-
export const loadFixture = (url: string): Uint8Array => {
12-
const relativePath = url.startsWith('/') ? url.slice(1) : url
13-
const filePath = join(process.cwd(), relativePath)
14-
return new Uint8Array(readFileSync(filePath))
15-
}
16-
17-
/**
18-
* Determines if OpenAI integration tests should be skipped.
19-
* In CI environments, throws an error if API key is missing (tests should not be skipped).
20-
* In local development, skips tests if API key is not available.
21-
*
22-
* @returns true if tests should be skipped, false if they should run
23-
* @throws Error if running in CI and API key is missing
24-
*/
25-
export const shouldSkipOpenAITests = (): boolean => {
26-
try {
27-
const isCI = !!process.env.CI
28-
const hasKey = !!process.env.OPENAI_API_KEY
29-
30-
if (isCI && !hasKey) {
31-
throw new Error('OpenAI API key must be available in CI environments')
32-
}
33-
34-
if (hasKey) {
35-
if (isCI) {
36-
console.log('✅ Running in CI environment with OpenAI API key - tests will run')
37-
} else {
38-
console.log('✅ OpenAI API key found for integration tests')
39-
}
40-
return false
41-
} else {
42-
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
43-
return true
44-
}
45-
} catch (error) {
46-
if (error instanceof Error && error.message.includes('CI environments')) {
47-
throw error
48-
}
49-
console.log('⏭️ OpenAI API key not available - integration tests will be skipped')
50-
return true
12+
export const loadFixture = async (url: string): Promise<Uint8Array> => {
13+
if (isInBrowser()) {
14+
const response = await globalThis.fetch(url)
15+
const arrayBuffer = await response.arrayBuffer()
16+
return new Uint8Array(arrayBuffer)
17+
} else {
18+
const { join } = await import('node:path')
19+
const { readFile } = await import('node:fs/promises')
20+
const relativePath = url.startsWith('/') ? url.slice(1) : url
21+
const filePath = join(process.cwd(), relativePath)
22+
return new Uint8Array(await readFile(filePath))
5123
}
5224
}

0 commit comments

Comments
 (0)