Resolved 7 conflicts and reconciled local patches with upstream's evolved API: - packages/core/src/plugin-loader.ts: dropped local createContextFor()/buildContext() in favor of upstream's createRouteContext() which provides the same TaskStore- override capability our patch added. - packages/dashboard/src/routes.ts: removed local manual plugin-route mounter (upstream's createPluginRouter handles this with richer response support); kept registerPluginExemptPath loop so external webhook plugins (telemetry- watcher, Grafana/Sentry) still bypass daemon-token auth. Removed obsolete resolveHeartbeatMonitorFor (replaced by upstream's resolveHeartbeatMonitor). - packages/dashboard/src/routes/register-agent-runtime-routes.ts: ported 4 call sites to upstream's isHeartbeatMonitorForProject + resolveHeartbeatMonitor dual-fallback pattern (multi-project monitor resolution). - packages/cli/package.json: combined upstream's pi-ai 0.73.0 + dockerode bumps with our cross-spawn dep for vendored pi-claude-cli. - pnpm-workspace.yaml: kept telemetry-watcher entry alongside upstream's new plugin entries (droid-runtime, cursor-runtime, agent-browser, whatsapp-chat, roadmap, even-realities-glasses, even-cards, reports). - packages/dashboard/app/components/SetupWizardModal.css: kept z-index:110 fix (wizard stacks above ModelOnboardingModal) and added upstream's overflow-y/overscroll-behavior on .setup-wizard-overlay. - pnpm-lock.yaml: took upstream verbatim; pnpm install regenerated to include cross-spawn. Typechecks: @fusion/core and @fusion/dashboard pass cleanly.
138 lines
5.4 KiB
JavaScript
138 lines
5.4 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { HermesRuntimeAdapter } from "../runtime-adapter.js";
|
|
const { mockInvoke } = vi.hoisted(() => ({
|
|
mockInvoke: vi.fn(),
|
|
}));
|
|
vi.mock("../cli-spawn.js", async () => {
|
|
const actual = await vi.importActual("../cli-spawn.js");
|
|
return {
|
|
...actual,
|
|
invokeHermesCli: mockInvoke,
|
|
};
|
|
});
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockInvoke.mockResolvedValue({
|
|
body: "hello from hermes",
|
|
sessionId: "20260427_120000_abc123",
|
|
});
|
|
});
|
|
describe("HermesRuntimeAdapter — identity", () => {
|
|
it("has stable id/name", () => {
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
expect(adapter.id).toBe("hermes");
|
|
expect(adapter.name).toBe("Hermes Runtime");
|
|
});
|
|
});
|
|
describe("HermesRuntimeAdapter — createSession", () => {
|
|
it("returns a session with empty sessionId and undefined sessionFile", async () => {
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const onText = vi.fn();
|
|
const result = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "be helpful",
|
|
onText,
|
|
});
|
|
expect(result.sessionFile).toBeUndefined();
|
|
expect(result.session.sessionId).toBe("");
|
|
expect(result.session.systemPrompt).toBe("be helpful");
|
|
expect(result.session.callbacks.onText).toBe(onText);
|
|
});
|
|
});
|
|
describe("HermesRuntimeAdapter — promptWithFallback", () => {
|
|
it("invokes hermes CLI with no resume on first call", async () => {
|
|
const adapter = new HermesRuntimeAdapter({ model: "claude-sonnet-4-5" });
|
|
const onText = vi.fn();
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
onText,
|
|
});
|
|
await adapter.promptWithFallback(session, "first prompt");
|
|
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
|
const [prompt, settings, resumeId] = mockInvoke.mock.calls[0];
|
|
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");
|
|
// Session id is captured for next call
|
|
expect(session.sessionId).toBe("20260427_120000_abc123");
|
|
});
|
|
it("passes captured session id as --resume on subsequent calls", async () => {
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
await adapter.promptWithFallback(session, "p1");
|
|
await adapter.promptWithFallback(session, "p2");
|
|
const [prompt2, , resume2] = mockInvoke.mock.calls[1];
|
|
expect(prompt2).toBe("p2");
|
|
expect(resume2).toBe("20260427_120000_abc123");
|
|
});
|
|
it("propagates CLI errors", async () => {
|
|
mockInvoke.mockRejectedValueOnce(new Error("hermes: missing session_id"));
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
await expect(adapter.promptWithFallback(session, "p")).rejects.toThrow(/missing session_id/);
|
|
});
|
|
it("does NOT call onText when body is empty", async () => {
|
|
mockInvoke.mockResolvedValueOnce({
|
|
body: "",
|
|
sessionId: "20260427_120000_def456",
|
|
});
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const onText = vi.fn();
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
onText,
|
|
});
|
|
await adapter.promptWithFallback(session, "p");
|
|
expect(onText).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
describe("HermesRuntimeAdapter — describeModel", () => {
|
|
it("returns hermes/<provider>/<model> when both set", async () => {
|
|
const adapter = new HermesRuntimeAdapter({
|
|
provider: "anthropic",
|
|
model: "claude-sonnet-4-5",
|
|
});
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
expect(adapter.describeModel(session)).toBe("hermes/anthropic/claude-sonnet-4-5");
|
|
});
|
|
it("returns hermes/<model> when only model set", async () => {
|
|
const adapter = new HermesRuntimeAdapter({ model: "MiniMax-M2.7" });
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
expect(adapter.describeModel(session)).toBe("hermes/MiniMax-M2.7");
|
|
});
|
|
it("returns plain 'hermes' when neither set", async () => {
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
expect(adapter.describeModel(session)).toBe("hermes");
|
|
});
|
|
});
|
|
describe("HermesRuntimeAdapter — dispose", () => {
|
|
it("is a no-op", async () => {
|
|
const adapter = new HermesRuntimeAdapter({});
|
|
const { session } = await adapter.createSession({
|
|
cwd: "/repo",
|
|
systemPrompt: "sys",
|
|
});
|
|
await expect(adapter.dispose(session)).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
//# sourceMappingURL=runtime-adapter.test.js.map
|