offrouter-adapter-grok 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "offrouter-adapter-grok",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "test": "vitest run --root ../.. packages/adapter-grok/src"
10
+ },
11
+ "dependencies": {
12
+ "offrouter-core": "0.0.0",
13
+ "smol-toml": "0.0.0",
14
+ "zod": "0.0.0"
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "offrouter-adapter-grok",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "test": "vitest run --root ../.. packages/adapter-grok/src"
10
+ },
11
+ "dependencies": {
12
+ "offrouter-core": "*",
13
+ "smol-toml": "^1.7.0",
14
+ "zod": "^3.23.0"
15
+ },
16
+ "private": true
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * offrouter-adapter-grok public surface.
3
+ * Install / rollback for Grok personal profiles (MCP config entry).
4
+ */
5
+
6
+ export const ADAPTER_GROK_PACKAGE = "offrouter-adapter-grok" as const;
7
+
8
+ export {
9
+ isGrokHarnessProfile,
10
+ isSafeProfileId,
11
+ isWorkLikeProfile,
12
+ listAllowlistedPersonalGrokProfiles,
13
+ resolveGrokProfileDir,
14
+ SAFE_PROFILE_ID_RE,
15
+ } from "./profile.js";
16
+ export type { ResolveGrokProfileDirOptions } from "./profile.js";
17
+
18
+ export {
19
+ GROK_CONFIG_REL,
20
+ GROK_INSTALL_STATE_REL,
21
+ GrokInstallStateSchema,
22
+ evaluateInstallProfilePolicy,
23
+ formatInstallDiffText,
24
+ installGrokProfile,
25
+ patchConfigForMcp,
26
+ } from "./install.js";
27
+ export type {
28
+ GrokInstallOptions,
29
+ GrokInstallResult,
30
+ GrokInstallState,
31
+ InstallChangeKind,
32
+ InstallFileChange,
33
+ } from "./install.js";
34
+
35
+ export { rollbackGrokProfile } from "./rollback.js";
36
+ export type { GrokRollbackOptions, GrokRollbackResult } from "./rollback.js";
@@ -0,0 +1,584 @@
1
+ import {
2
+ access,
3
+ mkdir,
4
+ mkdtemp,
5
+ readFile,
6
+ rm,
7
+ writeFile,
8
+ } from "node:fs/promises";
9
+ import { constants } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { afterEach, describe, expect, it } from "vitest";
13
+ import { parse as parseToml } from "smol-toml";
14
+ import { loadConfig } from "offrouter-core";
15
+ import {
16
+ GROK_CONFIG_REL,
17
+ GROK_INSTALL_STATE_REL,
18
+ installGrokProfile,
19
+ patchConfigForMcp,
20
+ type GrokInstallOptions,
21
+ type GrokInstallResult,
22
+ } from "./install.js";
23
+ import {
24
+ listAllowlistedPersonalGrokProfiles,
25
+ resolveGrokProfileDir,
26
+ } from "./profile.js";
27
+ import { rollbackGrokProfile } from "./rollback.js";
28
+
29
+ const tempRoots: string[] = [];
30
+
31
+ async function makeTempHome(prefix: string): Promise<string> {
32
+ const root = await mkdtemp(join(tmpdir(), prefix));
33
+ tempRoots.push(root);
34
+ return root;
35
+ }
36
+
37
+ async function exists(path: string): Promise<boolean> {
38
+ try {
39
+ await access(path, constants.F_OK);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ async function writeAllowlist(
47
+ offrouterHome: string,
48
+ profiles: string[],
49
+ ): Promise<void> {
50
+ await mkdir(offrouterHome, { recursive: true });
51
+ const listed = profiles.map((p) => JSON.stringify(p)).join(", ");
52
+ await writeFile(
53
+ join(offrouterHome, "config.toml"),
54
+ `allowlisted_profiles = [${listed}]\n`,
55
+ "utf8",
56
+ );
57
+ }
58
+
59
+ function baseEnv(home: string): NodeJS.ProcessEnv {
60
+ return {
61
+ ...process.env,
62
+ HOME: home,
63
+ OFFROUTER_HOME: join(home, ".offrouter"),
64
+ OFFROUTER_GROK_PROFILES_DIR: join(home, "grok-profiles"),
65
+ // Never inherit real workspace trust / profile for these unit tests
66
+ OFFROUTER_PROFILE: undefined,
67
+ OFFROUTER_WORKSPACE_TRUSTED: undefined,
68
+ };
69
+ }
70
+
71
+ async function installOpts(
72
+ home: string,
73
+ profile: string,
74
+ overrides: Partial<GrokInstallOptions> = {},
75
+ ): Promise<GrokInstallOptions> {
76
+ const env = baseEnv(home);
77
+ const config = await loadConfig({
78
+ env,
79
+ homedir: () => home,
80
+ });
81
+ return {
82
+ profile,
83
+ env,
84
+ homedir: () => home,
85
+ config,
86
+ ...overrides,
87
+ };
88
+ }
89
+
90
+ function parseMcpServers(raw: string): {
91
+ mcp_servers: Record<string, { command: string; args: string[] }>;
92
+ } {
93
+ return parseToml(raw) as {
94
+ mcp_servers: Record<string, { command: string; args: string[] }>;
95
+ };
96
+ }
97
+
98
+ afterEach(async () => {
99
+ while (tempRoots.length > 0) {
100
+ const root = tempRoots.pop();
101
+ if (root) {
102
+ await rm(root, { recursive: true, force: true });
103
+ }
104
+ }
105
+ });
106
+
107
+ describe("resolveGrokProfileDir", () => {
108
+ it("uses OFFROUTER_GROK_PROFILES_DIR/<profile> when set", () => {
109
+ const home = "/tmp/not-used-home";
110
+ const dir = resolveGrokProfileDir("grok-personal", {
111
+ env: {
112
+ OFFROUTER_GROK_PROFILES_DIR: "/tmp/profiles-root",
113
+ },
114
+ homedir: () => home,
115
+ });
116
+ expect(dir).toBe(join("/tmp/profiles-root", "grok-personal"));
117
+ });
118
+
119
+ it("maps profile id grok to ~/.grok and grok-personal to ~/.grok-personal", () => {
120
+ const home = "/tmp/fake-home-map";
121
+ expect(
122
+ resolveGrokProfileDir("grok", {
123
+ env: {},
124
+ homedir: () => home,
125
+ }),
126
+ ).toBe(join(home, ".grok"));
127
+ expect(
128
+ resolveGrokProfileDir("grok-personal", {
129
+ env: {},
130
+ homedir: () => home,
131
+ }),
132
+ ).toBe(join(home, ".grok-personal"));
133
+ });
134
+
135
+ it("maps profile id grok to GROK_HOME when set", () => {
136
+ const home = "/tmp/fake-home-grok";
137
+ expect(
138
+ resolveGrokProfileDir("grok", {
139
+ env: { GROK_HOME: "/tmp/custom-grok-home" },
140
+ homedir: () => home,
141
+ }),
142
+ ).toBe("/tmp/custom-grok-home");
143
+ });
144
+
145
+ it("rejects unsafe profile ids before resolving profile paths", () => {
146
+ expect(() =>
147
+ resolveGrokProfileDir("../grok-personal", {
148
+ env: {
149
+ OFFROUTER_GROK_PROFILES_DIR: "/tmp/profiles-root",
150
+ },
151
+ homedir: () => "/tmp/fake-home",
152
+ }),
153
+ ).toThrow(/unsafe profile id/i);
154
+ });
155
+ });
156
+
157
+ describe("installGrokProfile", () => {
158
+ it("dry-run shows exact file changes and does not write files", async () => {
159
+ const home = await makeTempHome("lr-grok-install-dry-");
160
+ await writeAllowlist(join(home, ".offrouter"), ["grok-personal"]);
161
+ const opts = await installOpts(home, "grok-personal", { dryRun: true });
162
+ const result = await installGrokProfile(opts);
163
+
164
+ expect(result.ok).toBe(true);
165
+ expect(result.dryRun).toBe(true);
166
+ expect(result.profile).toBe("grok-personal");
167
+ expect(result.changes.length).toBeGreaterThan(0);
168
+
169
+ const profileDir = resolveGrokProfileDir("grok-personal", opts);
170
+ const configPath = join(profileDir, GROK_CONFIG_REL);
171
+ const statePath = join(profileDir, GROK_INSTALL_STATE_REL);
172
+
173
+ expect(result.changes.some((c) => c.relativePath === GROK_CONFIG_REL)).toBe(
174
+ true,
175
+ );
176
+ const configChange = result.changes.find(
177
+ (c) => c.relativePath === GROK_CONFIG_REL,
178
+ );
179
+ expect(configChange?.kind).toBe("create");
180
+ expect(await exists(configPath)).toBe(false);
181
+ expect(await exists(statePath)).toBe(false);
182
+ expect(await exists(join(home, ".grok"))).toBe(false);
183
+ });
184
+
185
+ it("install writes only selected profile and not sibling work/default dirs", async () => {
186
+ const home = await makeTempHome("lr-grok-install-scope-");
187
+ await writeAllowlist(join(home, ".offrouter"), ["grok-personal"]);
188
+
189
+ // Pre-create sibling profile dirs with markers so we can detect writes.
190
+ const profilesRoot = join(home, "grok-profiles");
191
+ await mkdir(join(profilesRoot, "grok-work"), { recursive: true });
192
+ await mkdir(join(profilesRoot, "grok"), { recursive: true });
193
+ await writeFile(
194
+ join(profilesRoot, "grok-work", "marker.txt"),
195
+ "work-untouched",
196
+ "utf8",
197
+ );
198
+ await writeFile(
199
+ join(profilesRoot, "grok", "marker.txt"),
200
+ "default-untouched",
201
+ "utf8",
202
+ );
203
+ await mkdir(join(home, ".grok"), { recursive: true });
204
+ await writeFile(
205
+ join(home, ".grok", "marker.txt"),
206
+ "global-untouched",
207
+ "utf8",
208
+ );
209
+
210
+ const opts = await installOpts(home, "grok-personal");
211
+ const result = await installGrokProfile(opts);
212
+ expect(result.ok).toBe(true);
213
+ expect(result.dryRun).toBe(false);
214
+
215
+ const profileDir = resolveGrokProfileDir("grok-personal", opts);
216
+ const configPath = join(profileDir, GROK_CONFIG_REL);
217
+ expect(await exists(configPath)).toBe(true);
218
+ const configRaw = await readFile(configPath, "utf8");
219
+ const parsed = parseMcpServers(configRaw);
220
+ const entry = parsed.mcp_servers.offrouter;
221
+ expect(entry).toBeDefined();
222
+ expect(entry.command).toBe("offrouter");
223
+ expect(entry.args).toEqual(["mcp", "serve"]);
224
+
225
+ // Sibling profile and real home ~/.grok markers untouched
226
+ expect(
227
+ await readFile(join(profilesRoot, "grok-work", "marker.txt"), "utf8"),
228
+ ).toBe("work-untouched");
229
+ expect(
230
+ await readFile(join(profilesRoot, "grok", "marker.txt"), "utf8"),
231
+ ).toBe("default-untouched");
232
+ expect(await readFile(join(home, ".grok", "marker.txt"), "utf8")).toBe(
233
+ "global-untouched",
234
+ );
235
+ expect(await exists(join(profilesRoot, "grok-work", "config.toml"))).toBe(
236
+ false,
237
+ );
238
+ expect(await exists(join(home, ".grok", "config.toml"))).toBe(false);
239
+
240
+ // State lives inside target profile only
241
+ const statePath = join(profileDir, GROK_INSTALL_STATE_REL);
242
+ expect(await exists(statePath)).toBe(true);
243
+ const state = JSON.parse(await readFile(statePath, "utf8")) as {
244
+ profile: string;
245
+ harness: string;
246
+ files: Record<string, { previous: string | null }>;
247
+ };
248
+ expect(state.profile).toBe("grok-personal");
249
+ expect(state.harness).toBe("grok");
250
+ expect(state.files[GROK_CONFIG_REL]).toBeDefined();
251
+ });
252
+
253
+ it("refuses denied work profiles", async () => {
254
+ const home = await makeTempHome("lr-grok-install-work-");
255
+ // Even if someone allowlists a work profile, policy still denies *-work.
256
+ await writeAllowlist(join(home, ".offrouter"), ["grok-work"]);
257
+ const opts = await installOpts(home, "grok-work", { dryRun: true });
258
+ const result = await installGrokProfile(opts);
259
+ expect(result.ok).toBe(false);
260
+ expect(result.errorCode).toBe("work_profile_denied");
261
+ expect(result.message.toLowerCase()).toMatch(/work|denied/);
262
+ expect(
263
+ await exists(join(home, "grok-profiles", "grok-work", "config.toml")),
264
+ ).toBe(false);
265
+ });
266
+
267
+ it("refuses work-like profile ids such as grok-work-staging even if allowlisted", async () => {
268
+ const home = await makeTempHome("lr-grok-install-work-staging-");
269
+ // Core policy only hard-denies ids ending in -work, so grok-work-staging
270
+ // would otherwise pass routeRequest. The installer must still refuse it.
271
+ await writeAllowlist(join(home, ".offrouter"), ["grok-work-staging"]);
272
+ const opts = await installOpts(home, "grok-work-staging", {
273
+ dryRun: true,
274
+ });
275
+ const result = await installGrokProfile(opts);
276
+ expect(result.ok).toBe(false);
277
+ expect(result.errorCode).toBe("work_profile_denied");
278
+ expect(result.message.toLowerCase()).toContain("work");
279
+ expect(
280
+ await exists(
281
+ join(home, "grok-profiles", "grok-work-staging", "config.toml"),
282
+ ),
283
+ ).toBe(false);
284
+ });
285
+
286
+ it("refuses unallowlisted personal profiles", async () => {
287
+ const home = await makeTempHome("lr-grok-install-unallow-");
288
+ await writeAllowlist(join(home, ".offrouter"), []); // nothing allowlisted
289
+ const opts = await installOpts(home, "grok-personal", { dryRun: true });
290
+ const result = await installGrokProfile(opts);
291
+ expect(result.ok).toBe(false);
292
+ expect(result.errorCode).toBe("profile_not_allowlisted");
293
+ expect(result.message.toLowerCase()).toMatch(
294
+ /allowlist|not allowlisted|explicitly/,
295
+ );
296
+ });
297
+
298
+ it("refuses unsafe profile ids with a structured install error", async () => {
299
+ const home = await makeTempHome("lr-grok-install-unsafe-");
300
+ await writeAllowlist(join(home, ".offrouter"), ["../grok-personal"]);
301
+ const opts = await installOpts(home, "../grok-personal", {
302
+ dryRun: true,
303
+ });
304
+ const result = await installGrokProfile(opts);
305
+ expect(result.ok).toBe(false);
306
+ expect(result.errorCode).toBe("install_error");
307
+ expect(result.profileDir).toBe("");
308
+ expect(result.message.toLowerCase()).toContain("unsafe profile id");
309
+ expect(await exists(join(home, "skills"))).toBe(false);
310
+ });
311
+
312
+ it("refuses allowlisted non-Grok profiles for Grok install", async () => {
313
+ const home = await makeTempHome("lr-grok-install-non-grok-");
314
+ await writeAllowlist(join(home, ".offrouter"), ["codex-personal"]);
315
+ const opts = await installOpts(home, "codex-personal", { dryRun: true });
316
+ const result = await installGrokProfile(opts);
317
+ expect(result.ok).toBe(false);
318
+ expect(result.errorCode).toBe("install_error");
319
+ expect(result.message.toLowerCase()).toContain("grok install");
320
+ expect(
321
+ await exists(
322
+ join(home, "grok-profiles", "codex-personal", "config.toml"),
323
+ ),
324
+ ).toBe(false);
325
+ });
326
+
327
+ it("install --all-personal --dry-run returns a matrix of allowlisted personal Grok profiles", async () => {
328
+ const home = await makeTempHome("lr-grok-install-all-");
329
+ await writeAllowlist(join(home, ".offrouter"), [
330
+ "grok",
331
+ "grok-personal",
332
+ "grok-hobby",
333
+ "codex-personal",
334
+ "grok-work",
335
+ ]);
336
+ const env = baseEnv(home);
337
+ const config = await loadConfig({ env, homedir: () => home });
338
+ const profiles = listAllowlistedPersonalGrokProfiles(config);
339
+ expect(profiles).toEqual(
340
+ expect.arrayContaining(["grok-personal", "grok-hobby"]),
341
+ );
342
+ expect(profiles).not.toContain("grok");
343
+ expect(profiles).not.toContain("grok-work");
344
+ expect(profiles).not.toContain("codex-personal");
345
+
346
+ const matrix: GrokInstallResult[] = [];
347
+ for (const profile of profiles) {
348
+ matrix.push(
349
+ await installGrokProfile({
350
+ profile,
351
+ env,
352
+ homedir: () => home,
353
+ config,
354
+ dryRun: true,
355
+ }),
356
+ );
357
+ }
358
+ expect(matrix.every((r) => r.ok && r.dryRun)).toBe(true);
359
+ expect(matrix.map((r) => r.profile).sort()).toEqual(
360
+ ["grok-hobby", "grok-personal"].sort(),
361
+ );
362
+ expect(
363
+ matrix.every((r) =>
364
+ r.changes.some((c) => c.kind === "create" || c.kind === "update"),
365
+ ),
366
+ ).toBe(true);
367
+ });
368
+
369
+ it("rollback restores previous config.toml", async () => {
370
+ const home = await makeTempHome("lr-grok-install-rb-");
371
+ await writeAllowlist(join(home, ".offrouter"), ["grok-personal"]);
372
+
373
+ const opts = await installOpts(home, "grok-personal");
374
+ const profileDir = resolveGrokProfileDir("grok-personal", opts);
375
+ const configPath = join(profileDir, GROK_CONFIG_REL);
376
+ await mkdir(profileDir, { recursive: true });
377
+ await writeFile(configPath, 'model = "grok-code-fast-1"\n', "utf8");
378
+
379
+ const installResult = await installGrokProfile(opts);
380
+ expect(installResult.ok).toBe(true);
381
+ const afterInstall = parseToml(await readFile(configPath, "utf8")) as {
382
+ model?: string;
383
+ mcp_servers: Record<string, unknown>;
384
+ };
385
+ expect(afterInstall.mcp_servers.offrouter).toBeDefined();
386
+ expect(afterInstall.model).toBe("grok-code-fast-1");
387
+
388
+ // Preserve a sibling file inside .offrouter
389
+ const siblingStateFile = join(profileDir, ".offrouter", "notes.json");
390
+ await writeFile(siblingStateFile, '{"keep":true}\n', "utf8");
391
+
392
+ const rb = await rollbackGrokProfile({
393
+ profile: "grok-personal",
394
+ env: opts.env,
395
+ homedir: opts.homedir,
396
+ dryRun: false,
397
+ });
398
+ expect(rb.ok).toBe(true);
399
+ const restored = parseToml(await readFile(configPath, "utf8")) as {
400
+ model?: string;
401
+ mcp_servers?: Record<string, unknown>;
402
+ };
403
+ expect(restored).toEqual({ model: "grok-code-fast-1" });
404
+
405
+ const statePath = join(profileDir, GROK_INSTALL_STATE_REL);
406
+ expect(await exists(statePath)).toBe(false);
407
+ expect(await readFile(siblingStateFile, "utf8")).toBe('{"keep":true}\n');
408
+ });
409
+
410
+ it("refuses to reinstall when prior install state is invalid", async () => {
411
+ const home = await makeTempHome("lr-grok-install-corrupt-state-");
412
+ await writeAllowlist(join(home, ".offrouter"), ["grok-personal"]);
413
+ const opts = await installOpts(home, "grok-personal");
414
+ const profileDir = resolveGrokProfileDir("grok-personal", opts);
415
+ const configPath = join(profileDir, GROK_CONFIG_REL);
416
+
417
+ const first = await installGrokProfile(opts);
418
+ expect(first.ok).toBe(true);
419
+ await writeFile(configPath, "# user edit is not toml\n", "utf8");
420
+ await writeFile(
421
+ join(profileDir, GROK_INSTALL_STATE_REL),
422
+ "{not valid json",
423
+ "utf8",
424
+ );
425
+
426
+ const second = await installGrokProfile(opts);
427
+ expect(second.ok).toBe(false);
428
+ expect(second.message.toLowerCase()).toContain("invalid install state");
429
+ expect(await readFile(configPath, "utf8")).toBe(
430
+ "# user edit is not toml\n",
431
+ );
432
+ });
433
+
434
+ it("rollback refuses install state paths outside the profile root (path traversal)", async () => {
435
+ const home = await makeTempHome("lr-grok-install-rb-escape-");
436
+ const env = baseEnv(home);
437
+ const profile = "grok-personal";
438
+ const profileDir = resolveGrokProfileDir(profile, {
439
+ env,
440
+ homedir: () => home,
441
+ });
442
+ const outsidePath = join(home, "outside.txt");
443
+ const statePath = join(profileDir, GROK_INSTALL_STATE_REL);
444
+ await writeFile(outsidePath, "keep\n", "utf8");
445
+ await mkdir(join(profileDir, ".offrouter"), { recursive: true });
446
+ await writeFile(
447
+ statePath,
448
+ `${JSON.stringify(
449
+ {
450
+ version: 1,
451
+ profile,
452
+ harness: "grok",
453
+ installedAt: new Date(0).toISOString(),
454
+ configRelative: GROK_CONFIG_REL,
455
+ files: {
456
+ "../../outside.txt": { previous: null },
457
+ },
458
+ },
459
+ null,
460
+ 2,
461
+ )}\n`,
462
+ "utf8",
463
+ );
464
+
465
+ const rb = await rollbackGrokProfile({
466
+ profile,
467
+ env,
468
+ homedir: () => home,
469
+ });
470
+ expect(rb.ok).toBe(false);
471
+ expect(rb.errorCode).toBe("invalid_install_state");
472
+ expect(await readFile(outsidePath, "utf8")).toBe("keep\n");
473
+ expect(await exists(statePath)).toBe(true);
474
+ });
475
+
476
+ it("repeated install is idempotent and preserves original rollback", async () => {
477
+ const home = await makeTempHome("lr-grok-install-idemp-");
478
+ await writeAllowlist(join(home, ".offrouter"), ["grok-personal"]);
479
+ const opts = await installOpts(home, "grok-personal");
480
+ const profileDir = resolveGrokProfileDir("grok-personal", opts);
481
+ const configPath = join(profileDir, GROK_CONFIG_REL);
482
+
483
+ await mkdir(profileDir, { recursive: true });
484
+ await writeFile(configPath, 'model = "grok-code-fast-1"\n', "utf8");
485
+
486
+ const first = await installGrokProfile(opts);
487
+ const second = await installGrokProfile(opts);
488
+ expect(first.ok).toBe(true);
489
+ expect(second.ok).toBe(true);
490
+
491
+ const after = parseToml(await readFile(configPath, "utf8")) as {
492
+ model?: string;
493
+ mcp_servers: Record<string, { command: string; args: string[] }>;
494
+ };
495
+ expect(after.mcp_servers.offrouter).toBeDefined();
496
+ // Unrelated key is preserved across the TOML round-trip.
497
+ expect(after.model).toBe("grok-code-fast-1");
498
+
499
+ // State still tracks original contents for true rollback
500
+ const state = JSON.parse(
501
+ await readFile(join(profileDir, GROK_INSTALL_STATE_REL), "utf8"),
502
+ ) as { files: Record<string, { previous: string | null }> };
503
+ expect(state.files[GROK_CONFIG_REL]?.previous).toContain("model");
504
+
505
+ const rb = await rollbackGrokProfile({
506
+ profile: "grok-personal",
507
+ env: opts.env,
508
+ homedir: opts.homedir,
509
+ });
510
+ expect(rb.ok).toBe(true);
511
+ expect(parseToml(await readFile(configPath, "utf8"))).toEqual({
512
+ model: "grok-code-fast-1",
513
+ });
514
+ });
515
+ });
516
+
517
+ describe("patchConfigForMcp", () => {
518
+ it("creates a fresh config from a missing file", () => {
519
+ const out = patchConfigForMcp(null);
520
+ const parsed = parseMcpServers(out);
521
+ expect(parsed.mcp_servers.offrouter).toBeDefined();
522
+ expect(parsed.mcp_servers.offrouter.command).toBe("offrouter");
523
+ expect(parsed.mcp_servers.offrouter.args).toEqual(["mcp", "serve"]);
524
+ });
525
+
526
+ it("creates a fresh config from an empty or whitespace-only file", () => {
527
+ for (const empty of ["", " ", "\n\n"]) {
528
+ const out = patchConfigForMcp(empty);
529
+ const parsed = parseMcpServers(out);
530
+ expect(parsed.mcp_servers.offrouter).toBeDefined();
531
+ }
532
+ });
533
+
534
+ it("preserves unrelated keys when installing", () => {
535
+ const existing =
536
+ 'model = "grok-code-fast-1"\n\n' +
537
+ "[mcp_servers.other]\n" +
538
+ 'command = "other-tool"\n' +
539
+ 'args = ["run"]\n';
540
+ const out = patchConfigForMcp(existing);
541
+ const parsed = parseToml(out) as {
542
+ model: string;
543
+ mcp_servers: Record<string, { command: string; args: string[] }>;
544
+ };
545
+ expect(parsed.mcp_servers.offrouter).toBeDefined();
546
+ expect(parsed.mcp_servers.offrouter.command).toBe("offrouter");
547
+ // Unrelated keys and the other MCP server survive the round-trip.
548
+ expect(parsed.model).toBe("grok-code-fast-1");
549
+ expect(parsed.mcp_servers.other).toBeDefined();
550
+ expect(parsed.mcp_servers.other.command).toBe("other-tool");
551
+ });
552
+
553
+ it("overwrites a conflicting offrouter MCP entry", () => {
554
+ const existing =
555
+ "[mcp_servers.offrouter]\n" +
556
+ 'command = "old-tool"\n' +
557
+ 'args = ["different"]\n';
558
+ const out = patchConfigForMcp(existing);
559
+ const parsed = parseMcpServers(out);
560
+ expect(parsed.mcp_servers.offrouter.command).toBe("offrouter");
561
+ expect(parsed.mcp_servers.offrouter.args).toEqual(["mcp", "serve"]);
562
+ // The old command must be gone (overwritten, not appended).
563
+ expect(parsed.mcp_servers.offrouter.command).not.toBe("old-tool");
564
+ });
565
+
566
+ it("is idempotent when already installed correctly (detected by value)", () => {
567
+ // Matching command/args: detected as already installed, bytes untouched.
568
+ const existing =
569
+ "[mcp_servers.offrouter]\n" +
570
+ 'command = "offrouter"\n' +
571
+ 'args = ["mcp", "serve"]\n';
572
+ expect(patchConfigForMcp(existing)).toBe(existing);
573
+ });
574
+
575
+ it("treats a offrouter entry with wrong args as conflicting", () => {
576
+ const existing =
577
+ "[mcp_servers.offrouter]\n" +
578
+ 'command = "offrouter"\n' +
579
+ 'args = ["serve"]\n';
580
+ const out = patchConfigForMcp(existing);
581
+ const parsed = parseMcpServers(out);
582
+ expect(parsed.mcp_servers.offrouter.args).toEqual(["mcp", "serve"]);
583
+ });
584
+ });