test(pi-claude-cli): cover async CLI validation probes
Adds vitest coverage for validateCliPresenceAsync and validateCliAuthAsync,
verifying success paths, spawn-error paths, non-zero exit codes, and that
auth failures emit the expected warning. Pairs with the async/memoized
validation refactor in f7df0d4e3 that unblocks the dashboard event loop
on every chat send.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/pi-claude-cli-async-validation.md
Normal file
5
.changeset/pi-claude-cli-async-validation.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Stop blocking the Node event loop on every chat send. The pi-claude-cli extension factory used to run two `execSync` probes (`claude --version`, `claude auth status`) on every `createFnAgent` call, which Fusion invokes per chat message — so each send froze every other dashboard API for a few seconds while the Claude CLI cold-started. Probes now run async via `spawn` and are memoized to once per process.
|
||||||
@@ -47,6 +47,8 @@ import {
|
|||||||
captureStderr,
|
captureStderr,
|
||||||
validateCliPresence,
|
validateCliPresence,
|
||||||
validateCliAuth,
|
validateCliAuth,
|
||||||
|
validateCliPresenceAsync,
|
||||||
|
validateCliAuthAsync,
|
||||||
forceKillProcess,
|
forceKillProcess,
|
||||||
registerProcess,
|
registerProcess,
|
||||||
killAllProcesses,
|
killAllProcesses,
|
||||||
@@ -407,6 +409,94 @@ describe("validateCliAuth", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("validateCliPresenceAsync", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves ok=true when claude --version exits 0", async () => {
|
||||||
|
const EventEmitter = require("node:events");
|
||||||
|
(spawn as any).mockImplementationOnce(() => {
|
||||||
|
const proc = new EventEmitter();
|
||||||
|
proc.kill = vi.fn();
|
||||||
|
setImmediate(() => proc.emit("exit", 0));
|
||||||
|
return proc;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await validateCliPresenceAsync();
|
||||||
|
expect(result).toEqual({ ok: true });
|
||||||
|
const args = (spawn as any).mock.calls[0][1] as string[];
|
||||||
|
expect(args).toEqual(["--version"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves ok=false with install message when spawn errors", async () => {
|
||||||
|
const EventEmitter = require("node:events");
|
||||||
|
(spawn as any).mockImplementationOnce(() => {
|
||||||
|
const proc = new EventEmitter();
|
||||||
|
proc.kill = vi.fn();
|
||||||
|
setImmediate(() => proc.emit("error", new Error("ENOENT")));
|
||||||
|
return proc;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await validateCliPresenceAsync();
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error.message).toContain("Claude Code CLI not found");
|
||||||
|
expect(result.error.message).toContain("npm install");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves ok=false when claude --version exits non-zero", async () => {
|
||||||
|
const EventEmitter = require("node:events");
|
||||||
|
(spawn as any).mockImplementationOnce(() => {
|
||||||
|
const proc = new EventEmitter();
|
||||||
|
proc.kill = vi.fn();
|
||||||
|
setImmediate(() => proc.emit("exit", 1));
|
||||||
|
return proc;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await validateCliPresenceAsync();
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateCliAuthAsync", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves true when claude auth status exits 0", async () => {
|
||||||
|
const EventEmitter = require("node:events");
|
||||||
|
(spawn as any).mockImplementationOnce(() => {
|
||||||
|
const proc = new EventEmitter();
|
||||||
|
proc.kill = vi.fn();
|
||||||
|
setImmediate(() => proc.emit("exit", 0));
|
||||||
|
return proc;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await validateCliAuthAsync()).toBe(true);
|
||||||
|
const args = (spawn as any).mock.calls[0][1] as string[];
|
||||||
|
expect(args).toEqual(["auth", "status"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves false and warns when claude auth status fails", async () => {
|
||||||
|
const EventEmitter = require("node:events");
|
||||||
|
(spawn as any).mockImplementationOnce(() => {
|
||||||
|
const proc = new EventEmitter();
|
||||||
|
proc.kill = vi.fn();
|
||||||
|
setImmediate(() => proc.emit("exit", 1));
|
||||||
|
return proc;
|
||||||
|
});
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
|
||||||
|
expect(await validateCliAuthAsync()).toBe(false);
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("not authenticated"),
|
||||||
|
);
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("CLI flags", () => {
|
describe("CLI flags", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
Reference in New Issue
Block a user