fix(dashboard): write ChatView visualViewport vars imperatively
The mobile composer/footer slid over the message list when the user swiped with the keyboard up. Cause: --vv-height / --vv-offset-top were routed through React state via useMobileKeyboard, so on iOS — which fires visualViewport scroll/resize on the same frame as its keyboard animation — the .chat-thread translation lagged by one paint, visible as the composer momentarily floating over messages. Now those two vars are written imperatively in a useLayoutEffect directly to the .chat-thread DOM node on every visualViewport event, mirroring the working pattern at QuickChatFAB.tsx:1032-1052 (which already works correctly on mobile). Only --keyboard-overlap (a structural open/close signal, not per-frame) still flows through React state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.d.ts
vendored
Normal file
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=cli-spawn.test.d.ts.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.d.ts.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"cli-spawn.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-spawn.test.ts"],"names":[],"mappings":""}
|
||||
353
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.js
vendored
Normal file
353
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.js
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
// ── Mock node:child_process before imports that use it ─────────────────────
|
||||
const { mockSpawn } = vi.hoisted(() => ({ mockSpawn: vi.fn() }));
|
||||
vi.mock("node:child_process", () => ({ spawn: mockSpawn }));
|
||||
import { buildHermesArgs, invokeHermesCli, listHermesProfiles, parseHermesOutput, resolveCliSettings, } from "../cli-spawn.js";
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
function defaultSettings(overrides = {}) {
|
||||
return {
|
||||
binaryPath: "hermes",
|
||||
maxTurns: 12,
|
||||
yolo: false,
|
||||
cliTimeoutMs: 5_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create a fake ChildProcess-like EventEmitter with controllable
|
||||
* stdout/stderr streams and a kill spy.
|
||||
*/
|
||||
function makeFakeChild() {
|
||||
const main = new EventEmitter();
|
||||
const stdoutEmitter = new EventEmitter();
|
||||
const stderrEmitter = new EventEmitter();
|
||||
main.stdout = stdoutEmitter;
|
||||
main.stderr = stderrEmitter;
|
||||
const kill = vi.fn().mockReturnValue(true);
|
||||
main.kill = kill;
|
||||
return {
|
||||
child: main,
|
||||
emitStdout: (data) => stdoutEmitter.emit("data", Buffer.from(data)),
|
||||
emitStderr: (data) => stderrEmitter.emit("data", Buffer.from(data)),
|
||||
emitError: (err) => main.emit("error", err),
|
||||
emitClose: (code) => main.emit("close", code),
|
||||
kill,
|
||||
};
|
||||
}
|
||||
/** Build a stdout string that hermes would produce for a given body + session id. */
|
||||
function fakeHermesOutput(body, sessionId = "20260427_120000_abcd12") {
|
||||
return `${body}\nsession_id: ${sessionId}\n`;
|
||||
}
|
||||
// ── resolveCliSettings ──────────────────────────────────────────────────────
|
||||
describe("resolveCliSettings", () => {
|
||||
const origEnv = { ...process.env };
|
||||
afterEach(() => {
|
||||
process.env = { ...origEnv };
|
||||
});
|
||||
it("returns defaults when settings and env are empty", () => {
|
||||
delete process.env.HERMES_BIN;
|
||||
delete process.env.HERMES_MODEL_ID;
|
||||
delete process.env.HERMES_PROVIDER;
|
||||
delete process.env.HERMES_MAX_TURNS;
|
||||
delete process.env.HERMES_YOLO;
|
||||
delete process.env.HERMES_CLI_TIMEOUT_MS;
|
||||
expect(resolveCliSettings()).toEqual({
|
||||
binaryPath: "hermes",
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
maxTurns: 12,
|
||||
yolo: false,
|
||||
cliTimeoutMs: 300_000,
|
||||
});
|
||||
});
|
||||
it("prefers settings over env vars", () => {
|
||||
process.env.HERMES_BIN = "/usr/bin/hermes";
|
||||
process.env.HERMES_MODEL_ID = "env-model";
|
||||
process.env.HERMES_PROVIDER = "openrouter";
|
||||
expect(resolveCliSettings({
|
||||
binaryPath: "/custom/hermes",
|
||||
model: "gpt-4o",
|
||||
provider: "openai-codex",
|
||||
maxTurns: 5,
|
||||
yolo: true,
|
||||
cliTimeoutMs: 10_000,
|
||||
})).toEqual({
|
||||
binaryPath: "/custom/hermes",
|
||||
model: "gpt-4o",
|
||||
provider: "openai-codex",
|
||||
maxTurns: 5,
|
||||
yolo: true,
|
||||
cliTimeoutMs: 10_000,
|
||||
});
|
||||
});
|
||||
it("falls back to env vars when settings omit values", () => {
|
||||
process.env.HERMES_BIN = "/env/hermes";
|
||||
process.env.HERMES_MODEL_ID = "env-model";
|
||||
process.env.HERMES_PROVIDER = "gemini";
|
||||
process.env.HERMES_MAX_TURNS = "7";
|
||||
process.env.HERMES_YOLO = "true";
|
||||
process.env.HERMES_CLI_TIMEOUT_MS = "60000";
|
||||
expect(resolveCliSettings({})).toEqual({
|
||||
binaryPath: "/env/hermes",
|
||||
model: "env-model",
|
||||
provider: "gemini",
|
||||
maxTurns: 7,
|
||||
yolo: true,
|
||||
cliTimeoutMs: 60_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
// ── buildHermesArgs ────────────────────────────────────────────────────────
|
||||
describe("buildHermesArgs", () => {
|
||||
it("builds minimal args without resume or optional flags", () => {
|
||||
const args = buildHermesArgs("hello world", defaultSettings());
|
||||
expect(args).toEqual(["chat", "-q", "hello world", "-Q", "--source", "tool", "--max-turns", "12"]);
|
||||
});
|
||||
it("adds --resume when sessionId provided", () => {
|
||||
const args = buildHermesArgs("hello", defaultSettings(), "20260427_120000_abcd12");
|
||||
expect(args).toContain("--resume");
|
||||
expect(args[args.indexOf("--resume") + 1]).toBe("20260427_120000_abcd12");
|
||||
});
|
||||
it("adds -m and --provider when configured", () => {
|
||||
const args = buildHermesArgs("hello", defaultSettings({ model: "claude-sonnet-4-5", provider: "anthropic" }));
|
||||
expect(args).toContain("-m");
|
||||
expect(args[args.indexOf("-m") + 1]).toBe("claude-sonnet-4-5");
|
||||
expect(args).toContain("--provider");
|
||||
expect(args[args.indexOf("--provider") + 1]).toBe("anthropic");
|
||||
});
|
||||
it("adds --yolo when yolo is true", () => {
|
||||
const args = buildHermesArgs("hello", defaultSettings({ yolo: true }));
|
||||
expect(args).toContain("--yolo");
|
||||
});
|
||||
it("does not add --yolo when yolo is false", () => {
|
||||
const args = buildHermesArgs("hello", defaultSettings({ yolo: false }));
|
||||
expect(args).not.toContain("--yolo");
|
||||
});
|
||||
it("uses configured maxTurns", () => {
|
||||
const args = buildHermesArgs("hello", defaultSettings({ maxTurns: 5 }));
|
||||
expect(args[args.indexOf("--max-turns") + 1]).toBe("5");
|
||||
});
|
||||
it("first call has no --resume arg", () => {
|
||||
const args = buildHermesArgs("hi", defaultSettings());
|
||||
expect(args).not.toContain("--resume");
|
||||
});
|
||||
});
|
||||
// ── parseHermesOutput ──────────────────────────────────────────────────────
|
||||
describe("parseHermesOutput", () => {
|
||||
it("extracts session_id and body from clean output", () => {
|
||||
const stdout = "This is the response\nsession_id: 20260427_120000_abcd12\n";
|
||||
const result = parseHermesOutput(stdout, "");
|
||||
expect(result.sessionId).toBe("20260427_120000_abcd12");
|
||||
expect(result.body).toBe("This is the response");
|
||||
});
|
||||
it("strips ANSI escape codes from body", () => {
|
||||
const stdout = "\x1b[32mGreen text\x1b[0m\nsession_id: 20260427_120000_abcd12\n";
|
||||
const result = parseHermesOutput(stdout, "");
|
||||
expect(result.body).toBe("Green text");
|
||||
expect(result.body).not.toContain("\x1b");
|
||||
});
|
||||
it("strips preamble chrome lines", () => {
|
||||
const stdout = [
|
||||
"╭─ Hermes ─╮",
|
||||
"↻ Resumed session foo",
|
||||
" ┊ preparing memory…",
|
||||
"Query: what is 2+2?",
|
||||
"4",
|
||||
"╰─────────╯",
|
||||
"session_id: 20260427_120000_abcd12",
|
||||
].join("\n") + "\n";
|
||||
const result = parseHermesOutput(stdout, "");
|
||||
expect(result.body).toBe("4");
|
||||
expect(result.body).not.toContain("Hermes");
|
||||
expect(result.body).not.toContain("Resumed");
|
||||
expect(result.body).not.toContain("preparing");
|
||||
expect(result.body).not.toContain("Query:");
|
||||
expect(result.body).not.toContain("╭");
|
||||
expect(result.body).not.toContain("╰");
|
||||
});
|
||||
it("normalizes CRLF to LF", () => {
|
||||
const stdout = "line1\r\nline2\r\nsession_id: 20260427_120000_abcd12\r\n";
|
||||
const result = parseHermesOutput(stdout, "");
|
||||
expect(result.body).toBe("line1\nline2");
|
||||
});
|
||||
it("throws when session_id line is missing", () => {
|
||||
expect(() => parseHermesOutput("some output without id", "stderr text")).toThrow(/missing session_id/);
|
||||
});
|
||||
});
|
||||
// ── invokeHermesCli ────────────────────────────────────────────────────────
|
||||
describe("invokeHermesCli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("first call passes correct args and no --resume", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const PROMPT = "what is typescript?";
|
||||
const promise = invokeHermesCli(PROMPT, defaultSettings());
|
||||
emitStdout(fakeHermesOutput("TypeScript is a language."));
|
||||
emitClose(0);
|
||||
const result = await promise;
|
||||
expect(mockSpawn).toHaveBeenCalledOnce();
|
||||
const [bin, args] = mockSpawn.mock.calls[0];
|
||||
expect(bin).toBe("hermes");
|
||||
expect(args).toEqual(["chat", "-q", PROMPT, "-Q", "--source", "tool", "--max-turns", "12"]);
|
||||
expect(result.body).toBe("TypeScript is a language.");
|
||||
expect(result.sessionId).toBe("20260427_120000_abcd12");
|
||||
});
|
||||
it("subsequent call with sessionId passes --resume", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = invokeHermesCli("hello again", defaultSettings(), "20260427_120000_abcd12");
|
||||
emitStdout(fakeHermesOutput("Hi there!"));
|
||||
emitClose(0);
|
||||
await promise;
|
||||
const args = mockSpawn.mock.calls[0][1];
|
||||
expect(args).toContain("--resume");
|
||||
expect(args[args.indexOf("--resume") + 1]).toBe("20260427_120000_abcd12");
|
||||
});
|
||||
it("captures session id from stdout", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = invokeHermesCli("hi", defaultSettings());
|
||||
emitStdout("Hello!\nsession_id: 20260427_120000_abcd12\n");
|
||||
emitClose(0);
|
||||
const result = await promise;
|
||||
expect(result.sessionId).toBe("20260427_120000_abcd12");
|
||||
});
|
||||
it("rejects on non-zero exit code and surfaces stderr", async () => {
|
||||
const { child, emitStdout, emitStderr, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = invokeHermesCli("hi", defaultSettings());
|
||||
emitStdout("partial output");
|
||||
emitStderr("fatal error from hermes");
|
||||
emitClose(1);
|
||||
await expect(promise).rejects.toThrow(/exited with code 1/);
|
||||
});
|
||||
it("rejects when session_id is missing from stdout on exit 0", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = invokeHermesCli("hi", defaultSettings());
|
||||
emitStdout("Some output without session id line");
|
||||
emitClose(0);
|
||||
await expect(promise).rejects.toThrow(/missing session_id/);
|
||||
});
|
||||
it("includes -m, --provider, --max-turns, --yolo when settings configured", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const settings = defaultSettings({
|
||||
model: "claude-sonnet-4-5",
|
||||
provider: "anthropic",
|
||||
maxTurns: 20,
|
||||
yolo: true,
|
||||
});
|
||||
const promise = invokeHermesCli("test", settings);
|
||||
emitStdout(fakeHermesOutput("ok"));
|
||||
emitClose(0);
|
||||
await promise;
|
||||
const args = mockSpawn.mock.calls[0][1];
|
||||
expect(args).toContain("-m");
|
||||
expect(args[args.indexOf("-m") + 1]).toBe("claude-sonnet-4-5");
|
||||
expect(args).toContain("--provider");
|
||||
expect(args[args.indexOf("--provider") + 1]).toBe("anthropic");
|
||||
expect(args[args.indexOf("--max-turns") + 1]).toBe("20");
|
||||
expect(args).toContain("--yolo");
|
||||
});
|
||||
it("rejects on ENOENT with not-found message", async () => {
|
||||
const { child, emitError } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = invokeHermesCli("hi", defaultSettings());
|
||||
const err = Object.assign(new Error("not found"), { code: "ENOENT" });
|
||||
emitError(err);
|
||||
await expect(promise).rejects.toThrow(/binary not found/);
|
||||
});
|
||||
it("AbortSignal triggers child.kill and rejects", async () => {
|
||||
const { child, kill, emitStdout } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const ac = new AbortController();
|
||||
const promise = invokeHermesCli("hi", defaultSettings(), undefined, ac.signal);
|
||||
// Abort before any output arrives.
|
||||
ac.abort();
|
||||
emitStdout("irrelevant");
|
||||
await expect(promise).rejects.toThrow(/aborted/);
|
||||
expect(kill).toHaveBeenCalledWith("SIGTERM");
|
||||
});
|
||||
});
|
||||
// ── listHermesProfiles ─────────────────────────────────────────────────────
|
||||
describe("listHermesProfiles", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
const SAMPLE_OUTPUT = [
|
||||
" Profile Model Gateway Alias",
|
||||
" ─────────────── ───────────────────────── ─────────── ────────────",
|
||||
" ◆default MiniMax-M2.7 stopped —",
|
||||
].join("\n") + "\n";
|
||||
it("happy path: parses single default profile", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = listHermesProfiles();
|
||||
emitStdout(SAMPLE_OUTPUT);
|
||||
emitClose(0);
|
||||
const profiles = await promise;
|
||||
expect(profiles).toHaveLength(1);
|
||||
expect(profiles[0]).toMatchObject({
|
||||
name: "default",
|
||||
model: "MiniMax-M2.7",
|
||||
gateway: "stopped",
|
||||
alias: undefined,
|
||||
isDefault: true,
|
||||
});
|
||||
});
|
||||
it("happy path: multi-profile ordered correctly", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const multiOutput = [
|
||||
" Profile Model Gateway Alias",
|
||||
" ─────────────── ───────────────────────── ─────────── ────────────",
|
||||
" ◆default MiniMax-M2.7 stopped —",
|
||||
" work claude-sonnet-4-5 running work-hermes",
|
||||
].join("\n") + "\n";
|
||||
const promise = listHermesProfiles();
|
||||
emitStdout(multiOutput);
|
||||
emitClose(0);
|
||||
const profiles = await promise;
|
||||
expect(profiles).toHaveLength(2);
|
||||
expect(profiles[0].name).toBe("default");
|
||||
expect(profiles[0].isDefault).toBe(true);
|
||||
expect(profiles[1].name).toBe("work");
|
||||
expect(profiles[1].isDefault).toBe(false);
|
||||
expect(profiles[1].model).toBe("claude-sonnet-4-5");
|
||||
expect(profiles[1].alias).toBe("work-hermes");
|
||||
});
|
||||
it("ENOENT rejects with binary-not-found error", async () => {
|
||||
const { child, emitError } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = listHermesProfiles({ binaryPath: "/no/such/hermes" });
|
||||
const err = Object.assign(new Error("not found"), { code: "ENOENT" });
|
||||
emitError(err);
|
||||
await expect(promise).rejects.toThrow(/hermes profile list failed.*binary not found/);
|
||||
});
|
||||
it("non-zero exit rejects with exit code error", async () => {
|
||||
const { child, emitStdout, emitStderr, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = listHermesProfiles();
|
||||
emitStdout("");
|
||||
emitStderr("unknown subcommand");
|
||||
emitClose(2);
|
||||
await expect(promise).rejects.toThrow(/hermes profile list failed.*exited with code 2/);
|
||||
});
|
||||
it("spawns with 'profile list' args", async () => {
|
||||
const { child, emitStdout, emitClose } = makeFakeChild();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
const promise = listHermesProfiles({ binaryPath: "/custom/hermes" });
|
||||
emitStdout(SAMPLE_OUTPUT);
|
||||
emitClose(0);
|
||||
await promise;
|
||||
expect(mockSpawn).toHaveBeenCalledOnce();
|
||||
const [bin, args] = mockSpawn.mock.calls[0];
|
||||
expect(bin).toBe("/custom/hermes");
|
||||
expect(args).toEqual(["profile", "list"]);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=cli-spawn.test.js.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.js.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/cli-spawn.test.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.d.ts
vendored
Normal file
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=fusion-skill-install.test.d.ts.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.d.ts.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"fusion-skill-install.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fusion-skill-install.test.ts"],"names":[],"mappings":""}
|
||||
93
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.js
vendored
Normal file
93
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.js
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getFusionSkillSourceCandidates, installFusionSkillIntoHermesHome, resolveBundledFusionSkillSourceFromCandidates, } from "../fusion-skill-install.js";
|
||||
const tempDirs = [];
|
||||
function tempDir(prefix) {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
function makeSkillSource(dir) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(path.join(dir, "SKILL.md"), "# Fusion Skill\n");
|
||||
}
|
||||
afterEach(() => {
|
||||
delete process.env.HERMES_HOME;
|
||||
for (const dir of tempDirs.splice(0))
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
describe("fusion skill installer", () => {
|
||||
it("resolves workspace and packaged candidate layouts", () => {
|
||||
const workspaceCandidates = getFusionSkillSourceCandidates("file:///repo/plugins/fusion-plugin-hermes-runtime/src/index.ts");
|
||||
expect(workspaceCandidates[0].replace(/\\/g, "/")).toMatch(/\/packages\/cli\/skill\/fusion$/);
|
||||
const packagedCandidates = getFusionSkillSourceCandidates("file:///repo/packages/cli/dist/plugins/fusion-plugin-hermes-runtime/bundled.js");
|
||||
expect(packagedCandidates.some((candidate) => candidate.replace(/\\/g, "/").endsWith("/packages/cli/dist/skill/fusion"))).toBe(true);
|
||||
});
|
||||
it("installs skill for configured Hermes home", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
process.env.HERMES_HOME = home;
|
||||
const result = installFusionSkillIntoHermesHome({ sourceDir: source });
|
||||
expect(result.outcome).toBe("installed");
|
||||
expect(result.targetDir).toBe(path.join(home, "skills", "fusion"));
|
||||
});
|
||||
it("no-ops when already installed to same source", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
process.env.HERMES_HOME = home;
|
||||
expect(installFusionSkillIntoHermesHome({ sourceDir: source }).outcome).toBe("installed");
|
||||
expect(installFusionSkillIntoHermesHome({ sourceDir: source }).outcome).toBe("already-installed");
|
||||
});
|
||||
it("replaces prior fusion directory install safely", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
process.env.HERMES_HOME = home;
|
||||
const target = path.join(home, "skills", "fusion");
|
||||
makeSkillSource(target);
|
||||
writeFileSync(path.join(target, "old.txt"), "stale");
|
||||
const result = installFusionSkillIntoHermesHome({ sourceDir: source });
|
||||
expect(result.outcome).toBe("replaced");
|
||||
});
|
||||
it("skips replacement for unrelated existing directory", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
process.env.HERMES_HOME = home;
|
||||
const target = path.join(home, "skills", "fusion");
|
||||
mkdirSync(target, { recursive: true });
|
||||
writeFileSync(path.join(target, "README.md"), "my custom skill");
|
||||
const result = installFusionSkillIntoHermesHome({ sourceDir: source });
|
||||
expect(result.outcome).toBe("skipped");
|
||||
});
|
||||
it("warns when source is missing", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
process.env.HERMES_HOME = home;
|
||||
const result = installFusionSkillIntoHermesHome({ sourceDir: null });
|
||||
expect(result.outcome).toBe("warning");
|
||||
});
|
||||
it("can resolve first existing candidate", () => {
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
const resolved = resolveBundledFusionSkillSourceFromCandidates(["/missing", source]);
|
||||
expect(resolved).toBe(source);
|
||||
});
|
||||
it("replaces stale fusion symlink", () => {
|
||||
const home = tempDir("hermes-home-");
|
||||
const source = path.join(tempDir("fusion-source-"), "fusion");
|
||||
const oldSource = path.join(tempDir("fusion-old-source-"), "fusion");
|
||||
makeSkillSource(source);
|
||||
makeSkillSource(oldSource);
|
||||
process.env.HERMES_HOME = home;
|
||||
const target = path.join(home, "skills", "fusion");
|
||||
mkdirSync(path.dirname(target), { recursive: true });
|
||||
symlinkSync(oldSource, target, "dir");
|
||||
const result = installFusionSkillIntoHermesHome({ sourceDir: source });
|
||||
expect(result.outcome).toBe("replaced");
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=fusion-skill-install.test.js.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.js.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/fusion-skill-install.test.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const { mockResolveCli } = vi.hoisted(() => ({
|
||||
const { mockResolveCli, mockInstallFusionSkill } = vi.hoisted(() => ({
|
||||
mockResolveCli: vi.fn().mockReturnValue({
|
||||
binaryPath: "hermes",
|
||||
model: undefined,
|
||||
@@ -8,6 +8,11 @@ const { mockResolveCli } = vi.hoisted(() => ({
|
||||
yolo: false,
|
||||
cliTimeoutMs: 300_000,
|
||||
}),
|
||||
mockInstallFusionSkill: vi.fn().mockReturnValue({
|
||||
outcome: "installed",
|
||||
sourceDir: "/tmp/source",
|
||||
targetDir: "/tmp/target",
|
||||
}),
|
||||
}));
|
||||
vi.mock("../cli-spawn.js", async () => {
|
||||
const actual = await vi.importActual("../cli-spawn.js");
|
||||
@@ -16,7 +21,14 @@ vi.mock("../cli-spawn.js", async () => {
|
||||
resolveCliSettings: mockResolveCli,
|
||||
};
|
||||
});
|
||||
import plugin, { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID, } from "../index.js";
|
||||
vi.mock("../fusion-skill-install.js", async () => {
|
||||
const actual = await vi.importActual("../fusion-skill-install.js");
|
||||
return {
|
||||
...actual,
|
||||
installFusionSkillIntoHermesHome: mockInstallFusionSkill,
|
||||
};
|
||||
});
|
||||
import plugin, { HERMES_RUNTIME_ID, hermesRuntimeFactory, hermesRuntimeMetadata } from "../index.js";
|
||||
import { HermesRuntimeAdapter } from "../runtime-adapter.js";
|
||||
function createMockContext(settings = {}) {
|
||||
return {
|
||||
@@ -37,6 +49,12 @@ describe("hermes-runtime plugin", () => {
|
||||
maxTurns: 12,
|
||||
yolo: false,
|
||||
cliTimeoutMs: 300_000,
|
||||
profile: undefined,
|
||||
});
|
||||
mockInstallFusionSkill.mockReturnValue({
|
||||
outcome: "installed",
|
||||
sourceDir: "/tmp/source",
|
||||
targetDir: "/tmp/target",
|
||||
});
|
||||
});
|
||||
it("has expected manifest identity", () => {
|
||||
@@ -59,11 +77,28 @@ describe("hermes-runtime plugin", () => {
|
||||
maxTurns: 12,
|
||||
yolo: false,
|
||||
cliTimeoutMs: 300_000,
|
||||
profile: undefined,
|
||||
});
|
||||
const ctx = createMockContext({ binaryPath: "/opt/homebrew/bin/hermes" });
|
||||
await plugin.hooks.onLoad(ctx);
|
||||
expect(mockResolveCli).toHaveBeenCalledWith(ctx.settings);
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringContaining("/opt/homebrew/bin/hermes"));
|
||||
expect(mockInstallFusionSkill).toHaveBeenCalledWith({ profile: undefined });
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringContaining("fusionSkill=installed"));
|
||||
expect(ctx.emitEvent).toHaveBeenCalledWith("hermes-runtime:loaded", {
|
||||
runtimeId: "hermes",
|
||||
version: plugin.manifest.version,
|
||||
});
|
||||
});
|
||||
it("onLoad warns but continues when skill install warns", async () => {
|
||||
mockInstallFusionSkill.mockReturnValue({
|
||||
outcome: "warning",
|
||||
sourceDir: null,
|
||||
targetDir: "/tmp/target",
|
||||
reason: "missing skill source",
|
||||
});
|
||||
const ctx = createMockContext();
|
||||
await plugin.hooks.onLoad(ctx);
|
||||
expect(ctx.logger.warn).toHaveBeenCalledWith(expect.stringContaining("auto-install warning"));
|
||||
expect(ctx.emitEvent).toHaveBeenCalledWith("hermes-runtime:loaded", {
|
||||
runtimeId: "hermes",
|
||||
version: plugin.manifest.version,
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"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"}
|
||||
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.d.ts
vendored
Normal file
2
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=probe.test.d.ts.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.d.ts.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"probe.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/probe.test.ts"],"names":[],"mappings":""}
|
||||
107
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.js
vendored
Normal file
107
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.js
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
// ── Mock node:child_process ────────────────────────────────────────────────
|
||||
const { mockSpawn } = vi.hoisted(() => ({ mockSpawn: vi.fn() }));
|
||||
vi.mock("node:child_process", () => ({ spawn: mockSpawn }));
|
||||
import { probeHermesBinary } from "../probe.js";
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
/** Yields to the microtask and I/O queue so awaited code can continue. */
|
||||
function flushAsync() {
|
||||
return new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
function makeFakeChild() {
|
||||
const main = new EventEmitter();
|
||||
const stdoutEmitter = new EventEmitter();
|
||||
const stderrEmitter = new EventEmitter();
|
||||
main.stdout = stdoutEmitter;
|
||||
main.stderr = stderrEmitter;
|
||||
main.kill = vi.fn();
|
||||
return {
|
||||
child: main,
|
||||
emitStdout: (data) => stdoutEmitter.emit("data", Buffer.from(data)),
|
||||
emitStderr: (data) => stderrEmitter.emit("data", Buffer.from(data)),
|
||||
emitError: (err) => main.emit("error", err),
|
||||
emitClose: (code) => main.emit("close", code),
|
||||
};
|
||||
}
|
||||
// ── Tests ──────────────────────────────────────────────────────────────────
|
||||
describe("probeHermesBinary", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it("returns available: false with not-found reason on ENOENT", async () => {
|
||||
// probeHermesBinary first awaits tryResolveBinaryPath (which/where spawn),
|
||||
// then spawns the --version process. We must drive them sequentially.
|
||||
const whichChild = makeFakeChild();
|
||||
const versionChild = makeFakeChild();
|
||||
mockSpawn
|
||||
.mockReturnValueOnce(whichChild.child) // which hermes
|
||||
.mockReturnValueOnce(versionChild.child); // hermes --version
|
||||
const promise = probeHermesBinary({ timeoutMs: 500 });
|
||||
// Settle the `which` call — which causes tryResolveBinaryPath to resolve.
|
||||
whichChild.emitClose(1);
|
||||
// Yield so the awaited tryResolveBinaryPath continuation runs and spawns
|
||||
// the version child before we emit its error.
|
||||
await flushAsync();
|
||||
const enoent = Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
||||
versionChild.emitError(enoent);
|
||||
const result = await promise;
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.reason).toMatch(/not found on PATH/);
|
||||
expect(result.probeDurationMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
it("returns available: true with parsed version on success", async () => {
|
||||
const whichChild = makeFakeChild();
|
||||
const versionChild = makeFakeChild();
|
||||
mockSpawn
|
||||
.mockReturnValueOnce(whichChild.child)
|
||||
.mockReturnValueOnce(versionChild.child);
|
||||
const promise = probeHermesBinary({ binaryPath: "hermes", timeoutMs: 500 });
|
||||
whichChild.emitStdout("/usr/local/bin/hermes\n");
|
||||
whichChild.emitClose(0);
|
||||
await flushAsync();
|
||||
versionChild.emitStdout("Hermes Agent v1.2.3\n");
|
||||
versionChild.emitClose(0);
|
||||
const result = await promise;
|
||||
expect(result.available).toBe(true);
|
||||
expect(result.version).toBe("Hermes Agent v1.2.3");
|
||||
expect(result.binaryPath).toBe("/usr/local/bin/hermes");
|
||||
expect(result.reason).toBeUndefined();
|
||||
expect(result.probeDurationMs).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
it("returns available: false when version exits non-zero", async () => {
|
||||
const whichChild = makeFakeChild();
|
||||
const versionChild = makeFakeChild();
|
||||
mockSpawn
|
||||
.mockReturnValueOnce(whichChild.child)
|
||||
.mockReturnValueOnce(versionChild.child);
|
||||
const promise = probeHermesBinary({ timeoutMs: 500 });
|
||||
whichChild.emitClose(1);
|
||||
await flushAsync();
|
||||
versionChild.emitStderr("error: something went wrong");
|
||||
versionChild.emitClose(2);
|
||||
const result = await promise;
|
||||
expect(result.available).toBe(false);
|
||||
expect(result.reason).toContain("error: something went wrong");
|
||||
});
|
||||
it("uses custom binaryPath when provided", async () => {
|
||||
const whichChild = makeFakeChild();
|
||||
const versionChild = makeFakeChild();
|
||||
mockSpawn
|
||||
.mockReturnValueOnce(whichChild.child)
|
||||
.mockReturnValueOnce(versionChild.child);
|
||||
const promise = probeHermesBinary({ binaryPath: "/opt/bin/hermes", timeoutMs: 500 });
|
||||
whichChild.emitClose(1);
|
||||
await flushAsync();
|
||||
versionChild.emitStdout("Hermes Agent v2.0.0\n");
|
||||
versionChild.emitClose(0);
|
||||
const result = await promise;
|
||||
expect(result.available).toBe(true);
|
||||
expect(result.version).toBe("Hermes Agent v2.0.0");
|
||||
// Verify the version spawn used our custom path.
|
||||
const versionSpawnArgs = mockSpawn.mock.calls[1];
|
||||
expect(versionSpawnArgs[0]).toBe("/opt/bin/hermes");
|
||||
expect(versionSpawnArgs[1]).toEqual(["--version"]);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=probe.test.js.map
|
||||
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.js.map
vendored
Normal file
1
plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"probe.test.js","sourceRoot":"","sources":["../../src/__tests__/probe.test.ts"],"names":[],"mappings":"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"}
|
||||
@@ -51,7 +51,8 @@ describe("HermesRuntimeAdapter — promptWithFallback", () => {
|
||||
await adapter.promptWithFallback(session, "first prompt");
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
||||
const [prompt, settings, resumeId] = mockInvoke.mock.calls[0];
|
||||
expect(prompt).toBe("first prompt");
|
||||
expect(prompt).toContain("User request:\nfirst prompt");
|
||||
expect(prompt).toContain("Fusion runtime context:");
|
||||
expect(settings.model).toBe("claude-sonnet-4-5");
|
||||
expect(resumeId).toBeUndefined();
|
||||
expect(onText).toHaveBeenCalledWith("hello from hermes");
|
||||
@@ -66,7 +67,8 @@ describe("HermesRuntimeAdapter — promptWithFallback", () => {
|
||||
});
|
||||
await adapter.promptWithFallback(session, "p1");
|
||||
await adapter.promptWithFallback(session, "p2");
|
||||
const [, , resume2] = mockInvoke.mock.calls[1];
|
||||
const [prompt2, , resume2] = mockInvoke.mock.calls[1];
|
||||
expect(prompt2).toBe("p2");
|
||||
expect(resume2).toBe("20260427_120000_abc123");
|
||||
});
|
||||
it("propagates CLI errors", async () => {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user