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 @@...@@ -1,141 +1,172 @@
1import { describe, expect, it } from "@effect/vitest";1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Sink from "effect/Sink";3import * as Sink from "effect/Sink";
5import * as Stream from "effect/Stream";4import * as Stream from "effect/Stream";
6import { ChildProcessSpawner } from "effect/unstable/process";5import { ChildProcessSpawner } from "effect/unstable/process";
76
8import { discoverGrokSkills, parseGrokInspectSkills } from "./GrokSkills.ts";7import { discoverGrokSkills } from "./GrokSkills.ts";
98
10const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });9const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });
1110
12describe("parseGrokInspectSkills", () => {11const makeInspectSpawner = (stdout: string, exitCode = 0, spawnCwds?: Array<string | undefined>) =>
13 it("maps inspect entries onto provider skills, sorted by name", () => {12 ChildProcessSpawner.make((command) => {
14 const skills = parseGrokInspectSkills(13 spawnCwds?.push(command._tag === "StandardCommand" ? command.options.cwd : undefined);
15 inspectPayload([14 return Effect.succeed(
16 {15 ChildProcessSpawner.makeHandle({
17 name: "writing-docs",16 pid: ChildProcessSpawner.ProcessId(1),
18 description: "Write user docs.",17 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(exitCode)),
19 source: { type: "user", path: "/home/dev/.grok/skills/writing-docs/SKILL.md" },18 isRunning: Effect.succeed(false),
20 userInvocable: true,19 kill: () => Effect.void,
21 },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([
22 {37 {
23 name: "deploy",38 name: "deploy",
24 description: "Deploy the app.",39 description: "Deploy the app.",
25 source: {40 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",
26 type: "plugin",41 scope: "plugin",
27 path: "/home/dev/.grok/installed-plugins/pkg/plug/skills/deploy/SKILL.md",42 enabled: true,
28 },
29 userInvocable: true,
30 },43 },
31 ]),44 {
32 );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([78 it.effect("disables skills the CLI marks as not user-invocable", () =>
35 {79 Effect.gen(function* () {
36 name: "deploy",80 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {});
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 });
5181
52 it("disables skills the CLI marks as not user-invocable", () => {82 expect(skills).toEqual([
53 const skills = parseGrokInspectSkills(
54 inspectPayload([
55 {83 {
56 name: "internal-helper",84 name: "internal-helper",
57 source: { type: "bundled", path: "/opt/grok/bundled/skills/internal-helper/SKILL.md" },85 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
58 userInvocable: false,86 scope: "bundled",
87 enabled: false,
59 },88 },
60 ]),89 ]);
61 );90 }).pipe(
6291 Effect.provideService(
63 expect(skills).toEqual([92 ChildProcessSpawner.ChildProcessSpawner,
64 {93 makeInspectSpawner(
65 name: "internal-helper",94 inspectPayload([
66 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",95 {
67 scope: "bundled",96 name: "internal-helper",
68 enabled: false,97 source: {
69 },98 type: "bundled",
70 ]);99 path: "/opt/grok/bundled/skills/internal-helper/SKILL.md",
71 });100 },
72101 userInvocable: false,
73 it("skips entries without a name or a filesystem path", () => {102 },
74 const skills = parseGrokInspectSkills(103 ]),
75 inspectPayload([104 ),
76 { name: " ", source: { type: "user", path: "/tmp/skills/a/SKILL.md" } },105 ),
77 { name: "no-path", source: { type: "user" } },106 ),
78 { name: "no-source" },107 );
79 "not-an-object",
80 { name: "kept", source: { type: "project", path: "/repo/.grok/skills/kept/SKILL.md" } },
81 ]),
82 );
83108
84 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);109 it.effect("skips entries without a name or a filesystem path", () =>
85 });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", () => {129 it.effect("rejects malformed or unexpected output as a decode failure", () =>
88 expect(parseGrokInspectSkills("not json")).toEqual([]);130 Effect.gen(function* () {
89 expect(parseGrokInspectSkills("null")).toEqual([]);131 for (const stdout of ["not json", "null", '{"skills":"nope"}', "{}"]) {
90 expect(parseGrokInspectSkills(JSON.stringify({ skills: "nope" }))).toEqual([]);132 const error = yield* discoverGrokSkills({ binaryPath: "grok" }, {}).pipe(
91 expect(parseGrokInspectSkills(JSON.stringify({}))).toEqual([]);133 Effect.flip,
92 });134 Effect.provideService(
93});135 ChildProcessSpawner.ChildProcessSpawner,
136 makeInspectSpawner(stdout),
137 ),
138 );
139 expect(error).toMatchObject({ _tag: "GrokSkillsProbeError", stage: "decode" });
140 }
141 }),
142 );
94143
95describe("discoverGrokSkills", () => {
96 it.effect("spawns in the configured cwd and rejects a failed probe", () => {144 it.effect("spawns in the configured cwd and rejects a failed probe", () => {
97 const spawnCwds: Array<string | undefined> = [];145 const spawnCwds: Array<string | undefined> = [];
98 let exitCode = 0;146 const stdout = inspectPayload([
99 const spawner = ChildProcessSpawner.make((command) => {147 {
100 spawnCwds.push(command._tag === "StandardCommand" ? command.options.cwd : undefined);148 name: "kept",
101 return Effect.succeed(149 source: { type: "project", path: "/workspaces/demo/.grok/skills/kept/SKILL.md" },
102 ChildProcessSpawner.makeHandle({150 },
103 pid: ChildProcessSpawner.ProcessId(1),151 ]);
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 });
126152
127 return Effect.gen(function* () {153 return Effect.gen(function* () {
128 const skills = yield* discoverGrokSkills({ binaryPath: "grok" }, {}, "/workspaces/demo").pipe(154 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 ),
130 );159 );
131160
132 expect(spawnCwds).toEqual(["/workspaces/demo"]);161 expect(spawnCwds).toEqual(["/workspaces/demo"]);
133 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);162 expect(skills.map((skill) => skill.name)).toEqual(["kept"]);
134163
135 exitCode = 1;
136 const failed = yield* discoverGrokSkills({ binaryPath: "grok" }).pipe(164 const failed = yield* discoverGrokSkills({ binaryPath: "grok" }).pipe(
137 Effect.result,165 Effect.result,
138 Effect.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)),166 Effect.provideService(
167 ChildProcessSpawner.ChildProcessSpawner,
168 makeInspectSpawner(stdout, 1),
169 ),
139 );170 );
140 expect(failed._tag).toBe("Failure");171 expect(failed._tag).toBe("Failure");
141 });172 });
apps/server/src/provider/Drivers/GrokSkills.ts-4
...@@ -91,10 +91,6 @@ function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSk...@@ -91,10 +91,6 @@ function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSk
91 return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));91 return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
92}92}
9393
94export function parseGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSkill> {
95 return decodeGrokInspectSkills(stdout) ?? [];
96}
97
98/**94/**
99 * Run `grok inspect --json` and map the reported catalog onto provider95 * Run `grok inspect --json` and map the reported catalog onto provider
100 * skills. Callers that need best-effort discovery can recover this effect to96 * skills. Callers that need best-effort discovery can recover this effect to