amazon-bedrock.test.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import { test, expect, mock } from "bun:test"
  2. import path from "path"
  3. import { unlink } from "fs/promises"
  4. // === Mocks ===
  5. // These mocks are required because Provider.list() triggers:
  6. // 1. BunProc.install("@aws-sdk/credential-providers") - in bedrock custom loader
  7. // 2. Plugin.list() which calls BunProc.install() for default plugins
  8. // Without mocks, these would attempt real package installations that timeout in tests.
  9. mock.module("../../src/bun/index", () => ({
  10. BunProc: {
  11. install: async (pkg: string, _version?: string) => {
  12. // Return package name without version for mocking
  13. const lastAtIndex = pkg.lastIndexOf("@")
  14. return lastAtIndex > 0 ? pkg.substring(0, lastAtIndex) : pkg
  15. },
  16. run: async () => {
  17. throw new Error("BunProc.run should not be called in tests")
  18. },
  19. which: () => process.execPath,
  20. InstallFailedError: class extends Error {},
  21. },
  22. }))
  23. mock.module("@aws-sdk/credential-providers", () => ({
  24. fromNodeProviderChain: () => async () => ({
  25. accessKeyId: "mock-access-key-id",
  26. secretAccessKey: "mock-secret-access-key",
  27. }),
  28. }))
  29. const mockPlugin = () => ({})
  30. mock.module("opencode-copilot-auth", () => ({ default: mockPlugin }))
  31. mock.module("opencode-anthropic-auth", () => ({ default: mockPlugin }))
  32. mock.module("@gitlab/opencode-gitlab-auth", () => ({ default: mockPlugin }))
  33. // Import after mocks are set up
  34. const { tmpdir } = await import("../fixture/fixture")
  35. const { Instance } = await import("../../src/project/instance")
  36. const { Provider } = await import("../../src/provider/provider")
  37. const { Env } = await import("../../src/env")
  38. const { Global } = await import("../../src/global")
  39. test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
  40. await using tmp = await tmpdir({
  41. init: async (dir) => {
  42. await Bun.write(
  43. path.join(dir, "opencode.json"),
  44. JSON.stringify({
  45. $schema: "https://opencode.ai/config.json",
  46. provider: {
  47. "amazon-bedrock": {
  48. options: {
  49. region: "eu-west-1",
  50. },
  51. },
  52. },
  53. }),
  54. )
  55. },
  56. })
  57. await Instance.provide({
  58. directory: tmp.path,
  59. init: async () => {
  60. Env.set("AWS_REGION", "us-east-1")
  61. Env.set("AWS_PROFILE", "default")
  62. },
  63. fn: async () => {
  64. const providers = await Provider.list()
  65. expect(providers["amazon-bedrock"]).toBeDefined()
  66. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  67. },
  68. })
  69. })
  70. test("Bedrock: falls back to AWS_REGION env var when no config region", async () => {
  71. await using tmp = await tmpdir({
  72. init: async (dir) => {
  73. await Bun.write(
  74. path.join(dir, "opencode.json"),
  75. JSON.stringify({
  76. $schema: "https://opencode.ai/config.json",
  77. }),
  78. )
  79. },
  80. })
  81. await Instance.provide({
  82. directory: tmp.path,
  83. init: async () => {
  84. Env.set("AWS_REGION", "eu-west-1")
  85. Env.set("AWS_PROFILE", "default")
  86. },
  87. fn: async () => {
  88. const providers = await Provider.list()
  89. expect(providers["amazon-bedrock"]).toBeDefined()
  90. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  91. },
  92. })
  93. })
  94. test("Bedrock: loads when bearer token from auth.json is present", async () => {
  95. await using tmp = await tmpdir({
  96. init: async (dir) => {
  97. await Bun.write(
  98. path.join(dir, "opencode.json"),
  99. JSON.stringify({
  100. $schema: "https://opencode.ai/config.json",
  101. provider: {
  102. "amazon-bedrock": {
  103. options: {
  104. region: "eu-west-1",
  105. },
  106. },
  107. },
  108. }),
  109. )
  110. },
  111. })
  112. const authPath = path.join(Global.Path.data, "auth.json")
  113. // Save original auth.json if it exists
  114. let originalAuth: string | undefined
  115. try {
  116. originalAuth = await Bun.file(authPath).text()
  117. } catch {
  118. // File doesn't exist, that's fine
  119. }
  120. try {
  121. // Write test auth.json
  122. await Bun.write(
  123. authPath,
  124. JSON.stringify({
  125. "amazon-bedrock": {
  126. type: "api",
  127. key: "test-bearer-token",
  128. },
  129. }),
  130. )
  131. await Instance.provide({
  132. directory: tmp.path,
  133. init: async () => {
  134. Env.set("AWS_PROFILE", "")
  135. Env.set("AWS_ACCESS_KEY_ID", "")
  136. Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
  137. },
  138. fn: async () => {
  139. const providers = await Provider.list()
  140. expect(providers["amazon-bedrock"]).toBeDefined()
  141. expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
  142. },
  143. })
  144. } finally {
  145. // Restore original or delete
  146. if (originalAuth !== undefined) {
  147. await Bun.write(authPath, originalAuth)
  148. } else {
  149. try {
  150. await unlink(authPath)
  151. } catch {
  152. // Ignore errors if file doesn't exist
  153. }
  154. }
  155. }
  156. })
  157. test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => {
  158. await using tmp = await tmpdir({
  159. init: async (dir) => {
  160. await Bun.write(
  161. path.join(dir, "opencode.json"),
  162. JSON.stringify({
  163. $schema: "https://opencode.ai/config.json",
  164. provider: {
  165. "amazon-bedrock": {
  166. options: {
  167. profile: "my-custom-profile",
  168. region: "us-east-1",
  169. },
  170. },
  171. },
  172. }),
  173. )
  174. },
  175. })
  176. await Instance.provide({
  177. directory: tmp.path,
  178. init: async () => {
  179. Env.set("AWS_PROFILE", "default")
  180. Env.set("AWS_ACCESS_KEY_ID", "test-key-id")
  181. },
  182. fn: async () => {
  183. const providers = await Provider.list()
  184. expect(providers["amazon-bedrock"]).toBeDefined()
  185. expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
  186. },
  187. })
  188. })
  189. test("Bedrock: includes custom endpoint in options when specified", async () => {
  190. await using tmp = await tmpdir({
  191. init: async (dir) => {
  192. await Bun.write(
  193. path.join(dir, "opencode.json"),
  194. JSON.stringify({
  195. $schema: "https://opencode.ai/config.json",
  196. provider: {
  197. "amazon-bedrock": {
  198. options: {
  199. endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
  200. },
  201. },
  202. },
  203. }),
  204. )
  205. },
  206. })
  207. await Instance.provide({
  208. directory: tmp.path,
  209. init: async () => {
  210. Env.set("AWS_PROFILE", "default")
  211. },
  212. fn: async () => {
  213. const providers = await Provider.list()
  214. expect(providers["amazon-bedrock"]).toBeDefined()
  215. expect(providers["amazon-bedrock"].options?.endpoint).toBe(
  216. "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
  217. )
  218. },
  219. })
  220. })
  221. test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async () => {
  222. await using tmp = await tmpdir({
  223. init: async (dir) => {
  224. await Bun.write(
  225. path.join(dir, "opencode.json"),
  226. JSON.stringify({
  227. $schema: "https://opencode.ai/config.json",
  228. provider: {
  229. "amazon-bedrock": {
  230. options: {
  231. region: "us-east-1",
  232. },
  233. },
  234. },
  235. }),
  236. )
  237. },
  238. })
  239. await Instance.provide({
  240. directory: tmp.path,
  241. init: async () => {
  242. Env.set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
  243. Env.set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
  244. Env.set("AWS_PROFILE", "")
  245. Env.set("AWS_ACCESS_KEY_ID", "")
  246. },
  247. fn: async () => {
  248. const providers = await Provider.list()
  249. expect(providers["amazon-bedrock"]).toBeDefined()
  250. expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
  251. },
  252. })
  253. })