History

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

Julius Marminge committed 2 months ago

Commit 25cbcd61 parent 009c13f2 files · +136 −109Browse files

Unified · Split

2 files
  1. apps/server/src/provider/Drivers/GrokSkills.test.ts +136 −105
  2. apps/server/src/provider/Drivers/GrokSkills.ts +0 −4
apps/server/src/provider/Drivers/GrokSkills.test.ts+136−105
@@ -1,141 +1,172 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3−import * as Layer from "effect/Layer";
43 import * as Sink from "effect/Sink";
54 import * as Stream from "effect/Stream";
65 import { ChildProcessSpawner } from "effect/unstable/process";
76
8−import { discoverGrokSkills, parseGrokInspectSkills } from "./GrokSkills.ts";
7+import { discoverGrokSkills } from "./GrokSkills.ts";
98
109 const inspectPayload = (skills: ReadonlyArray<unknown>) => JSON.stringify({ skills });
1110
12−describe("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− },
11+const 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+
31+describe("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
95−describe("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+0−4
@@ -91,10 +91,6 @@ function decodeGrokInspectSkills(stdout: string): ReadonlyArray<ServerProviderSk · 90 unmodified lines
9191 return [...skillsByName.values()].sort((left, right) => left.name.localeCompare(right.name));
9292 }
9393
94−export 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

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t