| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as EffectAcpErrors from "effect-acp/errors"; |
| 5 | |
| 6 | import { |
| 7 | applyGrokAcpModelSelection, |
| 8 | buildGrokAcpSpawnInput, |
| 9 | grokAcpRuntimeProcessOwnership, |
| 10 | grokAcpSpawnArgs, |
| 11 | isValidGrokReasoningEffortToken, |
| 12 | resolveGrokAcpBaseModelId, |
| 13 | } from "./acpSupport.ts"; |
| 14 | |
| 15 | describe("grokAcpRuntimeProcessOwnership", () => { |
| 16 | it("opts Grok into detached process-tree ownership on the injected host platform", () => { |
| 17 | expect(grokAcpRuntimeProcessOwnership("linux")).toEqual({ |
| 18 | ownDescendantProcessGroups: true, |
| 19 | ownDetachedProcessGroup: true, |
| 20 | processGroupPlatform: "linux", |
| 21 | }); |
| 22 | }); |
| 23 | |
| 24 | it("uses the prior provider-group path on Darwin and Windows", () => { |
| 25 | expect(grokAcpRuntimeProcessOwnership("darwin")).toEqual({ |
| 26 | ownDescendantProcessGroups: false, |
| 27 | ownDetachedProcessGroup: true, |
| 28 | processGroupPlatform: "darwin", |
| 29 | }); |
| 30 | expect(grokAcpRuntimeProcessOwnership("win32")).toEqual({ |
| 31 | ownDescendantProcessGroups: false, |
| 32 | ownDetachedProcessGroup: true, |
| 33 | processGroupPlatform: "win32", |
| 34 | }); |
| 35 | }); |
| 36 | }); |
| 37 | |
| 38 | describe("resolveGrokAcpBaseModelId", () => { |
| 39 | it("normalizes empty and custom Grok model ids", () => { |
| 40 | expect(resolveGrokAcpBaseModelId(undefined)).toBe("grok-build"); |
| 41 | expect(resolveGrokAcpBaseModelId(" ")).toBe("grok-build"); |
| 42 | expect(resolveGrokAcpBaseModelId(" grok-test-custom-model ")).toBe("grok-test-custom-model"); |
| 43 | }); |
| 44 | }); |
| 45 | |
| 46 | describe("grokAcpSpawnArgs", () => { |
| 47 | it("inherits the Grok CLI config when no T3 runtime mode is set", () => { |
| 48 | expect(grokAcpSpawnArgs()).toEqual(["agent", "stdio"]); |
| 49 | }); |
| 50 | |
| 51 | it("forces Grok to ask when T3 is Supervised", () => { |
| 52 | expect(grokAcpSpawnArgs("approval-required")).toEqual([ |
| 53 | "--permission-mode", |
| 54 | "default", |
| 55 | "agent", |
| 56 | "stdio", |
| 57 | ]); |
| 58 | }); |
| 59 | |
| 60 | it("maps Full access to Grok always-approve", () => { |
| 61 | expect(grokAcpSpawnArgs("full-access")).toEqual(["agent", "--always-approve", "stdio"]); |
| 62 | }); |
| 63 | |
| 64 | it("launches Auto on Grok's classifier and a mode Grok does not offer asking", () => { |
| 65 | expect(grokAcpSpawnArgs("auto")).toEqual(["--permission-mode", "auto", "agent", "stdio"]); |
| 66 | expect(grokAcpSpawnArgs("auto-accept-edits")).toEqual([ |
| 67 | "--permission-mode", |
| 68 | "default", |
| 69 | "agent", |
| 70 | "stdio", |
| 71 | ]); |
| 72 | }); |
| 73 | }); |
| 74 | |
| 75 | describe("buildGrokAcpSpawnInput", () => { |
| 76 | it("passes the T3 Code referrer through Grok OAuth env", () => { |
| 77 | const spawn = buildGrokAcpSpawnInput({ binaryPath: "/usr/local/bin/grok" }, "/tmp/project", { |
| 78 | XAI_API_KEY: "secret", |
| 79 | GROK_OAUTH2_REFERRER: "other-client", |
| 80 | }); |
| 81 | |
| 82 | expect(spawn).toEqual({ |
| 83 | command: "/usr/local/bin/grok", |
| 84 | args: ["agent", "stdio"], |
| 85 | cwd: "/tmp/project", |
| 86 | env: { |
| 87 | XAI_API_KEY: "secret", |
| 88 | GROK_OAUTH2_REFERRER: "t3code", |
| 89 | }, |
| 90 | }); |
| 91 | }); |
| 92 | |
| 93 | it("puts Supervised on the Grok argv so config always-approve cannot win", () => { |
| 94 | const spawn = buildGrokAcpSpawnInput( |
| 95 | { binaryPath: "/usr/local/bin/grok" }, |
| 96 | "/tmp/project", |
| 97 | undefined, |
| 98 | "approval-required", |
| 99 | ); |
| 100 | expect(spawn.args).toEqual(["--permission-mode", "default", "agent", "stdio"]); |
| 101 | }); |
| 102 | }); |
| 103 | |
| 104 | describe("isValidGrokReasoningEffortToken", () => { |
| 105 | it("accepts future ACP tokens and rejects malformed metadata values", () => { |
| 106 | expect(isValidGrokReasoningEffortToken("xhigh")).toBe(true); |
| 107 | expect(isValidGrokReasoningEffortToken("turbo_v2")).toBe(true); |
| 108 | expect(isValidGrokReasoningEffortToken("not a token")).toBe(false); |
| 109 | expect(isValidGrokReasoningEffortToken("-leading-dash")).toBe(false); |
| 110 | expect(isValidGrokReasoningEffortToken("x".repeat(33))).toBe(false); |
| 111 | }); |
| 112 | }); |
| 113 | |
| 114 | describe("applyGrokAcpModelSelection", () => { |
| 115 | const makeRecordingRuntime = (failure?: EffectAcpErrors.AcpError) => { |
| 116 | const modelCalls: Array<{ |
| 117 | modelId: string; |
| 118 | meta?: { readonly [key: string]: unknown } | null; |
| 119 | }> = []; |
| 120 | const runtime = { |
| 121 | setSessionModel: (modelId: string, meta?: { readonly [key: string]: unknown } | null) => |
| 122 | Effect.gen(function* () { |
| 123 | modelCalls.push(meta === undefined ? { modelId } : { modelId, meta }); |
| 124 | if (failure) return yield* failure; |
| 125 | return {}; |
| 126 | }), |
| 127 | }; |
| 128 | return { runtime, modelCalls }; |
| 129 | }; |
| 130 | |
| 131 | it.effect("calls session/set_model when the requested model differs from current", () => |
| 132 | Effect.gen(function* () { |
| 133 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 134 | const result = yield* applyGrokAcpModelSelection({ |
| 135 | runtime, |
| 136 | currentModelId: "grok-build", |
| 137 | requestedModelId: "grok-mock-alt", |
| 138 | mapError: (cause) => cause.message, |
| 139 | }); |
| 140 | expect(modelCalls).toEqual([{ modelId: "grok-mock-alt" }]); |
| 141 | expect(result).toBe("grok-mock-alt"); |
| 142 | }), |
| 143 | ); |
| 144 | |
| 145 | it.effect("applies reasoning effort through session/set_model metadata", () => |
| 146 | Effect.gen(function* () { |
| 147 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 148 | const result = yield* applyGrokAcpModelSelection({ |
| 149 | runtime, |
| 150 | currentModelId: "grok-4.6", |
| 151 | currentReasoningEffort: "high", |
| 152 | requestedModelId: "grok-4.6", |
| 153 | requestedReasoningEffort: "xhigh", |
| 154 | mapError: (cause) => cause.message, |
| 155 | }); |
| 156 | expect(modelCalls).toEqual([{ modelId: "grok-4.6", meta: { reasoningEffort: "xhigh" } }]); |
| 157 | expect(result).toBe("grok-4.6"); |
| 158 | }), |
| 159 | ); |
| 160 | |
| 161 | it.effect("does not clear reasoning when same-model selection omits effort", () => |
| 162 | Effect.gen(function* () { |
| 163 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 164 | const result = yield* applyGrokAcpModelSelection({ |
| 165 | runtime, |
| 166 | currentModelId: "grok-4.6", |
| 167 | currentReasoningEffort: "high", |
| 168 | requestedModelId: "grok-4.6", |
| 169 | requestedReasoningEffort: undefined, |
| 170 | mapError: (cause) => cause.message, |
| 171 | }); |
| 172 | expect(modelCalls).toEqual([]); |
| 173 | expect(result).toBe("grok-4.6"); |
| 174 | }), |
| 175 | ); |
| 176 | |
| 177 | it.effect("drops malformed effort metadata instead of sending it", () => |
| 178 | Effect.gen(function* () { |
| 179 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 180 | yield* applyGrokAcpModelSelection({ |
| 181 | runtime, |
| 182 | currentModelId: "grok-4.6", |
| 183 | currentReasoningEffort: "high", |
| 184 | requestedModelId: "grok-4.6", |
| 185 | requestedReasoningEffort: "not a token", |
| 186 | mapError: (cause) => cause.message, |
| 187 | }); |
| 188 | expect(modelCalls).toEqual([{ modelId: "grok-4.6" }]); |
| 189 | }), |
| 190 | ); |
| 191 | |
| 192 | it.effect("skips set_model when requested matches current", () => |
| 193 | Effect.gen(function* () { |
| 194 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 195 | const result = yield* applyGrokAcpModelSelection({ |
| 196 | runtime, |
| 197 | currentModelId: "grok-build", |
| 198 | requestedModelId: "grok-build", |
| 199 | mapError: (cause) => cause.message, |
| 200 | }); |
| 201 | expect(modelCalls).toEqual([]); |
| 202 | expect(result).toBe("grok-build"); |
| 203 | }), |
| 204 | ); |
| 205 | |
| 206 | it.effect("keeps the session's current model when the product slug is requested", () => |
| 207 | Effect.gen(function* () { |
| 208 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 209 | const result = yield* applyGrokAcpModelSelection({ |
| 210 | runtime, |
| 211 | currentModelId: "grok-4.6", |
| 212 | requestedModelId: "grok-build", |
| 213 | mapError: (cause) => cause.message, |
| 214 | }); |
| 215 | expect(modelCalls).toEqual([]); |
| 216 | expect(result).toBe("grok-4.6"); |
| 217 | }), |
| 218 | ); |
| 219 | |
| 220 | it.effect("applies reasoning to the current model when the product slug is requested", () => |
| 221 | Effect.gen(function* () { |
| 222 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 223 | const result = yield* applyGrokAcpModelSelection({ |
| 224 | runtime, |
| 225 | currentModelId: "grok-4.6", |
| 226 | currentReasoningEffort: "high", |
| 227 | requestedModelId: "grok-build", |
| 228 | requestedReasoningEffort: "xhigh", |
| 229 | mapError: (cause) => cause.message, |
| 230 | }); |
| 231 | expect(modelCalls).toEqual([{ modelId: "grok-4.6", meta: { reasoningEffort: "xhigh" } }]); |
| 232 | expect(result).toBe("grok-4.6"); |
| 233 | }), |
| 234 | ); |
| 235 | |
| 236 | it.effect("skips set_model when no model is requested", () => |
| 237 | Effect.gen(function* () { |
| 238 | const { runtime, modelCalls } = makeRecordingRuntime(); |
| 239 | const result = yield* applyGrokAcpModelSelection({ |
| 240 | runtime, |
| 241 | currentModelId: "grok-build", |
| 242 | requestedModelId: undefined, |
| 243 | mapError: (cause) => cause.message, |
| 244 | }); |
| 245 | expect(modelCalls).toEqual([]); |
| 246 | expect(result).toBe("grok-build"); |
| 247 | }), |
| 248 | ); |
| 249 | |
| 250 | it.effect("propagates session/set_model failures via mapError", () => |
| 251 | Effect.gen(function* () { |
| 252 | const failure = EffectAcpErrors.AcpRequestError.invalidParams("session id not known"); |
| 253 | const { runtime } = makeRecordingRuntime(failure); |
| 254 | const error = yield* Effect.flip( |
| 255 | applyGrokAcpModelSelection({ |
| 256 | runtime, |
| 257 | currentModelId: "grok-build", |
| 258 | requestedModelId: "grok-mock-alt", |
| 259 | mapError: (cause) => cause.message, |
| 260 | }), |
| 261 | ); |
| 262 | expect(error).toBe(failure.message); |
| 263 | }), |
| 264 | ); |
| 265 | }); |