feat(FN-2477): add executable OpenClaw runtime adapter

- Replace deferred OpenClaw placeholder runtime with an executable adapter that creates sessions, prompts with fallback, describes models, and disposes safely
- Add a dedicated pi-module seam and local runtime contract types to decouple plugin compilation from engine internals while preserving runtime behavior
- Update OpenClaw plugin metadata, manifest/package descriptions, and README to reflect active execution support instead of deferred status
- Harden dashboard DevServerStore save flow against ENOENT temp-directory races and add regression coverage for project-directory removal during save
This commit is contained in:
Fusion
2026-04-24 10:55:28 -07:00
committed by gsxdsm
parent d92dc37f92
commit 403e3a751a
18 changed files with 391 additions and 125 deletions

View File

@@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { OpenClawRuntimeAdapter } from "../runtime-adapter.js";
const { mockCreateFnAgent, mockPromptWithFallback, mockDescribeModel } = vi.hoisted(() => ({
mockCreateFnAgent: vi.fn(),
mockPromptWithFallback: vi.fn(),
mockDescribeModel: vi.fn(),
}));
vi.mock("../pi-module.js", () => ({
createFnAgent: mockCreateFnAgent,
promptWithFallback: mockPromptWithFallback,
describeModel: mockDescribeModel,
}));
describe("OpenClawRuntimeAdapter", () => {
let adapter: OpenClawRuntimeAdapter;
beforeEach(() => {
vi.clearAllMocks();
mockDescribeModel.mockReturnValue("mock/anthropic-claude");
adapter = new OpenClawRuntimeAdapter();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("has stable runtime identity", () => {
expect(adapter.id).toBe("openclaw");
expect(adapter.name).toBe("OpenClaw Runtime");
});
it("delegates createSession to createFnAgent with mapped options", async () => {
const mockSession = { dispose: vi.fn() };
mockCreateFnAgent.mockResolvedValue({ session: mockSession, sessionFile: "/tmp/session.json" });
const result = await adapter.createSession({
cwd: "/project",
systemPrompt: "You are helpful",
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
skills: ["bash"],
});
expect(mockCreateFnAgent).toHaveBeenCalledWith({
cwd: "/project",
systemPrompt: "You are helpful",
tools: undefined,
customTools: undefined,
onText: undefined,
onThinking: undefined,
onToolStart: undefined,
onToolEnd: undefined,
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4o",
defaultThinkingLevel: undefined,
sessionManager: undefined,
skillSelection: undefined,
skills: ["bash"],
});
expect(result.session).toBe(mockSession);
expect(result.sessionFile).toBe("/tmp/session.json");
});
it("delegates promptWithFallback to pi seam", async () => {
const session = { id: "s-1" };
mockPromptWithFallback.mockResolvedValue(undefined);
await adapter.promptWithFallback(session as any, "Hello", { images: [] });
expect(mockPromptWithFallback).toHaveBeenCalledWith(session, "Hello", { images: [] });
});
it("delegates describeModel to pi seam", () => {
const session = { id: "s-2" };
mockDescribeModel.mockReturnValue("anthropic/claude-sonnet-4-5");
const result = adapter.describeModel(session as any);
expect(mockDescribeModel).toHaveBeenCalledWith(session);
expect(result).toBe("anthropic/claude-sonnet-4-5");
});
it("dispose calls session.dispose when present and no-ops otherwise", async () => {
const disposeMock = vi.fn().mockResolvedValue(undefined);
await adapter.dispose({ dispose: disposeMock });
await expect(adapter.dispose({ id: "no-dispose" } as any)).resolves.toBeUndefined();
expect(disposeMock).toHaveBeenCalledTimes(1);
});
});