variant.shared.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import path from "path"
  2. import { NodeFileSystem } from "@effect/platform-node"
  3. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  4. import { FSUtil } from "@kirincode-ai/core/fs-util"
  5. import { describe, expect, test } from "bun:test"
  6. import { Effect, FileSystem, Layer } from "effect"
  7. import { Global } from "@kirincode-ai/core/global"
  8. import {
  9. createVariantRuntime,
  10. cycleVariant,
  11. formatModelLabel,
  12. pickVariant,
  13. resolveVariant,
  14. } from "@/cli/cmd/run/variant.shared"
  15. import type { SessionMessages } from "@/cli/cmd/run/session.shared"
  16. import type { RunProvider } from "@/cli/cmd/run/types"
  17. import { testEffect } from "../../lib/effect"
  18. const model = {
  19. providerID: "openai",
  20. modelID: "gpt-5",
  21. }
  22. const providers: RunProvider[] = [
  23. {
  24. id: "openai",
  25. name: "OpenAI",
  26. source: "api",
  27. env: [],
  28. options: {},
  29. models: {
  30. "gpt-5": {
  31. id: "gpt-5",
  32. providerID: "openai",
  33. api: {
  34. id: "gpt-5",
  35. url: "https://openai.test",
  36. npm: "@ai-sdk/openai",
  37. },
  38. name: "GPT-5",
  39. capabilities: {
  40. temperature: true,
  41. reasoning: true,
  42. attachment: true,
  43. toolcall: true,
  44. input: {
  45. text: true,
  46. audio: false,
  47. image: false,
  48. video: false,
  49. pdf: false,
  50. },
  51. output: {
  52. text: true,
  53. audio: false,
  54. image: false,
  55. video: false,
  56. pdf: false,
  57. },
  58. interleaved: false,
  59. },
  60. cost: {
  61. input: 0,
  62. output: 0,
  63. cache: {
  64. read: 0,
  65. write: 0,
  66. },
  67. },
  68. limit: {
  69. context: 128000,
  70. output: 8192,
  71. },
  72. status: "active",
  73. options: {},
  74. headers: {},
  75. release_date: "2026-01-01",
  76. },
  77. },
  78. },
  79. ]
  80. function userMessage(
  81. id: string,
  82. input: { providerID: string; modelID: string; variant?: string },
  83. ): SessionMessages[number] {
  84. return {
  85. info: {
  86. id,
  87. sessionID: "session-1",
  88. role: "user",
  89. time: {
  90. created: 1,
  91. },
  92. agent: "build",
  93. model: input,
  94. },
  95. parts: [],
  96. }
  97. }
  98. const it = testEffect(Layer.mergeAll(LayerNode.compile(FSUtil.node), NodeFileSystem.layer))
  99. function remap(root: string, file: string) {
  100. if (file === Global.Path.state) {
  101. return root
  102. }
  103. if (file.startsWith(Global.Path.state + path.sep)) {
  104. return path.join(root, path.relative(Global.Path.state, file))
  105. }
  106. return file
  107. }
  108. function remappedFs(root: string) {
  109. return Layer.effect(
  110. FSUtil.Service,
  111. Effect.gen(function* () {
  112. const fs = yield* FSUtil.Service
  113. return FSUtil.Service.of({
  114. ...fs,
  115. readJson: (file) => fs.readJson(remap(root, file)),
  116. writeJson: (file, data, mode) => fs.writeJson(remap(root, file), data, mode),
  117. })
  118. }),
  119. ).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
  120. }
  121. describe("run variant shared", () => {
  122. test("prefers cli then session then saved variants", () => {
  123. expect(resolveVariant("max", "high", "low", ["low", "high"])).toBe("max")
  124. expect(resolveVariant(undefined, "high", "low", ["low", "high"])).toBe("high")
  125. expect(resolveVariant(undefined, "missing", "low", ["low", "high"])).toBe("low")
  126. })
  127. test("cycles through variants and back to default", () => {
  128. expect(cycleVariant(undefined, ["low", "high"])).toBe("low")
  129. expect(cycleVariant("low", ["low", "high"])).toBe("high")
  130. expect(cycleVariant("high", ["low", "high"])).toBeUndefined()
  131. expect(cycleVariant(undefined, [])).toBeUndefined()
  132. })
  133. test("formats model labels", () => {
  134. expect(formatModelLabel(model, undefined)).toBe("gpt-5 · openai")
  135. expect(formatModelLabel(model, "high")).toBe("gpt-5 · openai · high")
  136. expect(formatModelLabel(model, undefined, providers)).toBe("GPT-5 · OpenAI")
  137. expect(formatModelLabel(model, "high", providers)).toBe("GPT-5 · OpenAI · high")
  138. })
  139. test("picks the latest matching variant from raw session messages", () => {
  140. const msgs: SessionMessages = [
  141. userMessage("msg-1", { providerID: "openai", modelID: "gpt-5", variant: "high" }),
  142. userMessage("msg-2", { providerID: "anthropic", modelID: "sonnet", variant: "max" }),
  143. userMessage("msg-3", { providerID: "openai", modelID: "gpt-5", variant: "minimal" }),
  144. ]
  145. expect(pickVariant(model, msgs)).toBe("minimal")
  146. })
  147. it.live("reads and writes saved variants through a runtime-backed app fs layer", () =>
  148. Effect.gen(function* () {
  149. const filesys = yield* FileSystem.FileSystem
  150. const fs = yield* FSUtil.Service
  151. const root = yield* filesys.makeTempDirectoryScoped()
  152. const file = path.join(root, "model.json")
  153. yield* fs.writeJson(file, {
  154. recent: [{ providerID: "anthropic", modelID: "sonnet" }],
  155. variant: {
  156. "openai/gpt-4.1": "low",
  157. },
  158. })
  159. const svc = createVariantRuntime(remappedFs(root))
  160. yield* Effect.promise(() => svc.saveVariant(model, "high"))
  161. expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
  162. expect(yield* fs.readJson(file)).toEqual({
  163. recent: [{ providerID: "anthropic", modelID: "sonnet" }],
  164. variant: {
  165. "openai/gpt-4.1": "low",
  166. "openai/gpt-5": "high",
  167. },
  168. })
  169. yield* Effect.promise(() => svc.saveVariant(model, undefined))
  170. expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBeUndefined()
  171. expect(yield* fs.readJson(file)).toEqual({
  172. recent: [{ providerID: "anthropic", modelID: "sonnet" }],
  173. variant: {
  174. "openai/gpt-4.1": "low",
  175. },
  176. })
  177. }),
  178. )
  179. it.live("repairs malformed saved variant state on the next write", () =>
  180. Effect.gen(function* () {
  181. const filesys = yield* FileSystem.FileSystem
  182. const fs = yield* FSUtil.Service
  183. const root = yield* filesys.makeTempDirectoryScoped()
  184. const file = path.join(root, "model.json")
  185. yield* filesys.writeFileString(file, "{")
  186. const svc = createVariantRuntime(remappedFs(root))
  187. yield* Effect.promise(() => svc.saveVariant(model, "high"))
  188. expect(yield* Effect.promise(() => svc.resolveSavedVariant(model))).toBe("high")
  189. expect(yield* fs.readJson(file)).toEqual({
  190. variant: {
  191. "openai/gpt-5": "high",
  192. },
  193. })
  194. }),
  195. )
  196. })