mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { AcpRuntimeError } from "../../../src/acp/runtime/errors.js";
|
|
import {
|
|
__testing,
|
|
getAcpRuntimeBackend,
|
|
requireAcpRuntimeBackend,
|
|
} from "../../../src/acp/runtime/registry.js";
|
|
import { ACPX_BUNDLED_BIN, ACPX_PINNED_VERSION } from "./config.js";
|
|
import { createAcpxRuntimeService } from "./service.js";
|
|
|
|
const { ensureAcpxSpy } = vi.hoisted(() => ({
|
|
ensureAcpxSpy: vi.fn(async () => {}),
|
|
}));
|
|
|
|
vi.mock("./ensure.js", () => ({
|
|
ensureAcpx: ensureAcpxSpy,
|
|
}));
|
|
|
|
type RuntimeStub = AcpRuntime & {
|
|
probeAvailability(): Promise<void>;
|
|
isHealthy(): boolean;
|
|
};
|
|
|
|
function createRuntimeStub(healthy: boolean): {
|
|
runtime: RuntimeStub;
|
|
probeAvailabilitySpy: ReturnType<typeof vi.fn>;
|
|
isHealthySpy: ReturnType<typeof vi.fn>;
|
|
} {
|
|
const probeAvailabilitySpy = vi.fn(async () => {});
|
|
const isHealthySpy = vi.fn(() => healthy);
|
|
return {
|
|
runtime: {
|
|
ensureSession: vi.fn(async (input) => ({
|
|
sessionKey: input.sessionKey,
|
|
backend: "acpx",
|
|
runtimeSessionName: input.sessionKey,
|
|
})),
|
|
runTurn: vi.fn(async function* () {
|
|
yield { type: "done" as const };
|
|
}),
|
|
cancel: vi.fn(async () => {}),
|
|
close: vi.fn(async () => {}),
|
|
async probeAvailability() {
|
|
await probeAvailabilitySpy();
|
|
},
|
|
isHealthy() {
|
|
return isHealthySpy();
|
|
},
|
|
},
|
|
probeAvailabilitySpy,
|
|
isHealthySpy,
|
|
};
|
|
}
|
|
|
|
function createServiceContext(
|
|
overrides: Partial<OpenClawPluginServiceContext> = {},
|
|
): OpenClawPluginServiceContext {
|
|
return {
|
|
config: {},
|
|
workspaceDir: "/tmp/workspace",
|
|
stateDir: "/tmp/state",
|
|
logger: {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("createAcpxRuntimeService", () => {
|
|
beforeEach(() => {
|
|
__testing.resetAcpRuntimeBackendsForTests();
|
|
ensureAcpxSpy.mockReset();
|
|
ensureAcpxSpy.mockImplementation(async () => {});
|
|
});
|
|
|
|
it("registers and unregisters the acpx backend", async () => {
|
|
const { runtime, probeAvailabilitySpy } = createRuntimeStub(true);
|
|
const service = createAcpxRuntimeService({
|
|
runtimeFactory: () => runtime,
|
|
});
|
|
const context = createServiceContext();
|
|
|
|
await service.start(context);
|
|
expect(getAcpRuntimeBackend("acpx")?.runtime).toBe(runtime);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(ensureAcpxSpy).toHaveBeenCalledOnce();
|
|
expect(probeAvailabilitySpy).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
await service.stop?.(context);
|
|
expect(getAcpRuntimeBackend("acpx")).toBeNull();
|
|
});
|
|
|
|
it("marks backend unavailable when runtime health check fails", async () => {
|
|
const { runtime } = createRuntimeStub(false);
|
|
const service = createAcpxRuntimeService({
|
|
runtimeFactory: () => runtime,
|
|
});
|
|
const context = createServiceContext();
|
|
|
|
await service.start(context);
|
|
|
|
expect(() => requireAcpRuntimeBackend("acpx")).toThrowError(AcpRuntimeError);
|
|
try {
|
|
requireAcpRuntimeBackend("acpx");
|
|
throw new Error("expected ACP backend lookup to fail");
|
|
} catch (error) {
|
|
expect((error as AcpRuntimeError).code).toBe("ACP_BACKEND_UNAVAILABLE");
|
|
}
|
|
});
|
|
|
|
it("passes queue-owner TTL from plugin config", async () => {
|
|
const { runtime } = createRuntimeStub(true);
|
|
const runtimeFactory = vi.fn(() => runtime);
|
|
const service = createAcpxRuntimeService({
|
|
runtimeFactory,
|
|
pluginConfig: {
|
|
queueOwnerTtlSeconds: 0.25,
|
|
},
|
|
});
|
|
const context = createServiceContext();
|
|
|
|
await service.start(context);
|
|
|
|
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queueOwnerTtlSeconds: 0.25,
|
|
pluginConfig: expect.objectContaining({
|
|
command: ACPX_BUNDLED_BIN,
|
|
expectedVersion: ACPX_PINNED_VERSION,
|
|
allowPluginLocalInstall: true,
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses a short default queue-owner TTL", async () => {
|
|
const { runtime } = createRuntimeStub(true);
|
|
const runtimeFactory = vi.fn(() => runtime);
|
|
const service = createAcpxRuntimeService({
|
|
runtimeFactory,
|
|
});
|
|
const context = createServiceContext();
|
|
|
|
await service.start(context);
|
|
|
|
expect(runtimeFactory).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
queueOwnerTtlSeconds: 0.1,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not block startup while acpx ensure runs", async () => {
|
|
const { runtime } = createRuntimeStub(true);
|
|
ensureAcpxSpy.mockImplementation(() => new Promise<void>(() => {}));
|
|
const service = createAcpxRuntimeService({
|
|
runtimeFactory: () => runtime,
|
|
});
|
|
const context = createServiceContext();
|
|
|
|
const startResult = await Promise.race([
|
|
Promise.resolve(service.start(context)).then(() => "started"),
|
|
new Promise<string>((resolve) => setTimeout(() => resolve("timed_out"), 100)),
|
|
]);
|
|
|
|
expect(startResult).toBe("started");
|
|
expect(getAcpRuntimeBackend("acpx")?.runtime).toBe(runtime);
|
|
});
|
|
});
|