1import { assert, describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Sink from "effect/Sink";
4import * as Stream from "effect/Stream";
5import type * as ChildProcess from "effect/process/ChildProcess";
6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
7
8import { discoverGrokSkills, leadingGrokSkillMention } from "./skills.ts";
9
10const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });
11
12const makeInspectSpawner = (
13 stdout: string,
14 exitCode = 0,
15 spawnOptions?: Array<ChildProcess.CommandOptions | undefined>,
16) =>
17 ChildProcessSpawner.make((command) => {
18 spawnOptions?.push(command._tag === "StandardCommand" ? command.options : undefined);
19 return Effect.succeed(
20 ChildProcessSpawner.makeHandle({
21 pid: ChildProcessSpawner.ProcessId(1),
22 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
23 isRunning: Effect.succeed(false),
24 kill: () => Effect.void,
25 unref: Effect.succeed(Effect.void),
26 stdin: Sink.drain,
27 stdout: Stream.encodeText(Stream.make(stdout)),
28 stderr: Stream.empty,
29 all: Stream.empty,
30 getInputFd: () => Sink.drain,
31 getOutputFd: () => Stream.empty,
32 }),
33 );
34 });
35
36describe("discoverGrokSkills", () => {
37 it.effect("maps inspect entries onto provider skills, sorted by name", () =>
38 Effect.gen(function* () {
39 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
40
41 expect(skills).toEqual([
42 {
43 name: "deploy",
44 description: "Deploy the app.",
45 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
46 scope: "plugin",
47 enabled: true,
48 },
49 {
50 name: "writing-docs",
51 description: "Write user docs.",
52 path: "/home/dev/.grok/skills/writing-docs/SKILL.md",
53 scope: "user",
54 enabled: true,
55 },
56 ]);
57 }).pipe(
58 Effect.provideService(
59 ChildProcessSpawner.ChildProcessSpawner,
60 makeInspectSpawner(
61 inspectPayload([
62 {
63 name: "writing-docs",
64 description: "Write user docs.",
65 source: { type: "user", path: "/home/dev/.grok/skills/writing-docs/SKILL.md" },
66 userInvocable: true,
67 },
68 {
69 name: "deploy",
70 description: "Deploy the app.",
71 source: {
72 type: "plugin",
73 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
74 },
75 userInvocable: true,
76 },
77 ]),
78 ),
79 ),
80 ),
81 );
82
83 it.effect("disables skills the CLI marks as not user-invocable", () =>
84 Effect.gen(function* () {
85 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
86
87 expect(skills).toEqual([
88 {
89 name: "internal-helper",
90 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
91 scope: "bundled",
92 enabled: false,
93 },
94 ]);
95 }).pipe(
96 Effect.provideService(
97 ChildProcessSpawner.ChildProcessSpawner,
98 makeInspectSpawner(
99 inspectPayload([
100 {
101 name: "internal-helper",
102 source: {
103 type: "bundled",
104 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
105 },
106 userInvocable: false,
107 },
108 ]),
109 ),
110 ),
111 ),
112 );
113
114 it.effect("skips entries without a name or a filesystem path", () =>
115 Effect.gen(function* () {
116 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
117 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
118 }).pipe(
119 Effect.provideService(
120 ChildProcessSpawner.ChildProcessSpawner,
121 makeInspectSpawner(
122 inspectPayload([
123 { name: " ", source: { type: "user", path: "/tmp/skills/a/SKILL.md" } },
124 { name: "no-path", source: { type: "user" } },
125 { name: "no-source" },
126 "not-an-object",
127 { name: "kept", source: { type: "project", path: "/repo/.grok/skills/kept/SKILL.md" } },
128 ]),
129 ),
130 ),
131 ),
132 );
133
134 it.effect("rejects malformed or unexpected output as a decode failure", () =>
135 Effect.gen(function* () {
136 for (const stdout of ["not json", "null", '{"skills":"nope"}', "{}"]) {
137 const error = yield* discoverGrokSkills({ binaryPath: "grok" }, {}).pipe(
138 Effect.flip,
139 Effect.provideService(
140 ChildProcessSpawner.ChildProcessSpawner,
141 makeInspectSpawner(stdout),
142 ),
143 );
144 expect(error).toMatchObject({ _tag: "GrokSkillsProbeError", stage: "decode" });
145 }
146 }),
147 );
148
149 it.effect("spawns in the configured cwd and rejects a failed probe", () => {
150 const spawnOptions: Array<ChildProcess.CommandOptions | undefined> = [];
151 const stdout = inspectPayload([
152 {
153 name: "kept",
154 source: { type: "project", path: "/workspaces/demo/.grok/skills/kept/SKILL.md" },
155 },
156 ]);
157
158 return Effect.gen(function* () {
159 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {}, "/workspaces/demo").pipe(
160 Effect.provideService(
161 ChildProcessSpawner.ChildProcessSpawner,
162 makeInspectSpawner(stdout, 0, spawnOptions),
163 ),
164 );
165
166 expect(spawnOptions.map((options) => options?.cwd)).toEqual(["/workspaces/demo"]);
167 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
168
169 const failed = yield* discoverGrokSkills({ binaryPath: "grok" }).pipe(
170 Effect.result,
171 Effect.provideService(
172 ChildProcessSpawner.ChildProcessSpawner,
173 makeInspectSpawner(stdout, 1),
174 ),
175 );
176 expect(failed._tag).toBe("Failure");
177 });
178 });
179
180 it.effect("disables Grok's updater for the probe without touching the caller's env", () => {
181 const spawnOptions: Array<ChildProcess.CommandOptions | undefined> = [];
182 const environment = { GROK_DISABLE_AUTOUPDATER: "0", GROK_HOME: "/home/dev/.grok" };
183
184 return Effect.gen(function* () {
185 yield* discoverGrokSkills({ binaryPath: "grok" }, environment, "/workspaces/demo").pipe(
186 Effect.provideService(
187 ChildProcessSpawner.ChildProcessSpawner,
188 makeInspectSpawner(inspectPayload([]), 0, spawnOptions),
189 ),
190 );
191
192 expect(spawnOptions[0]?.env).toEqual({
193 GROK_DISABLE_AUTOUPDATER: "1",
194 GROK_HOME: "/home/dev/.grok",
195 MISE_OFFLINE: "1",
196 });
197 expect(environment.GROK_DISABLE_AUTOUPDATER).toBe("0");
198 });
199 });
200});
201
202describe("leadingGrokSkillMention", () => {
203 it.each([
204 { text: " €deploy\nship it", mention: { name: "deploy", rest: "\nship it" } },
205 { text: "$deploy", mention: { name: "deploy", rest: "" } },
206 { text: "$25 off", mention: undefined },
207 { text: "$deploy, then ship", mention: undefined },
208 ])("reads $text", ({ text, mention }) => {
209 assert.deepEqual(leadingGrokSkillMention(text), mention);
210 });
211});