provider-gitlab.test.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { describe, expect, mock } from "bun:test"
  2. import { Effect } from "effect"
  3. import { Catalog } from "@opencode-ai/core/catalog"
  4. import { PluginV2 } from "@opencode-ai/core/plugin"
  5. import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
  6. import { ProviderV2 } from "@opencode-ai/core/provider"
  7. import { addPlugin, it, model, required, withEnv } from "./provider-helper"
  8. const gitlabSDKOptions: Record<string, unknown>[] = []
  9. void mock.module("gitlab-ai-provider", () => ({
  10. VERSION: "test-version",
  11. createGitLab: (options: Record<string, unknown>) => {
  12. gitlabSDKOptions.push(options)
  13. return {
  14. agenticChat: (id: string, options: unknown) => ({ id, options, type: "agentic" }),
  15. workflowChat: (id: string, options: unknown) => ({ id, options, type: "workflow" }),
  16. }
  17. },
  18. discoverWorkflowModels: async () => ({ models: [], project: undefined }),
  19. isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact",
  20. }))
  21. describe("GitLabPlugin", () => {
  22. it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
  23. withEnv(
  24. {
  25. GITLAB_INSTANCE_URL: undefined,
  26. GITLAB_TOKEN: "env-token",
  27. },
  28. () =>
  29. Effect.gen(function* () {
  30. gitlabSDKOptions.length = 0
  31. const plugin = yield* PluginV2.Service
  32. yield* addPlugin(plugin, GitLabPlugin)
  33. yield* plugin.trigger(
  34. "aisdk.sdk",
  35. { model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
  36. {},
  37. )
  38. expect(gitlabSDKOptions).toHaveLength(1)
  39. expect(gitlabSDKOptions[0].instanceUrl).toBe("https://gitlab.com")
  40. expect(gitlabSDKOptions[0].apiKey).toBe("env-token")
  41. expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({
  42. "anthropic-beta": "context-1m-2025-08-07",
  43. })
  44. expect(String((gitlabSDKOptions[0].aiGatewayHeaders as Record<string, string>)["User-Agent"])).toContain(
  45. "gitlab-ai-provider/test-version",
  46. )
  47. expect(gitlabSDKOptions[0].featureFlags).toEqual({
  48. duo_agent_platform_agentic_chat: true,
  49. duo_agent_platform: true,
  50. })
  51. }),
  52. ),
  53. )
  54. it.effect("uses GITLAB_INSTANCE_URL when instanceUrl is not configured", () =>
  55. withEnv(
  56. {
  57. GITLAB_INSTANCE_URL: "https://env.gitlab.example",
  58. GITLAB_TOKEN: undefined,
  59. },
  60. () =>
  61. Effect.gen(function* () {
  62. gitlabSDKOptions.length = 0
  63. const plugin = yield* PluginV2.Service
  64. yield* addPlugin(plugin, GitLabPlugin)
  65. yield* plugin.trigger(
  66. "aisdk.sdk",
  67. { model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
  68. {},
  69. )
  70. expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example")
  71. }),
  72. ),
  73. )
  74. it.effect("keeps configured instance URL, apiKey, aiGatewayHeaders, and featureFlags over env/defaults", () =>
  75. withEnv(
  76. {
  77. GITLAB_INSTANCE_URL: "https://env.gitlab.example",
  78. GITLAB_TOKEN: "env-token",
  79. },
  80. () =>
  81. Effect.gen(function* () {
  82. gitlabSDKOptions.length = 0
  83. const plugin = yield* PluginV2.Service
  84. yield* addPlugin(plugin, GitLabPlugin)
  85. yield* plugin.trigger(
  86. "aisdk.sdk",
  87. {
  88. model: model("gitlab", "claude"),
  89. package: "gitlab-ai-provider",
  90. options: {
  91. name: "gitlab",
  92. instanceUrl: "https://configured.gitlab.example",
  93. apiKey: "configured-token",
  94. aiGatewayHeaders: {
  95. "anthropic-beta": "configured-beta",
  96. "x-gitlab-test": "1",
  97. },
  98. featureFlags: {
  99. duo_agent_platform: false,
  100. custom_flag: true,
  101. },
  102. },
  103. },
  104. {},
  105. )
  106. expect(gitlabSDKOptions[0].instanceUrl).toBe("https://configured.gitlab.example")
  107. expect(gitlabSDKOptions[0].apiKey).toBe("configured-token")
  108. expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({
  109. "anthropic-beta": "configured-beta",
  110. "x-gitlab-test": "1",
  111. })
  112. expect(gitlabSDKOptions[0].featureFlags).toEqual({
  113. duo_agent_platform_agentic_chat: true,
  114. duo_agent_platform: false,
  115. custom_flag: true,
  116. })
  117. }),
  118. ),
  119. )
  120. it.effect("ignores non-GitLab SDK packages", () =>
  121. Effect.gen(function* () {
  122. gitlabSDKOptions.length = 0
  123. const plugin = yield* PluginV2.Service
  124. yield* addPlugin(plugin, GitLabPlugin)
  125. const result = yield* plugin.trigger(
  126. "aisdk.sdk",
  127. { model: model("gitlab", "claude"), package: "@ai-sdk/openai", options: { name: "gitlab" } },
  128. {},
  129. )
  130. expect(result.sdk).toBeUndefined()
  131. expect(gitlabSDKOptions).toHaveLength(0)
  132. }),
  133. )
  134. it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
  135. Effect.gen(function* () {
  136. const plugin = yield* PluginV2.Service
  137. const calls: [string, unknown][] = []
  138. yield* addPlugin(plugin, GitLabPlugin)
  139. const result = yield* plugin.trigger(
  140. "aisdk.language",
  141. {
  142. model: model("gitlab", "duo-workflow-custom", {
  143. request: {
  144. headers: {},
  145. body: { workflowRef: "ref", workflowDefinition: "definition" },
  146. },
  147. }),
  148. sdk: {
  149. workflowChat: (id: string, options: unknown) => {
  150. calls.push([id, options])
  151. return { id, options }
  152. },
  153. agenticChat: () => undefined,
  154. },
  155. options: { featureFlags: { configured: true } },
  156. },
  157. {},
  158. )
  159. expect(calls).toEqual([
  160. ["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: "definition" }],
  161. ])
  162. expect(result.language as unknown).toEqual({
  163. id: "duo-workflow",
  164. options: calls[0]?.[1],
  165. selectedModelRef: "ref",
  166. })
  167. }),
  168. )
  169. it.effect("uses exact static workflow model ids when the provider recognizes them", () =>
  170. Effect.gen(function* () {
  171. const plugin = yield* PluginV2.Service
  172. const calls: [string, unknown][] = []
  173. yield* addPlugin(plugin, GitLabPlugin)
  174. const result = yield* plugin.trigger(
  175. "aisdk.language",
  176. {
  177. model: model("gitlab", "duo-workflow-exact"),
  178. sdk: {
  179. workflowChat: (id: string, options: unknown) => {
  180. calls.push([id, options])
  181. return { id, options }
  182. },
  183. agenticChat: () => undefined,
  184. },
  185. options: { featureFlags: { configured: true } },
  186. },
  187. {},
  188. )
  189. expect(calls).toEqual([
  190. ["duo-workflow-exact", { featureFlags: { configured: true }, workflowDefinition: undefined }],
  191. ])
  192. expect(result.language as unknown).toEqual({ id: "duo-workflow-exact", options: calls[0]?.[1] })
  193. }),
  194. )
  195. it.effect("uses provider feature flags instead of request feature flags", () =>
  196. Effect.gen(function* () {
  197. const plugin = yield* PluginV2.Service
  198. const calls: [string, unknown][] = []
  199. yield* addPlugin(plugin, GitLabPlugin)
  200. yield* plugin.trigger(
  201. "aisdk.language",
  202. {
  203. model: model("gitlab", "duo-workflow-custom", {
  204. request: {
  205. headers: {},
  206. body: { featureFlags: { request_flag: true } },
  207. },
  208. }),
  209. sdk: {
  210. workflowChat: (id: string, options: unknown) => {
  211. calls.push([id, options])
  212. return { id, options }
  213. },
  214. agenticChat: () => undefined,
  215. },
  216. options: { featureFlags: { configured: true } },
  217. },
  218. {},
  219. )
  220. expect(calls).toEqual([["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: undefined }]])
  221. }),
  222. )
  223. it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () =>
  224. Effect.gen(function* () {
  225. const plugin = yield* PluginV2.Service
  226. const calls: [string, unknown][] = []
  227. yield* addPlugin(plugin, GitLabPlugin)
  228. yield* plugin.trigger(
  229. "aisdk.language",
  230. {
  231. model: model("gitlab", "claude", {
  232. request: { headers: { h: "v" }, body: {} },
  233. }),
  234. sdk: {
  235. workflowChat: () => undefined,
  236. agenticChat: (id: string, options: unknown) => {
  237. const selected = options as {
  238. aiGatewayHeaders?: Record<string, string>
  239. featureFlags?: Record<string, boolean>
  240. }
  241. calls.push([
  242. id,
  243. { aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } },
  244. ])
  245. },
  246. },
  247. options: { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } },
  248. },
  249. {},
  250. )
  251. expect(calls).toEqual([
  252. ["claude", { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } }],
  253. ])
  254. }),
  255. )
  256. })