Files
fathah--hermes-desktop/tests/skills-cli-output.test.ts
T
2026-07-13 13:36:53 +08:00

97 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { classifySkillCliOutput } from "../src/main/skills";
/**
* Issue #310: `hermes skills install` exits 0 even when no skill was
* installed (resolution failure, unknown name, etc.) and prints the actual
* failure on stdout. Before this classifier the desktop trusted the 0 exit
* and reported `{success:true}` — leaving the user with a button that
* flashed and did nothing. These cases lock the classifier's behaviour
* against the CLI output captured live on 2026-05-22 (Hermes Agent
* v0.14.0).
*/
describe("classifySkillCliOutput", () => {
it("returns success on clean exit-0 output with no failure markers", () => {
expect(classifySkillCliOutput("Resolving 'demo'...\nInstalled.")).toEqual({
success: true,
});
expect(classifySkillCliOutput("")).toEqual({ success: true });
});
it("detects the ambiguous-short-name failure ('No exact match for ...')", () => {
const out =
"Resolving 'concept-diagram'...\n" +
"No exact match for 'concept-diagram'. Did you mean one of these?\n" +
" concept-diagrams - official/creative/concept-diagrams\n";
const result = classifySkillCliOutput(out);
expect(result.success).toBe(false);
expect(result.error).toContain("No exact match for");
expect(result.error).toContain("Did you mean");
expect(result.error).toContain("concept-diagrams");
// The noisy "Resolving '...'..." progress line is stripped.
expect(result.error).not.toContain("Resolving");
});
it("detects the unknown-name failure ('Error: No skill named ... found')", () => {
const out =
"Resolving 'definitely-not-real'...\n" +
"Error: No skill named 'definitely-not-real' found in any source.\n";
const result = classifySkillCliOutput(out);
expect(result.success).toBe(false);
expect(result.error).toContain("No skill named");
expect(result.error).toContain("definitely-not-real");
});
it("treats a generic 'Error:' line as failure (defensive fallback)", () => {
const result = classifySkillCliOutput("Error: something else went wrong\n");
expect(result.success).toBe(false);
expect(result.error).toContain("something else went wrong");
});
it("classifies failure markers on stderr too", () => {
const result = classifySkillCliOutput(
"",
"Error: stderr-only failure path\n",
);
expect(result.success).toBe(false);
expect(result.error).toContain("stderr-only failure path");
});
it("does not false-flag a clean Resolving progress line as failure", () => {
expect(classifySkillCliOutput("Resolving 'x'...")).toEqual({
success: true,
});
expect(classifySkillCliOutput("Resolving 'x'...\nInstalled x.")).toEqual({
success: true,
});
});
it("does not crash when error has undefined stdout/stderr (#145)", () => {
// When execFileSync throws a system-level error (e.g. binary not found),
// both stdout and stderr can be undefined. The catch block in
// installSkill/uninstallSkill must not call .trim() on undefined.
const buildMsg = (
stderr: string | undefined,
message: string | undefined,
stdout: string | undefined,
): string => {
const msg = (stderr || message || "").trim();
return msg || stdout?.toString()?.trim() || "Install failed.";
};
expect(buildMsg(undefined, undefined, undefined)).toBe("Install failed.");
expect(buildMsg(undefined, "spawn ENOENT", undefined)).toBe("spawn ENOENT");
expect(buildMsg("pip error", undefined, undefined)).toBe("pip error");
expect(buildMsg(undefined, undefined, "some output")).toBe("some output");
});
it("falls back to the raw output if every line gets filtered as noise", () => {
// Pathological input: only the Resolving line is present and it
// contains a failure marker — extractSkillCliMessage's filter would
// otherwise produce an empty string.
const result = classifySkillCliOutput("Error:");
expect(result.success).toBe(false);
expect(result.error).toBe("Error:");
});
});