directory.test.ts 5.6 KB

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