import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterEach, describe, expect, it, vi } from "vitest" ;
const { runtimeRegistry } = vi.hoisted(() => ({
runtimeRegistry: new Map<string, { runtime: unknown; healthy?: () => boolean }>(),
}));
const { prepareAcpxCodexAuthConfigMock } = vi.hoisted(() => ({
prepareAcpxCodexAuthConfigMock: vi.fn(
async ({ pluginConfig }: { pluginConfig: unknown }) => pluginConfig,
),
}));
vi.mock("../runtime-api.js" , () => ({
getAcpRuntimeBackend: (id: string) => runtimeRegistry.get(id),
registerAcpRuntimeBackend: (entry: { id: string; runtime: unknown; healthy?: () => boolean }) => {
runtimeRegistry.set(entry.id, entry);
},
unregisterAcpRuntimeBackend: (id: string) => {
runtimeRegistry.delete (id);
},
}));
vi.mock("./runtime.js" , () => ({
ACPX_BACKEND_ID: "acpx" ,
AcpxRuntime: function AcpxRuntime() {},
createAgentRegistry: vi.fn(() => ({})),
createFileSessionStore: vi.fn(() => ({})),
}));
vi.mock("./codex-auth-bridge.js" , () => ({
prepareAcpxCodexAuthConfig: prepareAcpxCodexAuthConfigMock,
}));
import { getAcpRuntimeBackend } from "../runtime-api.js" ;
import { createAcpxRuntimeService } from "./service.js" ;
const tempDirs: string[] = [];
async function makeTempDir(): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-acpx-service-" ));
tempDirs.push(dir);
return dir;
}
afterEach(async () => {
runtimeRegistry.clear();
prepareAcpxCodexAuthConfigMock.mockClear();
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME;
delete process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE;
for (const dir of tempDirs.splice(0 )) {
await fs.rm(dir, { recursive: true , force: true });
}
});
function createServiceContext(workspaceDir: string) {
return {
workspaceDir,
stateDir: path.join(workspaceDir, ".openclaw-plugin-state" ),
config: {},
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
};
}
function createMockRuntime(overrides: Record<string, unknown> = {}) {
return {
ensureSession: vi.fn(),
runTurn: vi.fn(),
cancel: vi.fn(),
close: vi.fn(),
probeAvailability: vi.fn(async () => {}),
isHealthy: vi.fn(() => true ),
doctor: vi.fn(async () => ({ ok: true , message: "ok" })),
...overrides,
};
}
describe("createAcpxRuntimeService" , () => {
it("registers and unregisters the embedded backend" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = createMockRuntime();
const service = createAcpxRuntimeService({
runtimeFactory: () => runtime as never,
});
await service.start(ctx);
expect(getAcpRuntimeBackend("acpx" )?.runtime).toBe(runtime);
await service.stop?.(ctx);
expect(getAcpRuntimeBackend("acpx" )).toBeUndefined();
});
it("creates the embedded runtime state directory before probing" , async () => {
const workspaceDir = await makeTempDir();
const stateDir = path.join(workspaceDir, "custom-state" );
const ctx = createServiceContext(workspaceDir);
const probeAvailability = vi.fn(async () => {
await fs.access(stateDir);
});
const runtime = createMockRuntime({
doctor: async () => ({ ok: true , message: "ok" }),
isHealthy: () => true ,
probeAvailability,
});
const service = createAcpxRuntimeService({
pluginConfig: { stateDir },
runtimeFactory: () => runtime as never,
});
await service.start(ctx);
expect(probeAvailability).toHaveBeenCalledOnce();
await service.stop?.(ctx);
});
it("passes the default runtime timeout to the embedded runtime factory" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = createMockRuntime();
const runtimeFactory = vi.fn(() => runtime as never);
const service = createAcpxRuntimeService({
runtimeFactory,
});
await service.start(ctx);
expect(runtimeFactory).toHaveBeenCalledWith(
expect.objectContaining({
pluginConfig: expect.objectContaining({
timeoutSeconds: 120 ,
}),
}),
);
await service.stop?.(ctx);
});
it("forwards a configured probeAgent to the runtime factory so the probe does not hardcode the default" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = {
ensureSession: vi.fn(),
runTurn: vi.fn(),
cancel: vi.fn(),
close: vi.fn(),
probeAvailability: vi.fn(async () => {}),
isHealthy: vi.fn(() => true ),
doctor: vi.fn(async () => ({ ok: true , message: "ok" })),
};
const runtimeFactory = vi.fn(() => runtime as never);
const service = createAcpxRuntimeService({
pluginConfig: { probeAgent: "opencode" },
runtimeFactory,
});
await service.start(ctx);
expect(runtimeFactory).toHaveBeenCalledWith(
expect.objectContaining({
pluginConfig: expect.objectContaining({
probeAgent: "opencode" ,
}),
}),
);
await service.stop?.(ctx);
});
it("uses the first allowed ACP agent as the default probe agent" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
ctx.config = {
acp: {
allowedAgents: [" OpenCode " , "codex" ],
},
};
const runtime = createMockRuntime();
const runtimeFactory = vi.fn(() => runtime as never);
const service = createAcpxRuntimeService({
runtimeFactory,
});
await service.start(ctx);
expect(runtimeFactory).toHaveBeenCalledWith(
expect.objectContaining({
pluginConfig: expect.objectContaining({
probeAgent: "opencode" ,
}),
}),
);
await service.stop?.(ctx);
});
it("keeps explicit probeAgent ahead of acp.allowedAgents" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
ctx.config = {
acp: {
allowedAgents: ["opencode" ],
},
};
const runtime = createMockRuntime();
const runtimeFactory = vi.fn(() => runtime as never);
const service = createAcpxRuntimeService({
pluginConfig: { probeAgent: "codex" },
runtimeFactory,
});
await service.start(ctx);
expect(runtimeFactory).toHaveBeenCalledWith(
expect.objectContaining({
pluginConfig: expect.objectContaining({
probeAgent: "codex" ,
}),
}),
);
await service.stop?.(ctx);
});
it("warns when legacy compatibility config is explicitly ignored" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = createMockRuntime();
const service = createAcpxRuntimeService({
pluginConfig: {
queueOwnerTtlSeconds: 30 ,
strictWindowsCmdWrapper: false ,
},
runtimeFactory: () => runtime as never,
});
await service.start(ctx);
expect(ctx.logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
"embedded acpx runtime ignores legacy compatibility config: queueOwnerTtlSeconds, strictWindowsCmdWrapper=false" ,
),
);
await service.stop?.(ctx);
});
it("can skip the embedded runtime probe via env" , async () => {
process.env.OPENCLAW_SKIP_ACPX_RUNTIME_PROBE = "1" ;
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const probeAvailability = vi.fn(async () => {});
const runtime = createMockRuntime({
doctor: async () => ({ ok: false , message: "nope" }),
isHealthy: () => false ,
probeAvailability,
});
const service = createAcpxRuntimeService({
runtimeFactory: () => runtime as never,
});
await service.start(ctx);
expect(probeAvailability).not.toHaveBeenCalled();
expect(getAcpRuntimeBackend("acpx" )).toBeTruthy();
await service.stop?.(ctx);
});
it("formats non-string doctor details without losing object payloads" , async () => {
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtime = createMockRuntime({
doctor: async () => ({
ok: false ,
message: "probe failed" ,
details: [{ code: "ACP_CLOSED" , agent: "codex" }, new Error("stdin closed" )],
}),
isHealthy: () => false ,
});
const service = createAcpxRuntimeService({
runtimeFactory: () => runtime as never,
});
await service.start(ctx);
await vi.waitFor(() => {
expect(ctx.logger.warn).toHaveBeenCalledWith(
'embedded acpx runtime backend probe failed: probe failed ({"code":"ACP_CLOSED","agent":"codex"}; stdin closed)' ,
);
});
await service.stop?.(ctx);
});
it("can skip the embedded runtime backend via env" , async () => {
process.env.OPENCLAW_SKIP_ACPX_RUNTIME = "1" ;
const workspaceDir = await makeTempDir();
const ctx = createServiceContext(workspaceDir);
const runtimeFactory = vi.fn(() => {
throw new Error("runtime factory should not run when ACPX is skipped" );
});
const service = createAcpxRuntimeService({
runtimeFactory: runtimeFactory as never,
});
await service.start(ctx);
expect(runtimeFactory).not.toHaveBeenCalled();
expect(getAcpRuntimeBackend("acpx" )).toBeUndefined();
expect(ctx.logger.info).toHaveBeenCalledWith(
"skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)" ,
);
});
});
Messung V0.5 in Prozent C=100 H=98 G=98
¤ Dauer der Verarbeitung: 0.3 Sekunden
¤
*© Formatika GbR, Deutschland