Files
fusion/plugins/fusion-plugin-hermes-runtime/dist/__tests__/probe.test.js
gsxdsm 72691c6fd6 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>
2026-05-06 20:25:38 -07:00

107 lines
5.3 KiB
JavaScript

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