config-option.test.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. buildConfigOptions,
  4. buildEffortSelectOption,
  5. buildModeSelectOption,
  6. buildModelSelectOption,
  7. formatVariantName,
  8. parseModelSelection,
  9. type ConfigOptionProvider,
  10. } from "../../src/acp/config-option"
  11. const providers: ConfigOptionProvider[] = [
  12. {
  13. id: "anthropic",
  14. name: "Anthropic",
  15. models: [
  16. { id: "claude/sonnet-4", name: "Claude Sonnet 4", variants: ["default", "high", "very-high"] },
  17. { id: "claude-haiku", name: "Claude Haiku" },
  18. ],
  19. },
  20. { id: "openai", name: "OpenAI", models: [{ id: "gpt-5", name: "GPT-5", variants: ["minimal", "low"] }] },
  21. ]
  22. describe("acp config options", () => {
  23. test("builds the model select option with ACP verifier category", () => {
  24. expect(
  25. buildModelSelectOption({
  26. providers,
  27. currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  28. }),
  29. ).toEqual({
  30. id: "model",
  31. name: "Model",
  32. category: "model",
  33. type: "select",
  34. currentValue: "anthropic/claude/sonnet-4",
  35. options: [
  36. { value: "anthropic/claude-haiku", name: "Anthropic/Claude Haiku" },
  37. { value: "anthropic/claude/sonnet-4", name: "Anthropic/Claude Sonnet 4" },
  38. { value: "openai/gpt-5", name: "OpenAI/GPT-5" },
  39. ],
  40. })
  41. })
  42. test("builds effort option from variants and falls back to default when current variant is invalid", () => {
  43. expect(buildEffortSelectOption({ variants: ["low", "default", "high"], currentVariant: "missing" })).toEqual({
  44. id: "effort",
  45. name: "Effort",
  46. description: "Available effort levels for this model",
  47. category: "thought_level",
  48. type: "select",
  49. currentValue: "default",
  50. options: [
  51. { value: "low", name: "Low" },
  52. { value: "default", name: "Default" },
  53. { value: "high", name: "High" },
  54. ],
  55. })
  56. })
  57. test("effort fallback uses the first variant when default is absent", () => {
  58. expect(buildEffortSelectOption({ variants: ["minimal", "low"], currentVariant: "missing" }).currentValue).toBe(
  59. "minimal",
  60. )
  61. })
  62. test("builds the mode select option with descriptions when present", () => {
  63. expect(
  64. buildModeSelectOption({
  65. currentModeId: "build",
  66. modes: [
  67. { id: "build", name: "Build", description: "Make code changes" },
  68. { id: "plan", name: "Plan" },
  69. ],
  70. }),
  71. ).toEqual({
  72. id: "mode",
  73. name: "Session Mode",
  74. category: "mode",
  75. type: "select",
  76. currentValue: "build",
  77. options: [
  78. { value: "build", name: "Build", description: "Make code changes" },
  79. { value: "plan", name: "Plan" },
  80. ],
  81. })
  82. })
  83. test("builds full config options with model, effort, and mode in stable order", () => {
  84. const options = buildConfigOptions({
  85. providers,
  86. currentModel: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  87. currentVariant: "very-high",
  88. modes: [
  89. { id: "build", name: "Build" },
  90. { id: "plan", name: "Plan" },
  91. ],
  92. currentModeId: "plan",
  93. })
  94. expect(options.map((option) => option.id)).toEqual(["model", "effort", "mode"])
  95. expect(options.map((option) => option.category)).toEqual(["model", "thought_level", "mode"])
  96. expect(options[0]?.currentValue).toBe("anthropic/claude/sonnet-4")
  97. expect(options[1]?.currentValue).toBe("very-high")
  98. })
  99. test("full config options omit effort for models without variants", () => {
  100. expect(
  101. buildConfigOptions({
  102. providers,
  103. currentModel: { providerID: "anthropic", modelID: "claude-haiku" },
  104. }).map((option) => option.id),
  105. ).toEqual(["model"])
  106. })
  107. test("parses provider/model selections", () => {
  108. expect(parseModelSelection("openai/gpt-5", providers)).toEqual({
  109. model: { providerID: "openai", modelID: "gpt-5" },
  110. })
  111. })
  112. test("parses provider/model/variant selections when the base model exposes that variant", () => {
  113. expect(parseModelSelection("openai/gpt-5/low", providers)).toEqual({
  114. model: { providerID: "openai", modelID: "gpt-5" },
  115. variant: "low",
  116. })
  117. })
  118. test("prefers exact slash-containing model ids before treating the tail as a variant", () => {
  119. expect(parseModelSelection("anthropic/claude/sonnet-4", providers)).toEqual({
  120. model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  121. })
  122. })
  123. test("parses trailing variants for slash-containing model ids", () => {
  124. expect(parseModelSelection("anthropic/claude/sonnet-4/high", providers)).toEqual({
  125. model: { providerID: "anthropic", modelID: "claude/sonnet-4" },
  126. variant: "high",
  127. })
  128. })
  129. test("keeps unknown trailing segments in the model id when they are not valid variants", () => {
  130. expect(parseModelSelection("anthropic/claude/sonnet-4/missing", providers)).toEqual({
  131. model: { providerID: "anthropic", modelID: "claude/sonnet-4/missing" },
  132. })
  133. })
  134. test("formats variant names for display", () => {
  135. expect(formatVariantName("very_high-effort")).toBe("Very High Effort")
  136. })
  137. })