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:
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
|
||||
Reference in New Issue
Block a user