| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { ACP_SESSION_MODE_OPTION_ID, acpProviderOptionDescriptors } from "./sessionConfig.ts"; |
| 4 | |
| 5 | describe("acpProviderOptionDescriptors", () => { |
| 6 | it("maps non-model select options and excludes model and collaboration categories", () => { |
| 7 | const descriptors = acpProviderOptionDescriptors({ |
| 8 | configOptions: [ |
| 9 | { |
| 10 | id: "model", |
| 11 | name: "Model", |
| 12 | category: "model", |
| 13 | type: "select", |
| 14 | currentValue: "gpt-5.6-sol", |
| 15 | options: [{ value: "gpt-5.6-sol", name: "GPT-5.6-Sol" }], |
| 16 | }, |
| 17 | { |
| 18 | id: "collaboration_mode", |
| 19 | name: "Collaboration mode", |
| 20 | category: "collaboration_mode", |
| 21 | type: "select", |
| 22 | currentValue: "default", |
| 23 | options: [ |
| 24 | { value: "default", name: "Default" }, |
| 25 | { value: "plan", name: "Plan" }, |
| 26 | ], |
| 27 | }, |
| 28 | { |
| 29 | id: "reasoning_effort", |
| 30 | name: "Reasoning effort", |
| 31 | category: "thought_level", |
| 32 | type: "select", |
| 33 | currentValue: "medium", |
| 34 | options: [ |
| 35 | { value: "low", name: "Low" }, |
| 36 | { value: "medium", name: "Medium", description: "Balanced" }, |
| 37 | ], |
| 38 | }, |
| 39 | ], |
| 40 | modeState: undefined, |
| 41 | }); |
| 42 | |
| 43 | expect(descriptors).toEqual([ |
| 44 | { |
| 45 | id: "reasoning_effort", |
| 46 | label: "Reasoning effort", |
| 47 | type: "select", |
| 48 | options: [ |
| 49 | { id: "low", label: "Low" }, |
| 50 | { id: "medium", label: "Medium", description: "Balanced" }, |
| 51 | ], |
| 52 | currentValue: "medium", |
| 53 | }, |
| 54 | ]); |
| 55 | }); |
| 56 | |
| 57 | it("maps boolean session options", () => { |
| 58 | const descriptors = acpProviderOptionDescriptors({ |
| 59 | configOptions: [ |
| 60 | { |
| 61 | id: "fast_mode", |
| 62 | name: "Fast mode", |
| 63 | description: "Use lower-latency inference.", |
| 64 | type: "boolean", |
| 65 | currentValue: true, |
| 66 | }, |
| 67 | ], |
| 68 | modeState: undefined, |
| 69 | }); |
| 70 | |
| 71 | expect(descriptors).toEqual([ |
| 72 | { |
| 73 | id: "fast_mode", |
| 74 | label: "Fast mode", |
| 75 | description: "Use lower-latency inference.", |
| 76 | type: "boolean", |
| 77 | currentValue: true, |
| 78 | }, |
| 79 | ]); |
| 80 | }); |
| 81 | |
| 82 | it("flattens grouped select choices and drops empty or duplicate values", () => { |
| 83 | const descriptors = acpProviderOptionDescriptors({ |
| 84 | configOptions: [ |
| 85 | { |
| 86 | id: "sandbox", |
| 87 | name: "Sandbox", |
| 88 | type: "select", |
| 89 | currentValue: "workspace", |
| 90 | options: [ |
| 91 | { |
| 92 | groupId: "standard", |
| 93 | name: "Standard", |
| 94 | options: [ |
| 95 | { value: "workspace", name: "Workspace" }, |
| 96 | { value: " ", name: "Blank" }, |
| 97 | ], |
| 98 | }, |
| 99 | { |
| 100 | groupId: "extra", |
| 101 | name: "Extra", |
| 102 | options: [ |
| 103 | { value: "workspace", name: "Duplicate" }, |
| 104 | { value: "full", name: "Full" }, |
| 105 | ], |
| 106 | }, |
| 107 | ], |
| 108 | }, |
| 109 | ], |
| 110 | modeState: undefined, |
| 111 | }); |
| 112 | |
| 113 | expect(descriptors).toEqual([ |
| 114 | { |
| 115 | id: "sandbox", |
| 116 | label: "Sandbox", |
| 117 | type: "select", |
| 118 | options: [ |
| 119 | { id: "workspace", label: "Workspace" }, |
| 120 | { id: "full", label: "Full" }, |
| 121 | ], |
| 122 | currentValue: "workspace", |
| 123 | }, |
| 124 | ]); |
| 125 | }); |
| 126 | |
| 127 | it("synthesizes a mode descriptor for agents without a mode config option", () => { |
| 128 | const descriptors = acpProviderOptionDescriptors({ |
| 129 | configOptions: [], |
| 130 | modeState: { |
| 131 | currentModeId: "safe", |
| 132 | availableModes: [ |
| 133 | { id: "safe", name: "Safe" }, |
| 134 | { id: "yolo", name: "YOLO", description: "No approvals" }, |
| 135 | ], |
| 136 | }, |
| 137 | }); |
| 138 | |
| 139 | expect(descriptors).toEqual([ |
| 140 | { |
| 141 | id: ACP_SESSION_MODE_OPTION_ID, |
| 142 | label: "Mode", |
| 143 | description: "Session mode advertised by the ACP agent.", |
| 144 | type: "select", |
| 145 | options: [ |
| 146 | { id: "safe", label: "Safe" }, |
| 147 | { id: "yolo", label: "YOLO", description: "No approvals" }, |
| 148 | ], |
| 149 | currentValue: "safe", |
| 150 | }, |
| 151 | ]); |
| 152 | }); |
| 153 | |
| 154 | it("suppresses the synthetic mode descriptor when modes duplicate a config option", () => { |
| 155 | const descriptors = acpProviderOptionDescriptors({ |
| 156 | configOptions: [ |
| 157 | { |
| 158 | id: "thinking", |
| 159 | name: "Thinking", |
| 160 | category: "thought_level", |
| 161 | type: "select", |
| 162 | currentValue: "medium", |
| 163 | options: [ |
| 164 | { value: "off", name: "Thinking: off" }, |
| 165 | { value: "medium", name: "Thinking: medium" }, |
| 166 | ], |
| 167 | }, |
| 168 | ], |
| 169 | modeState: { |
| 170 | currentModeId: "medium", |
| 171 | availableModes: [ |
| 172 | { id: "off", name: "Thinking: off" }, |
| 173 | { id: "medium", name: "Thinking: medium" }, |
| 174 | ], |
| 175 | }, |
| 176 | }); |
| 177 | |
| 178 | expect(descriptors.map((descriptor) => descriptor.id)).toEqual(["thinking"]); |
| 179 | }); |
| 180 | |
| 181 | it("does not let an unrelated select option hide the session modes API", () => { |
| 182 | const descriptors = acpProviderOptionDescriptors({ |
| 183 | configOptions: [ |
| 184 | { |
| 185 | id: "unrelated", |
| 186 | name: "Unrelated", |
| 187 | type: "select", |
| 188 | currentValue: "safe", |
| 189 | options: [ |
| 190 | { value: "safe", name: "Safe" }, |
| 191 | { value: "yolo", name: "YOLO" }, |
| 192 | ], |
| 193 | }, |
| 194 | ], |
| 195 | modeState: { |
| 196 | currentModeId: "safe", |
| 197 | availableModes: [ |
| 198 | { id: "safe", name: "Safe" }, |
| 199 | { id: "yolo", name: "YOLO" }, |
| 200 | ], |
| 201 | }, |
| 202 | }); |
| 203 | |
| 204 | expect(descriptors.map((descriptor) => descriptor.id)).toEqual([ |
| 205 | "unrelated", |
| 206 | ACP_SESSION_MODE_OPTION_ID, |
| 207 | ]); |
| 208 | }); |
| 209 | |
| 210 | it("preserves canonical opaque option values and omits values the wire would mutate", () => { |
| 211 | const exactValue = "value:with:opaque-markers"; |
| 212 | const descriptors = acpProviderOptionDescriptors({ |
| 213 | configOptions: [ |
| 214 | { |
| 215 | id: "opaque", |
| 216 | name: "Opaque", |
| 217 | type: "select", |
| 218 | currentValue: exactValue, |
| 219 | options: [ |
| 220 | { value: exactValue, name: "Exact" }, |
| 221 | { value: " value with spaces ", name: "Would be trimmed" }, |
| 222 | { value: "x".repeat(257), name: "Too long" }, |
| 223 | ], |
| 224 | }, |
| 225 | ], |
| 226 | modeState: undefined, |
| 227 | }); |
| 228 | |
| 229 | expect(descriptors[0]).toMatchObject({ |
| 230 | id: "opaque", |
| 231 | currentValue: exactValue, |
| 232 | options: [{ id: exactValue, label: "Exact" }], |
| 233 | }); |
| 234 | }); |
| 235 | |
| 236 | it("suppresses the synthetic mode descriptor when a mode-category option exists", () => { |
| 237 | const descriptors = acpProviderOptionDescriptors({ |
| 238 | configOptions: [ |
| 239 | { |
| 240 | id: "mode", |
| 241 | name: "Mode", |
| 242 | category: "mode", |
| 243 | type: "select", |
| 244 | currentValue: "agent", |
| 245 | options: [ |
| 246 | { value: "read-only", name: "Read-only" }, |
| 247 | { value: "agent", name: "Agent" }, |
| 248 | ], |
| 249 | }, |
| 250 | ], |
| 251 | modeState: { |
| 252 | currentModeId: "agent", |
| 253 | availableModes: [ |
| 254 | { id: "read-only", name: "Read-only" }, |
| 255 | { id: "agent", name: "Agent" }, |
| 256 | { id: "agent-full-access", name: "Agent (full access)" }, |
| 257 | ], |
| 258 | }, |
| 259 | }); |
| 260 | |
| 261 | expect(descriptors.map((descriptor) => descriptor.id)).toEqual(["mode"]); |
| 262 | }); |
| 263 | }); |