Files
fusion/plugins/fusion-plugin-omp-runtime/src/__tests__/runtime-adapter.test.ts
gsxdsm b563b12662 feat: add Oh My Pi (omp) ACP runtime plugin (#2083)
## Summary

- Add `fusion-plugin-omp-runtime` so Fusion agents can run through
operator-installed **Oh My Pi (`omp`)** over the [Agent Client
Protocol](https://omp.sh/docs/acp) (`omp acp`).
- Wire staged/bundled install, Settings → Authentication card (enable +
binary path), model discovery (`omp models` → `omp-cli/*`), and MCP
eligibility for runtime id `omp`.
- Forward Fusion `systemPrompt` via ACP `session/new`
`_meta.systemPromptOverride`.

## How operators use it

1. Install/auth `omp` (credentials under `~/.omp`).
2. Enable **Oh My Pi — via omp ACP** in Settings → Authentication
(optional binary path).
3. Set agent **Runtime Source → OMP Runtime** (`runtimeHint: "omp"`), or
pick an `omp-cli/*` model when enabled.

## Known v1 gaps

- No Grok-style Fusion `fn_*` loopback tool bridge yet (operator MCP is
forwarded; in-process custom tools are not).
- Model is fixed at spawn (`omp --model … acp`); no mid-session Fusion
model switch.

## Test plan

- [x] `pnpm --filter @fusion-plugin-examples/omp-runtime test` (unit +
live ACP when `omp` is on PATH)
- [x] Auth routes: `POST /api/auth/omp-cli`, `GET
/api/providers/omp-cli/status`
- [x] Engine `runtimeSupportsMcp("omp")`
- [ ] Manual: enable card in dashboard, select OMP runtime on an agent,
run a short chat turn

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Oh My Pi (OMP) CLI support as an ACP-backed runtime and model
provider, including model discovery and probing.
* Added dashboard auth/status controls to enable OMP, check readiness,
and configure the local binary path (with validation).
* Exposed OMP custom `fn_*` tools via an MCP loopback bridge, plus
optional filesystem capabilities and stricter tool permission gating.
* **Documentation**
* Added/expanded OMP runtime contract and integration docs (including
the ACP session/handshake flow).
* **Tests**
* Added Vitest coverage for settings wiring, provider status, model
discovery, runtime sessions, permissions, MCP bridging, and live
connectivity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-14 08:18:52 -07:00

192 lines
6.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { OmpRuntimeAdapter } from "../runtime-adapter.js";
import type { AgentRuntimeOptions, AgentSession, AgentSessionResult } from "../types.js";
function makeFakeSession(partial?: Partial<AgentSession>): AgentSession {
const messages: unknown[] = [];
return {
model: "omp/default",
messages,
state: { messages },
lastModelDescription: "omp/default",
callbacks: {},
connection: { live: true },
dispose: () => undefined,
...partial,
};
}
describe("OmpRuntimeAdapter", () => {
it("forwards Fusion systemPrompt via sessionMeta.systemPromptOverride", async () => {
let seenOptions: AgentRuntimeOptions | undefined;
const liveSession = makeFakeSession();
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async (options) => {
seenOptions = options;
return { session: liveSession };
},
promptWithFallback: async () => undefined,
describeModel: () => "omp/default",
}),
});
await adapter.createSession({
cwd: process.cwd(),
systemPrompt: "Fusion system context",
});
expect(seenOptions?.sessionMeta).toEqual(
expect.objectContaining({
systemPromptOverride: expect.stringContaining("Fusion system context"),
}),
);
});
it("forwards Fusion fn_* tools via fusion-custom-tools MCP server", async () => {
let seenOptions: AgentRuntimeOptions | undefined;
const liveSession = makeFakeSession();
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async (options) => {
seenOptions = options;
return { session: liveSession };
},
promptWithFallback: async () => undefined,
describeModel: () => "omp/default",
}),
});
const { session } = await adapter.createSession({
cwd: process.cwd(),
systemPrompt: "sys",
customTools: [
{
name: "fn_task_list",
description: "List tasks",
parameters: { type: "object", properties: {} },
execute: async () => ({ text: "ok" }),
},
],
});
try {
const mcp = seenOptions?.mcpServers as Array<{ name?: string }> | undefined;
expect(mcp?.some((s) => s.name === "fusion-custom-tools")).toBe(true);
expect(String(seenOptions?.sessionMeta?.systemPromptOverride ?? "")).toContain(
"fusion-custom-tools",
);
} finally {
await adapter.dispose(session);
}
});
it("surfaces create failures as onText diagnostics without throwing", async () => {
const onText = vi.fn();
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async () => {
throw new Error("spawn ENOENT");
},
promptWithFallback: async () => undefined,
describeModel: () => "omp/default",
}),
});
const { session } = await adapter.createSession({
cwd: process.cwd(),
systemPrompt: "",
onText,
});
expect(onText).toHaveBeenCalled();
expect(String(onText.mock.calls[0]?.[0])).toMatch(/OMP ACP failed to start/);
expect(session.state.errorMessage).toMatch(/OMP ACP failed to start/);
});
it("forwards prompts to the ACP adapter when connection is live", async () => {
const prompt = vi.fn(async () => ({ stopReason: "end_turn" }));
const liveSession = makeFakeSession();
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async (): Promise<AgentSessionResult> => ({
session: liveSession,
}),
promptWithFallback: prompt,
describeModel: () => "omp/default",
}),
});
const { session } = await adapter.createSession({
cwd: process.cwd(),
systemPrompt: "sys",
onText: (t) => {
liveSession.callbacks.onText?.(t);
},
});
// Simulate streamed text during the turn
liveSession.callbacks.onText = (t: string) => {
// turn accum is wired on create; call session path via adapter
void t;
};
await adapter.promptWithFallback(session, "hello");
expect(prompt).toHaveBeenCalledWith(session, "hello", undefined);
});
it("re-surfaces diagnostics when the session has no live connection", async () => {
const onText = vi.fn();
const dead = makeFakeSession({ connection: undefined, callbacks: { onText } });
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async () => ({ session: dead }),
promptWithFallback: async () => undefined,
describeModel: () => "omp/default",
}),
});
// Manually set adapters map by creating then killing connection semantics:
// createSession without connection still stores adapter, but hasConnection checks connection field.
const createOnly = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async () => ({
session: makeFakeSession({
connection: undefined,
callbacks: { onText },
state: { messages: [], errorMessage: "boom" },
}),
}),
promptWithFallback: async () => undefined,
describeModel: () => "omp/default",
}),
});
const { session } = await createOnly.createSession({
cwd: process.cwd(),
systemPrompt: "",
onText,
});
// Force no connection for follow-up
(session as { connection?: unknown }).connection = undefined;
onText.mockClear();
await createOnly.promptWithFallback(session, "again");
expect(onText).toHaveBeenCalled();
expect(String(onText.mock.calls[0]?.[0])).toMatch(/no live connection/);
});
it("describeModel uses lastModelDescription", async () => {
const adapter = new OmpRuntimeAdapter({
createAcpAdapter: () => ({
createSession: async () => ({
session: makeFakeSession({ lastModelDescription: "omp/claude-sonnet-4", model: "claude-sonnet-4" }),
}),
promptWithFallback: async () => undefined,
describeModel: () => "omp/claude-sonnet-4",
}),
});
const { session } = await adapter.createSession({ cwd: process.cwd(), systemPrompt: "" });
expect(adapter.describeModel(session)).toMatch(/^omp\//);
});
});