config-option.test.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. buildConfigOptions,
  4. buildEffortSelectOption,
  5. buildModeSelectOption,
  6. buildModelSelectOption,
  7. formatCurrentModelId,
  8. formatVariantName,
  9. parseModelSelection,
  10. type ConfigOptionProvider,
  11. } from "@/acp/config-option"
  12. const providers: ConfigOptionProvider[] = [
  13. {
  14. id: "anthropic",
  15. name: "Anthropic",
  16. models: {
  17. "claude/sonnet-4": {
  18. id: "claude/sonnet-4",
  19. name: "Claude Sonnet 4",
  20. variants: {
  21. default: {},
  22. high: {},
  23. "very-high": {},
  24. },
  25. },
  26. "claude-haiku": {
  27. id: "claude-haiku",
  28. name: "Claude Haiku",
  29. },
  30. },
  31. },
  32. {
  33. id: "openai",
  34. name: "OpenAI",
  35. models: {
  36. "gpt-5": {
  37. id: "gpt-5",
  38. name: "GPT-5",
  39. variants: {
  40. minimal: {},
  41. low: {},
  42. },
  43. },
  44. },
  45. },
  46. ]
  47. describe("acp config options", () => {
  48. test("builds the model select option with ACP verifier category", () => {
  49. expect(
  50. buildModelSelectOption({
  51. providers,
  52. currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  53. currentVariant: "high",
  54. }),
  55. ).toEqual({
  56. id: "model",
  57. name: "Model",
  58. category: "model",
  59. type: "select",
  60. currentValue: "anthropic/claude/sonnet-4",
  61. options: [
  62. { value: "anthropic/claude-haiku", name: "Anthropic/Claude Haiku" },
  63. { value: "anthropic/claude/sonnet-4", name: "Anthropic/Claude Sonnet 4" },
  64. { value: "openai/gpt-5", name: "OpenAI/GPT-5" },
  65. ],
  66. })
  67. })
  68. test("includes variant ids in the model option only when requested", () => {
  69. const option = buildModelSelectOption({
  70. providers,
  71. currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  72. currentVariant: "high",
  73. includeVariants: true,
  74. })
  75. expect(option.currentValue).toBe("anthropic/claude/sonnet-4/high")
  76. if (option.type !== "select") throw new Error("expected select option")
  77. expect(option.options).toContainEqual({
  78. value: "anthropic/claude/sonnet-4/high",
  79. name: "Anthropic/Claude Sonnet 4 (High)",
  80. })
  81. expect(option.options).not.toContainEqual({
  82. value: "anthropic/claude/sonnet-4/default",
  83. name: "Anthropic/Claude Sonnet 4 (Default)",
  84. })
  85. })
  86. test("builds effort option from variants and falls back to default when current variant is invalid", () => {
  87. expect(buildEffortSelectOption({ variants: ["low", "default", "high"], currentVariant: "missing" })).toEqual({
  88. id: "effort",
  89. name: "Effort",
  90. description: "Available effort levels for this model",
  91. category: "thought_level",
  92. type: "select",
  93. currentValue: "default",
  94. options: [
  95. { value: "low", name: "Low" },
  96. { value: "default", name: "Default" },
  97. { value: "high", name: "High" },
  98. ],
  99. })
  100. })
  101. test("effort fallback uses the first variant when default is absent", () => {
  102. expect(buildEffortSelectOption({ variants: ["minimal", "low"], currentVariant: "missing" })?.currentValue).toBe(
  103. "minimal",
  104. )
  105. })
  106. test("omits effort option when there are no variants", () => {
  107. expect(buildEffortSelectOption({ variants: [] })).toBeUndefined()
  108. })
  109. test("builds the mode select option with descriptions when present", () => {
  110. expect(
  111. buildModeSelectOption({
  112. currentModeId: "build",
  113. modes: [
  114. { id: "build", name: "Build", description: "Make code changes" },
  115. { id: "plan", name: "Plan" },
  116. ],
  117. }),
  118. ).toEqual({
  119. id: "mode",
  120. name: "Session Mode",
  121. category: "mode",
  122. type: "select",
  123. currentValue: "build",
  124. options: [
  125. { value: "build", name: "Build", description: "Make code changes" },
  126. { value: "plan", name: "Plan" },
  127. ],
  128. })
  129. })
  130. test("builds full config options with model, effort, and mode in stable order", () => {
  131. const options = buildConfigOptions({
  132. providers,
  133. currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  134. currentVariant: "very-high",
  135. modes: [
  136. { id: "build", name: "Build" },
  137. { id: "plan", name: "Plan" },
  138. ],
  139. currentModeId: "plan",
  140. })
  141. expect(options.map((option) => option.id)).toEqual(["model", "effort", "mode"])
  142. expect(options.map((option) => option.category)).toEqual(["model", "thought_level", "mode"])
  143. expect(options[1]?.currentValue).toBe("very-high")
  144. })
  145. test("full config options omit effort for models without variants", () => {
  146. expect(
  147. buildConfigOptions({
  148. providers,
  149. currentModel: { providerID: "anthropic", modelID: "claude-haiku" },
  150. }).map((option) => option.id),
  151. ).toEqual(["model"])
  152. })
  153. test("parses provider/model selections", () => {
  154. expect(parseModelSelection("openai/gpt-5", providers)).toEqual({
  155. model: { providerID: "openai", modelID: "gpt-5" },
  156. })
  157. })
  158. test("parses provider/model/variant selections when the base model exposes that variant", () => {
  159. expect(parseModelSelection("openai/gpt-5/low", providers)).toEqual({
  160. model: { providerID: "openai", modelID: "gpt-5" },
  161. variant: "low",
  162. })
  163. })
  164. test("prefers exact slash-containing model ids before treating the tail as a variant", () => {
  165. expect(parseModelSelection("anthropic/claude/sonnet-4", providers)).toEqual({
  166. model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  167. })
  168. })
  169. test("parses trailing variants for slash-containing model ids", () => {
  170. expect(parseModelSelection("anthropic/claude/sonnet-4/high", providers)).toEqual({
  171. model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  172. variant: "high",
  173. })
  174. })
  175. test("keeps unknown trailing segments in the model id when they are not valid variants", () => {
  176. expect(parseModelSelection("anthropic/claude/sonnet-4/missing", providers)).toEqual({
  177. model: { providerID: "anthropic", modelID: "claude/sonnet-4/missing" },
  178. })
  179. })
  180. test("formats current model ids with and without selected variants", () => {
  181. expect(
  182. formatCurrentModelId({
  183. model: { providerID: "openai", modelID: "gpt-5" },
  184. variant: "low",
  185. variants: ["minimal", "low"],
  186. }),
  187. ).toBe("openai/gpt-5")
  188. expect(
  189. formatCurrentModelId({
  190. model: { providerID: "openai", modelID: "gpt-5" },
  191. variant: "low",
  192. variants: ["minimal", "low"],
  193. includeVariant: true,
  194. }),
  195. ).toBe("openai/gpt-5/low")
  196. })
  197. test("formats current model ids with variant fallback", () => {
  198. expect(
  199. formatCurrentModelId({
  200. model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  201. variant: "missing",
  202. variants: ["default", "high"],
  203. includeVariant: true,
  204. }),
  205. ).toBe("anthropic/claude/sonnet-4/default")
  206. })
  207. test("formats variant names for display", () => {
  208. expect(formatVariantName("very_high-effort")).toBe("Very High Effort")
  209. })
  210. })