Adds a new bundled Grok CLI runtime plugin, wiring it end-to-end into settings, auth routes, model discovery, and the dashboard authentication UI. - New `fusion-plugin-grok-runtime` package with CLI spawn, probe, provider, process-manager, and runtime-adapter modules plus tests - Bundled-plugin install list (CLI + core) updated to auto-install the grok-cli plugin - New `useGrokCli`/`grokCliBinaryPath` settings in `settings-schema.ts` and `types.ts` - Dashboard: `GrokCliProviderCard` component/styles, `ProviderIcon` grok entry, `AuthenticationSection` wiring - New `grok-model-cache.ts` for caching `grok models` discovery results, registered model/auth routes for `/auth/grok-cli` and `/providers/grok-cli/status`, merged into `/api/models` - `runtime-provider-probes.ts` extended with Grok CLI probe/model-discovery delegation - Docs updated (`PLUGIN_AUTHORING.md`, `settings-reference.md`) and changeset added (minor, feature) - Workspace config (`pnpm-workspace.yaml`, `pnpm-lock.yaml`) updated to register the new plugin package Files changed: .changeset/fn-7705-grok-cli-runtime.md | 7 + docs/PLUGIN_AUTHORING.md | 2 +- docs/settings-reference.md | 4 + packages/cli/src/plugins/bundled-plugin-install.ts | 8 + .../cli/src/plugins/staged-bundled-plugin-ids.ts | 1 + packages/cli/vitest.config.ts | 12 + .../core/src/__tests__/grok-cli-settings.test.ts | 34 +++ packages/core/src/index.ts | 1 + .../core/src/plugins/bundled-plugin-install.ts | 10 + packages/core/src/settings-schema.ts | 6 + packages/core/src/types.ts | 9 + packages/dashboard/app/api/legacy.ts | 40 ++++ .../app/components/GrokCliProviderCard.css | 65 ++++++ .../app/components/GrokCliProviderCard.tsx | 204 ++++++++++++++++ packages/dashboard/app/components/ProviderIcon.tsx | 5 + .../__tests__/GrokCliProviderCard.test.tsx | 105 +++++++++ .../app/components/__tests__/ProviderIcon.test.tsx | 8 + .../settings/sections/AuthenticationSection.tsx | 8 +- packages/dashboard/package.json | 1 + .../src/__tests__/grok-model-cache.test.ts | 152 ++++++++++++ .../register-model-routes-grok-cli.test.ts | 214 +++++++++++++++++ .../dashboard/src/__tests__/routes-auth.test.ts | 258 ++++++++++++++++++++- packages/dashboard/src/grok-model-cache.ts | 166 +++++++++++++ packages/dashboard/src/routes.ts | 1 + .../dashboard/src/routes/register-auth-routes.ts | 134 ++++++++++- .../dashboard/src/routes/register-model-routes.ts | 51 ++++ packages/dashboard/src/runtime-provider-probes.ts | 43 ++++ packages/dashboard/vitest.config.ts | 12 + packages/desktop/scripts/workspace-tools.ts | 3 +- plugins/fusion-plugin-grok-runtime/CHANGELOG.md | 7 + plugins/fusion-plugin-grok-runtime/README.md | 54 +++++ plugins/fusion-plugin-grok-runtime/manifest.json | 6 + plugins/fusion-plugin-grok-runtime/package.json | 40 ++++ .../src/__tests__/cli-spawn.test.ts | 103 ++++++++ .../src/__tests__/index.test.ts | 12 + .../src/__tests__/probe.test.ts | 135 +++++++++++ .../src/__tests__/process-manager.test.ts | 96 ++++++++ .../src/__tests__/provider.test.ts | 57 +++++ .../src/__tests__/runtime-adapter.test.ts | 21 ++ .../fusion-plugin-grok-runtime/src/cli-spawn.ts | 50 ++++ plugins/fusion-plugin-grok-runtime/src/index.ts | 74 ++++++ plugins/fusion-plugin-grok-runtime/src/probe.ts | 107 +++++++++ .../src/process-manager.ts | 86 +++++++ plugins/fusion-plugin-grok-runtime/src/provider.ts | 25 ++ .../src/runtime-adapter.ts | 25 ++ plugins/fusion-plugin-grok-runtime/src/types.ts | 12 + plugins/fusion-plugin-grok-runtime/tsconfig.json | 10 + .../fusion-plugin-grok-runtime/vitest.config.ts | 22 ++ pnpm-lock.yaml | 25 ++ pnpm-workspace.yaml | 1 + 50 files changed, 2525 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7705 Fusion-Task-Lineage: b8194ea8-c773-4199-a52a-b0e4e7347192 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import { PassThrough } from "node:stream";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("node:child_process", () => ({ spawn: vi.fn() }));
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { runGrokCommand } from "../cli-spawn.js";
|
|
|
|
function mockPlatform(platform: NodeJS.Platform) {
|
|
return vi.spyOn(process, "platform", "get").mockReturnValue(platform);
|
|
}
|
|
|
|
function createMockChild() {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
stdout: PassThrough;
|
|
stderr: PassThrough;
|
|
kill: ReturnType<typeof vi.fn>;
|
|
};
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.kill = vi.fn();
|
|
vi.mocked(spawn).mockReturnValue(child as never);
|
|
return child;
|
|
}
|
|
|
|
describe("runGrokCommand", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("uses the Windows shell so PATH .cmd and .bat Grok shims can run", async () => {
|
|
mockPlatform("win32");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runGrokCommand("grok", ["--version"], 1000);
|
|
|
|
expect(spawn).toHaveBeenCalledWith("grok", ["--version"], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: true,
|
|
});
|
|
|
|
child.stdout.write("grok 1.0.0\n");
|
|
child.stderr.write("diagnostic\n");
|
|
child.emit("close", 0);
|
|
|
|
await expect(resultPromise).resolves.toEqual({
|
|
code: 0,
|
|
stdout: "grok 1.0.0\n",
|
|
stderr: "diagnostic\n",
|
|
});
|
|
});
|
|
|
|
it("keeps non-Windows Grok invocations on direct spawn", async () => {
|
|
mockPlatform("darwin");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runGrokCommand("grok", ["--version"], 1000);
|
|
|
|
expect(spawn).toHaveBeenCalledWith("grok", ["--version"], {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: false,
|
|
});
|
|
|
|
child.emit("close", 0);
|
|
await expect(resultPromise).resolves.toMatchObject({ code: 0 });
|
|
});
|
|
|
|
it("returns spawn errors with diagnostics instead of empty stderr", async () => {
|
|
mockPlatform("win32");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runGrokCommand("grok", ["--version"], 1000);
|
|
child.emit("error", Object.assign(new Error("spawn grok ENOENT"), { code: "ENOENT" }));
|
|
|
|
const result = await resultPromise;
|
|
expect(result.code).toBe(127);
|
|
expect(result.stderr).toContain("spawn error: ENOENT: spawn grok ENOENT");
|
|
});
|
|
|
|
it("kills timed-out Grok commands best-effort and resolves once", async () => {
|
|
vi.useFakeTimers();
|
|
mockPlatform("linux");
|
|
const child = createMockChild();
|
|
|
|
const resultPromise = runGrokCommand("grok", ["models"], 25);
|
|
child.stdout.write("partial");
|
|
|
|
await vi.advanceTimersByTimeAsync(25);
|
|
|
|
await expect(resultPromise).resolves.toEqual({ code: 124, stdout: "partial", stderr: "" });
|
|
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
|
|
|
|
child.emit("close", 0);
|
|
await expect(resultPromise).resolves.toMatchObject({ code: 124 });
|
|
});
|
|
});
|