fix(plugins): re-export probe symbols + declare plugin deps in dashboard
- Hermes / OpenClaw plugin index.ts now re-export `probeHermesBinary` / `probeOpenClawBinary` and their status types so the dashboard's `runtime-provider-probes.ts` façade can import them via the public package entry instead of deep paths. - Dashboard `package.json` adds `@fusion-plugin-examples/hermes-runtime`, `…/openclaw-runtime`, `…/paperclip-runtime` as workspace deps so pnpm symlinks them into `packages/dashboard/node_modules/`. Without these, the new probe imports failed with "Cannot find module" during `pnpm typecheck`. This clears 6 of the 9 outstanding typecheck errors. The remaining 3 are in the in-flight Hermes plugin rewrite (runtime-adapter still imports from a deleted `./pi-module.js`; the new `index.ts` calls a factory with the wrong arg type) and should be resolved by the same change set that landed the rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,90 +1,136 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { HermesRuntimeAdapter } from "../runtime-adapter.js";
|
||||
const { mockCreateStreamSession, mockStreamPrompt, mockDescribeStreamModel, } = vi.hoisted(() => ({
|
||||
mockCreateStreamSession: vi.fn(),
|
||||
mockStreamPrompt: vi.fn(),
|
||||
mockDescribeStreamModel: vi.fn(),
|
||||
const { mockInvoke } = vi.hoisted(() => ({
|
||||
mockInvoke: vi.fn(),
|
||||
}));
|
||||
vi.mock("../pi-module.js", () => ({
|
||||
createStreamSession: mockCreateStreamSession,
|
||||
streamPrompt: mockStreamPrompt,
|
||||
describeStreamModel: mockDescribeStreamModel,
|
||||
}));
|
||||
describe("HermesRuntimeAdapter", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
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",
|
||||
});
|
||||
it("has stable runtime identity", () => {
|
||||
const adapter = new HermesRuntimeAdapter({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
});
|
||||
describe("HermesRuntimeAdapter — identity", () => {
|
||||
it("has stable id/name", () => {
|
||||
const adapter = new HermesRuntimeAdapter({});
|
||||
expect(adapter.id).toBe("hermes");
|
||||
expect(adapter.name).toBe("Hermes Runtime");
|
||||
});
|
||||
it("createSession passes model config/systemPrompt/callbacks and returns undefined sessionFile", async () => {
|
||||
const adapter = new HermesRuntimeAdapter({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
apiKey: "secret",
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
const session = { messages: [], dispose: vi.fn() };
|
||||
mockCreateStreamSession.mockReturnValue(session);
|
||||
});
|
||||
describe("HermesRuntimeAdapter — createSession", () => {
|
||||
it("returns a session with empty sessionId and undefined sessionFile", async () => {
|
||||
const adapter = new HermesRuntimeAdapter({});
|
||||
const onText = vi.fn();
|
||||
const onThinking = vi.fn();
|
||||
const onToolStart = vi.fn();
|
||||
const onToolEnd = vi.fn();
|
||||
const result = await adapter.createSession({
|
||||
cwd: "/tmp/project",
|
||||
systemPrompt: "You are Hermes",
|
||||
tools: "coding",
|
||||
customTools: [{ name: "ignored" }],
|
||||
sessionManager: { foo: "bar" },
|
||||
skillSelection: { all: true },
|
||||
skills: ["bash"],
|
||||
cwd: "/repo",
|
||||
systemPrompt: "be helpful",
|
||||
onText,
|
||||
onThinking,
|
||||
onToolStart,
|
||||
onToolEnd,
|
||||
});
|
||||
expect(mockCreateStreamSession).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5",
|
||||
apiKey: "secret",
|
||||
thinkingLevel: "high",
|
||||
systemPrompt: "You are Hermes",
|
||||
callbacks: {
|
||||
onText,
|
||||
onThinking,
|
||||
onToolStart,
|
||||
onToolEnd,
|
||||
},
|
||||
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,
|
||||
});
|
||||
expect(result).toEqual({ session, sessionFile: undefined });
|
||||
expect(JSON.stringify(mockCreateStreamSession.mock.calls[0][0])).not.toContain("/tmp/project");
|
||||
expect(JSON.stringify(mockCreateStreamSession.mock.calls[0][0])).not.toContain("coding");
|
||||
await adapter.promptWithFallback(session, "first prompt");
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(1);
|
||||
const [prompt, settings, resumeId] = mockInvoke.mock.calls[0];
|
||||
expect(prompt).toBe("first prompt");
|
||||
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("promptWithFallback appends user message then delegates to streamPrompt", async () => {
|
||||
const adapter = new HermesRuntimeAdapter({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
const session = { messages: [], dispose: vi.fn() };
|
||||
await adapter.promptWithFallback(session, "Hello from Hermes");
|
||||
expect(session.messages).toEqual([{ role: "user", content: "Hello from Hermes" }]);
|
||||
expect(mockStreamPrompt).toHaveBeenCalledWith(session, {
|
||||
role: "user",
|
||||
content: "Hello from Hermes",
|
||||
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 [, , resume2] = mockInvoke.mock.calls[1];
|
||||
expect(resume2).toBe("20260427_120000_abc123");
|
||||
});
|
||||
it("describeModel delegates to describeStreamModel", () => {
|
||||
const adapter = new HermesRuntimeAdapter({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
const session = { messages: [], dispose: vi.fn() };
|
||||
mockDescribeStreamModel.mockReturnValue("anthropic/claude-sonnet-4-5");
|
||||
expect(adapter.describeModel(session)).toBe("anthropic/claude-sonnet-4-5");
|
||||
expect(mockDescribeStreamModel).toHaveBeenCalledWith(session);
|
||||
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("dispose is a no-op when missing and calls dispose when present", async () => {
|
||||
const adapter = new HermesRuntimeAdapter({ provider: "anthropic", modelId: "claude-sonnet-4-5" });
|
||||
const dispose = vi.fn();
|
||||
await expect(adapter.dispose({ messages: [], dispose })).resolves.toBeUndefined();
|
||||
await expect(adapter.dispose({ messages: [] })).resolves.toBeUndefined();
|
||||
expect(dispose).toHaveBeenCalledTimes(1);
|
||||
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
|
||||
Reference in New Issue
Block a user