1import { describe, expect, it } from "@effect/vitest";
2
3import * as Effect from "effect/Effect";
4import * as EffectAcpErrors from "effect-acp/errors";
5
6import {
7 applyGrokAcpModelSelection,
8 buildGrokAcpSpawnInput,
9 grokAcpRuntimeProcessOwnership,
10 grokAcpSpawnArgs,
11 isValidGrokReasoningEffortToken,
12 resolveGrokAcpBaseModelId,
13} from "./acpSupport.ts";
14
15describe("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
38describe("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
46describe("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
75describe("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
104describe("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
114describe("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});