1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import { ProviderInstanceId } from "@t3tools/contracts";
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import * as HttpClient from "effect/http/HttpClient";
11import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
12
13import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
14import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
15import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
16import { GrokDriver } from "./driver.ts";
17
18import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
19import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
20
21const layerTest = TestProviderHost.layer({ runBackgroundWork: false }).pipe(
22 Layer.provideMerge(NodeServices.layer),
23 Layer.provideMerge(IdAllocator.layer),
24 Layer.provideMerge(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)),
25 Layer.provideMerge(ProviderLatestVersions.layer),
26 Layer.provideMerge(
27 Layer.succeed(
28 ProviderEventLoggers.ProviderEventLoggers,
29 ProviderEventLoggers.NoOpProviderEventLoggers,
30 ),
31 ),
32 Layer.provideMerge(
33 Layer.succeed(
34 HttpClient.HttpClient,
35 HttpClient.make(() => Effect.die("Disabled Grok must not make an HTTP request")),
36 ),
37 ),
38);
39
40const noSpawner = ChildProcessSpawner.make(() =>
41 Effect.die("Disabled Grok must not spawn a process"),
42);
43
44// The `#!/bin/sh` stub below cannot be resolved as an executable on Windows.
45const windowsHost = HostProcess.Platform.defaultValue() === "win32";
46
47it.layer(layerTest)("GrokDriver", (it) => {
48 it.effect.skipIf(windowsHost)("updates through the configured executable's own updater", () =>
49 Effect.gen(function* () {
50 const fs = yield* FileSystem.FileSystem;
51 const path = yield* Path.Path;
52 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-driver-" });
53 const grokHome = path.join(tempDir, "Grok Home");
54 const binaryPath = path.join(grokHome, "bin", "grok");
55 yield* fs.makeDirectory(path.dirname(binaryPath), { recursive: true });
56 yield* fs.writeFileString(binaryPath, "#!/bin/sh\n");
57 yield* fs.chmod(binaryPath, 0o755);
58
59 const instance = yield* GrokDriver.create({
60 instanceId: ProviderInstanceId.make("grok-update"),
61 displayName: "Grok test",
62 enabled: false,
63 environment: [{ name: "GROK_HOME", value: grokHome, sensitive: false }],
64 config: { ...GrokDriver.defaultConfig(), binaryPath },
65 });
66
67 const capabilities = yield* instance.snapshot.resolveMaintenance();
68 expect(capabilities.packageName).toBe("@xai-official/grok");
69 expect(capabilities.update).toMatchObject({
70 command: `'${binaryPath}' update`,
71 executable: binaryPath,
72 args: ["update"],
73 });
74 // `grok update` installs under GROK_HOME, so it must target this instance's home.
75 expect(capabilities.update?.env?.GROK_HOME).toBe(grokHome);
76 }).pipe(
77 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawner),
78 Effect.scoped,
79 ),
80 );
81
82 it.effect("stays manual-only when the configured executable does not exist", () =>
83 Effect.gen(function* () {
84 const fs = yield* FileSystem.FileSystem;
85 const path = yield* Path.Path;
86 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-grok-missing-" });
87 const instance = yield* GrokDriver.create({
88 instanceId: ProviderInstanceId.make("grok-missing"),
89 displayName: "Grok test",
90 enabled: false,
91 environment: [],
92 config: { ...GrokDriver.defaultConfig(), binaryPath: path.join(tempDir, "grok") },
93 });
94 expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
95 }).pipe(
96 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, noSpawner),
97 Effect.scoped,
98 ),
99 );
100});