directory.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { describe, expect } from "bun:test"
  2. import { Directory } from "@/acp/directory"
  3. import { Command } from "@/command"
  4. import { ProviderV2 } from "@opencode-ai/core/provider"
  5. import { Provider } from "@/provider/provider"
  6. import { Effect, Layer } from "effect"
  7. import { it } from "../lib/effect"
  8. const command = (name: string): Command.Info => ({
  9. name,
  10. source: "command",
  11. template: `run ${name}`,
  12. hints: [],
  13. })
  14. const model = (providerID: ProviderV2.ID, id: string, variants?: Directory.ModelVariants): Provider.Model => ({
  15. id: ProviderV2.ModelID.make(id),
  16. providerID,
  17. api: {
  18. id,
  19. url: "https://example.com",
  20. npm: "@ai-sdk/openai-compatible",
  21. },
  22. name: id,
  23. family: "test",
  24. capabilities: {
  25. temperature: true,
  26. reasoning: Boolean(variants),
  27. attachment: false,
  28. toolcall: true,
  29. input: { text: true, audio: false, image: false, video: false, pdf: false },
  30. output: { text: true, audio: false, image: false, video: false, pdf: false },
  31. interleaved: false,
  32. },
  33. cost: {
  34. input: 0,
  35. output: 0,
  36. cache: { read: 0, write: 0 },
  37. },
  38. limit: {
  39. context: 128000,
  40. output: 4096,
  41. },
  42. status: "active",
  43. options: {},
  44. headers: {},
  45. release_date: "2026-01-01",
  46. ...(variants ? { variants } : {}),
  47. })
  48. const snapshot = (directory: string) => {
  49. const providerID = ProviderV2.ID.make(`provider-${directory}`)
  50. const modelID = ProviderV2.ModelID.make(`model-${directory}`)
  51. const providers = {
  52. [providerID]: {
  53. id: providerID,
  54. name: `Provider ${directory}`,
  55. source: "config",
  56. env: [],
  57. options: {},
  58. models: {
  59. [modelID]: model(providerID, modelID, {
  60. low: { reasoningEffort: "low" },
  61. high: { reasoningEffort: "high" },
  62. }),
  63. [ProviderV2.ModelID.make(`plain-${directory}`)]: model(providerID, `plain-${directory}`),
  64. },
  65. },
  66. } satisfies Record<ProviderV2.ID, Provider.Info>
  67. return Directory.build({
  68. directory,
  69. providers,
  70. modes: [
  71. { id: "build", name: `build-${directory}` },
  72. { id: "plan", name: `plan-${directory}`, description: "plan first" },
  73. ],
  74. defaultModeID: "build",
  75. commands: [command(`init-${directory}`), command(`review-${directory}`)],
  76. defaultModel: { providerID, modelID },
  77. })
  78. }
  79. const fakeLayer = (calls: string[]) =>
  80. Directory.layer.pipe(
  81. Layer.provide(
  82. Layer.succeed(
  83. Directory.Loader,
  84. Directory.Loader.of({
  85. load: (directory) =>
  86. Effect.sync(() => {
  87. calls.push(directory)
  88. return snapshot(directory)
  89. }),
  90. }),
  91. ),
  92. ),
  93. )
  94. describe("ACP directory snapshot", () => {
  95. it.effect("two concurrent callers share one load", () => {
  96. const calls: string[] = []
  97. return Effect.gen(function* () {
  98. const directory = yield* Directory.Service
  99. const [first, second] = yield* Effect.all([directory.get("alpha"), directory.get("alpha")], {
  100. concurrency: "unbounded",
  101. })
  102. expect(calls).toEqual(["alpha"])
  103. expect(first).toBe(second)
  104. }).pipe(Effect.provide(fakeLayer(calls)))
  105. })
  106. it.effect("warm calls use cached data", () => {
  107. const calls: string[] = []
  108. return Effect.gen(function* () {
  109. const directory = yield* Directory.Service
  110. const first = yield* directory.get("alpha")
  111. const second = yield* directory.get("alpha")
  112. expect(calls).toEqual(["alpha"])
  113. expect(first).toBe(second)
  114. }).pipe(Effect.provide(fakeLayer(calls)))
  115. })
  116. it.effect("different directories get different snapshots", () => {
  117. const calls: string[] = []
  118. return Effect.gen(function* () {
  119. const directory = yield* Directory.Service
  120. const [alpha, beta] = yield* Effect.all([directory.get("alpha"), directory.get("beta")], {
  121. concurrency: "unbounded",
  122. })
  123. expect(calls.toSorted()).toEqual(["alpha", "beta"])
  124. expect(alpha.directory).toBe("alpha")
  125. expect(beta.directory).toBe("beta")
  126. expect(alpha.defaultModel?.providerID).not.toBe(beta.defaultModel?.providerID)
  127. }).pipe(Effect.provide(fakeLayer(calls)))
  128. })
  129. it.effect("model variant lookup works", () =>
  130. Effect.gen(function* () {
  131. const directory = yield* Directory.Service
  132. const alpha = yield* directory.get("alpha")
  133. const model = alpha.defaultModel!
  134. expect(directory.variants(alpha, model)).toEqual({
  135. low: { reasoningEffort: "low" },
  136. high: { reasoningEffort: "high" },
  137. })
  138. expect(directory.variants(alpha, { ...model, modelID: ProviderV2.ModelID.make("missing") })).toBeUndefined()
  139. }).pipe(Effect.provide(fakeLayer([]))),
  140. )
  141. it.effect("commands and modes are included", () =>
  142. Effect.gen(function* () {
  143. const directory = yield* Directory.Service
  144. const alpha = yield* directory.get("alpha")
  145. expect(alpha.availableCommands.map((item) => item.name)).toEqual(["init-alpha", "review-alpha"])
  146. expect(alpha.availableModes).toEqual([
  147. { id: "build", name: "build-alpha" },
  148. { id: "plan", name: "plan-alpha", description: "plan first" },
  149. ])
  150. expect(alpha.defaultModeID).toBe("build")
  151. }).pipe(Effect.provide(fakeLayer([]))),
  152. )
  153. it.effect("falls back when the default mode is not available", () =>
  154. Effect.sync(() => {
  155. expect(
  156. Directory.build({
  157. directory: "alpha",
  158. providers: {},
  159. modes: [
  160. { id: "build", name: "Build" },
  161. { id: "plan", name: "Plan" },
  162. ],
  163. defaultModeID: "hidden",
  164. commands: [],
  165. }).defaultModeID,
  166. ).toBe("build")
  167. }),
  168. )
  169. })