authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 01:55:26-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 01:55:26-07:00
log25cbcd62d9750d3e8903fd752078e66c7943ecdb
tree022688272059c7d3b3c0bf20ca711b16eb8cc09b
parent009c13fade9dde2fe7eed068c72c579172f12e60
signature Signed by PGP key B5690EEEBB952194

test(server): cover Grok skill parsing through discovery (#10070)


2 files changed, 136 insertions(+), 109 deletions(-)

apps/server/src/provider/Drivers/GrokSkills.test.ts+136-105
......@@ -1,141 +1,172 @@
11import { describe, expect, it } from "@effect/vitest";
22import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
43import * as Sink from "effect/Sink";
54import * as Stream from "effect/Stream";
65import { ChildProcessSpawner } from "effect/unstable/process";
76
8import { discoverGrokSkills, parseGrokInspectSkills } from "./GrokSkills.ts";
7import { discoverGrokSkills } from "./GrokSkills.ts";
98
109const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });
1110
12describe("parseGrokInspectSkills", () => {
13 it("maps inspect entries onto provider skills, sorted by name", () => {
14 const skills = parseGrokInspectSkills(
15 inspectPayload([
16 {
17 name: "writing-docs",
18 description: "Write user docs.",
19 source: { type: "user", path: "/home/dev/.grok/skills/writing-docs/SKILL.md" },
20 userInvocable: true,
21 },
11const makeInspectSpawner = (stdout: string, exitCode = 0, spawnCwds?: Array<string | undefined>) =>
12 ChildProcessSpawner.make((command) => {
13 spawnCwds?.push(command._tag === "StandardCommand" ? command.options.cwd : undefined);
14 return Effect.succeed(
15 ChildProcessSpawner.makeHandle({
16 pid: ChildProcessSpawner.ProcessId(1),
17 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
18 isRunning: Effect.succeed(false),
19 kill: () => Effect.void,
20 unref: Effect.succeed(Effect.void),
21 stdin: Sink.drain,
22 stdout: Stream.encodeText(Stream.make(stdout)),
23 stderr: Stream.empty,
24 all: Stream.empty,
25 getInputFd: () => Sink.drain,
26 getOutputFd: () => Stream.empty,
27 }),
28 );
29 });
30
31describe("discoverGrokSkills", () => {
32 it.effect("maps inspect entries onto provider skills, sorted by name", () =>
33 Effect.gen(function* () {
34 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
35
36 expect(skills).toEqual([
2237 {
2338 name: "deploy",
2439 description: "Deploy the app.",
25 source: {
26 type: "plugin",
27 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
28 },
29 userInvocable: true,
40 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
41 scope: "plugin",
42 enabled: true,
3043 },
31 ]),
32 );
44 {
45 name: "writing-docs",
46 description: "Write user docs.",
47 path: "/home/dev/.grok/skills/writing-docs/SKILL.md",
48 scope: "user",
49 enabled: true,
50 },
51 ]);
52 }).pipe(
53 Effect.provideService(
54 ChildProcessSpawner.ChildProcessSpawner,
55 makeInspectSpawner(
56 inspectPayload([
57 {
58 name: "writing-docs",
59 description: "Write user docs.",
60 source: { type: "user", path: "/home/dev/.grok/skills/writing-docs/SKILL.md" },
61 userInvocable: true,
62 },
63 {
64 name: "deploy",
65 description: "Deploy the app.",
66 source: {
67 type: "plugin",
68 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
69 },
70 userInvocable: true,
71 },
72 ]),
73 ),
74 ),
75 ),
76 );
3377
34 expect(skills).toEqual([
35 {
36 name: "deploy",
37 description: "Deploy the app.",
38 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
39 scope: "plugin",
40 enabled: true,
41 },
42 {
43 name: "writing-docs",
44 description: "Write user docs.",
45 path: "/home/dev/.grok/skills/writing-docs/SKILL.md",
46 scope: "user",
47 enabled: true,
48 },
49 ]);
50 });
78 it.effect("disables skills the CLI marks as not user-invocable", () =>
79 Effect.gen(function* () {
80 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
5181
52 it("disables skills the CLI marks as not user-invocable", () => {
53 const skills = parseGrokInspectSkills(
54 inspectPayload([
82 expect(skills).toEqual([
5583 {
5684 name: "internal-helper",
57 source: { type: "bundled", path: "/opt/grok/bundled/skills/internal-helper/SKILL.md" },
58 userInvocable: false,
85 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
86 scope: "bundled",
87 enabled: false,
5988 },
60 ]),
61 );
62
63 expect(skills).toEqual([
64 {
65 name: "internal-helper",
66 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
67 scope: "bundled",
68 enabled: false,
69 },
70 ]);
71 });
72
73 it("skips entries without a name or a filesystem path", () => {
74 const skills = parseGrokInspectSkills(
75 inspectPayload([
76 { name: " ", source: { type: "user", path: "/tmp/skills/a/SKILL.md" } },
77 { name: "no-path", source: { type: "user" } },
78 { name: "no-source" },
79 "not-an-object",
80 { name: "kept", source: { type: "project", path: "/repo/.grok/skills/kept/SKILL.md" } },
81 ]),
82 );
89 ]);
90 }).pipe(
91 Effect.provideService(
92 ChildProcessSpawner.ChildProcessSpawner,
93 makeInspectSpawner(
94 inspectPayload([
95 {
96 name: "internal-helper",
97 source: {
98 type: "bundled",
99 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
100 },
101 userInvocable: false,
102 },
103 ]),
104 ),
105 ),
106 ),
107 );
83108
84 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
85 });
109 it.effect("skips entries without a name or a filesystem path", () =>
110 Effect.gen(function* () {
111 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
112 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
113 }).pipe(
114 Effect.provideService(
115 ChildProcessSpawner.ChildProcessSpawner,
116 makeInspectSpawner(
117 inspectPayload([
118 { name: " ", source: { type: "user", path: "/tmp/skills/a/SKILL.md" } },
119 { name: "no-path", source: { type: "user" } },
120 { name: "no-source" },
121 "not-an-object",
122 { name: "kept", source: { type: "project", path: "/repo/.grok/skills/kept/SKILL.md" } },
123 ]),
124 ),
125 ),
126 ),
127 );
86128
87 it("returns an empty list for malformed or unexpected output", () => {
88 expect(parseGrokInspectSkills("not json")).toEqual([]);
89 expect(parseGrokInspectSkills("null")).toEqual([]);
90 expect(parseGrokInspectSkills(JSON.stringify({ skills: "nope" }))).toEqual([]);
91 expect(parseGrokInspectSkills(JSON.stringify({}))).toEqual([]);
92 });
93});
129 it.effect("rejects malformed or unexpected output as a decode failure", () =>
130 Effect.gen(function* () {
131 for (const stdout of ["not json", "null", '{"skills":"nope"}', "{}"]) {
132 const error = yield* discoverGrokSkills({ binaryPath: "grok" }, {}).pipe(
133 Effect.flip,
134 Effect.provideService(
135 ChildProcessSpawner.ChildProcessSpawner,
136 makeInspectSpawner(stdout),
137 ),
138 );
139 expect(error).toMatchObject({ _tag: "GrokSkillsProbeError", stage: "decode" });
140 }
141 }),
142 );
94143
95describe("discoverGrokSkills", () => {
96144 it.effect("spawns in the configured cwd and rejects a failed probe", () => {
97145 const spawnCwds: Array<string | undefined> = [];
98 let exitCode = 0;
99 const spawner = ChildProcessSpawner.make((command) => {
100 spawnCwds.push(command._tag === "StandardCommand" ? command.options.cwd : undefined);
101 return Effect.succeed(
102 ChildProcessSpawner.makeHandle({
103 pid: ChildProcessSpawner.ProcessId(1),
104 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
105 isRunning: Effect.succeed(false),
106 kill: () => Effect.void,
107 unref: Effect.succeed(Effect.void),
108 stdin: Sink.drain,
109 stdout: Stream.encodeText(
110 Stream.make(
111 inspectPayload([
112 {
113 name: "kept",
114 source: { type: "project", path: "/workspaces/demo/.grok/skills/kept/SKILL.md" },
115 },
116 ]),
117 ),
118 ),
119 stderr: Stream.empty,
120 all: Stream.empty,
121 getInputFd: () => Sink.drain,
122 getOutputFd: () => Stream.empty,
123 }),
124 );
125 });
146 const stdout = inspectPayload([
147 {
148 name: "kept",
149 source: { type: "project", path: "/workspaces/demo/.grok/skills/kept/SKILL.md" },
150 },
151 ]);
126152
127153 return Effect.gen(function* () {
128154 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {}, "/workspaces/demo").pipe(
129 Effect.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)),
155 Effect.provideService(
156 ChildProcessSpawner.ChildProcessSpawner,
157 makeInspectSpawner(stdout, 0, spawnCwds),
158 ),
130159 );
131160
132161 expect(spawnCwds).toEqual(["/workspaces/demo"]);
133162 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
134163
135 exitCode = 1;
136164 const failed = yield* discoverGrokSkills({ binaryPath: "grok" }).pipe(
137165 Effect.result,
138 Effect.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)),
166 Effect.provideService(
167 ChildProcessSpawner.ChildProcessSpawner,
168 makeInspectSpawner(stdout, 1),
169 ),
139170 );
140171 expect(failed._tag).toBe("Failure");
141172 });
apps/server/src/provider/Drivers/GrokSkills.ts-4
......@@ -91,10 +91,6 @@ function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSk
9191 return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
9292}
9393
94export function parseGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSkill> {
95 return decodeGrokInspectSkills(stdout) ?? [];
96}
97
9894/**
9995 * Run `grok inspect --json` and map the reported catalog onto provider
10096 * skills. Callers that need best-effort discovery can recover this effect to