mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-01 06:42:26 +00:00
480 lines
14 KiB
TypeScript
480 lines
14 KiB
TypeScript
import { test, expect, mock, describe } from "bun:test"
|
|
import path from "path"
|
|
import { unlink } from "fs/promises"
|
|
|
|
// === Mocks ===
|
|
// These mocks are required because Provider.list() triggers:
|
|
// 1. BunProc.install("@aws-sdk/credential-providers") - in bedrock custom loader
|
|
// 2. Plugin.list() which calls BunProc.install() for default plugins
|
|
// Without mocks, these would attempt real package installations that timeout in tests.
|
|
|
|
mock.module("../../src/bun/index", () => ({
|
|
BunProc: {
|
|
install: async (pkg: string, _version?: string) => {
|
|
// Return package name without version for mocking
|
|
const lastAtIndex = pkg.lastIndexOf("@")
|
|
return lastAtIndex > 0 ? pkg.substring(0, lastAtIndex) : pkg
|
|
},
|
|
run: async () => {
|
|
throw new Error("BunProc.run should not be called in tests")
|
|
},
|
|
which: () => process.execPath,
|
|
InstallFailedError: class extends Error {},
|
|
},
|
|
}))
|
|
|
|
mock.module("@aws-sdk/credential-providers", () => ({
|
|
fromNodeProviderChain: () => async () => ({
|
|
accessKeyId: "mock-access-key-id",
|
|
secretAccessKey: "mock-secret-access-key",
|
|
}),
|
|
}))
|
|
|
|
const mockPlugin = () => ({})
|
|
mock.module("opencode-copilot-auth", () => ({ default: mockPlugin }))
|
|
mock.module("opencode-anthropic-auth", () => ({ default: mockPlugin }))
|
|
mock.module("@gitlab/opencode-gitlab-auth", () => ({ default: mockPlugin }))
|
|
|
|
// Import after mocks are set up
|
|
const { tmpdir } = await import("../fixture/fixture")
|
|
const { Instance } = await import("../../src/project/instance")
|
|
const { Provider } = await import("../../src/provider/provider")
|
|
const { Env } = await import("../../src/env")
|
|
const { Global } = await import("../../src/global")
|
|
|
|
test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "eu-west-1",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_REGION", "us-east-1")
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: falls back to AWS_REGION env var when no config region", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_REGION", "eu-west-1")
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "eu-west-1",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
|
|
const authPath = path.join(Global.Path.data, "auth.json")
|
|
|
|
// Save original auth.json if it exists
|
|
let originalAuth: string | undefined
|
|
try {
|
|
originalAuth = await Bun.file(authPath).text()
|
|
} catch {
|
|
// File doesn't exist, that's fine
|
|
}
|
|
|
|
try {
|
|
// Write test auth.json
|
|
await Bun.write(
|
|
authPath,
|
|
JSON.stringify({
|
|
"amazon-bedrock": {
|
|
type: "api",
|
|
key: "test-bearer-token",
|
|
},
|
|
}),
|
|
)
|
|
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "")
|
|
Env.set("AWS_ACCESS_KEY_ID", "")
|
|
Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
|
},
|
|
})
|
|
} finally {
|
|
// Restore original or delete
|
|
if (originalAuth !== undefined) {
|
|
await Bun.write(authPath, originalAuth)
|
|
} else {
|
|
try {
|
|
await unlink(authPath)
|
|
} catch {
|
|
// Ignore errors if file doesn't exist
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
profile: "my-custom-profile",
|
|
region: "us-east-1",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
Env.set("AWS_ACCESS_KEY_ID", "test-key-id")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: includes custom endpoint in options when specified", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.endpoint).toBe(
|
|
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
|
|
)
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "us-east-1",
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
|
Env.set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
|
Env.set("AWS_PROFILE", "")
|
|
Env.set("AWS_ACCESS_KEY_ID", "")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
|
|
},
|
|
})
|
|
})
|
|
|
|
// Tests for cross-region inference profile prefix handling
|
|
// Models from models.dev may come with prefixes already (e.g., us., eu., global.)
|
|
// These should NOT be double-prefixed when passed to the SDK
|
|
|
|
test("Bedrock: model with us. prefix should not be double-prefixed", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "us-east-1",
|
|
},
|
|
models: {
|
|
"us.anthropic.claude-opus-4-5-20251101-v1:0": {
|
|
name: "Claude Opus 4.5 (US)",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
// The model should exist with the us. prefix
|
|
expect(providers["amazon-bedrock"].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: model with global. prefix should not be prefixed", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "us-east-1",
|
|
},
|
|
models: {
|
|
"global.anthropic.claude-opus-4-5-20251101-v1:0": {
|
|
name: "Claude Opus 4.5 (Global)",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: model with eu. prefix should not be double-prefixed", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "eu-west-1",
|
|
},
|
|
models: {
|
|
"eu.anthropic.claude-opus-4-5-20251101-v1:0": {
|
|
name: "Claude Opus 4.5 (EU)",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
expect(providers["amazon-bedrock"].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
test("Bedrock: model without prefix in US region should get us. prefix added", async () => {
|
|
await using tmp = await tmpdir({
|
|
init: async (dir) => {
|
|
await Bun.write(
|
|
path.join(dir, "opencode.json"),
|
|
JSON.stringify({
|
|
$schema: "https://opencode.ai/config.json",
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: {
|
|
region: "us-east-1",
|
|
},
|
|
models: {
|
|
"anthropic.claude-opus-4-5-20251101-v1:0": {
|
|
name: "Claude Opus 4.5",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
)
|
|
},
|
|
})
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
init: async () => {
|
|
Env.set("AWS_PROFILE", "default")
|
|
},
|
|
fn: async () => {
|
|
const providers = await Provider.list()
|
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
|
// Non-prefixed model should still be registered
|
|
expect(providers["amazon-bedrock"].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
// Direct unit tests for cross-region inference profile prefix handling
|
|
// These test the prefix detection logic used in getModel
|
|
|
|
describe("Bedrock cross-region prefix detection", () => {
|
|
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
|
|
test("should detect global. prefix", () => {
|
|
const modelID = "global.anthropic.claude-opus-4-5-20251101-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should detect us. prefix", () => {
|
|
const modelID = "us.anthropic.claude-opus-4-5-20251101-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should detect eu. prefix", () => {
|
|
const modelID = "eu.anthropic.claude-opus-4-5-20251101-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should detect jp. prefix", () => {
|
|
const modelID = "jp.anthropic.claude-sonnet-4-20250514-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should detect apac. prefix", () => {
|
|
const modelID = "apac.anthropic.claude-sonnet-4-20250514-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should detect au. prefix", () => {
|
|
const modelID = "au.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(true)
|
|
})
|
|
|
|
test("should NOT detect prefix for non-prefixed model", () => {
|
|
const modelID = "anthropic.claude-opus-4-5-20251101-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for amazon nova models", () => {
|
|
const modelID = "amazon.nova-pro-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for cohere models", () => {
|
|
const modelID = "cohere.command-r-plus-v1:0"
|
|
const hasPrefix = crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))
|
|
expect(hasPrefix).toBe(false)
|
|
})
|
|
})
|