mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 01:30:43 +00:00
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { AcpRuntimeError } from "./errors.js";
|
|
import {
|
|
__testing,
|
|
getAcpRuntimeBackend,
|
|
registerAcpRuntimeBackend,
|
|
requireAcpRuntimeBackend,
|
|
unregisterAcpRuntimeBackend,
|
|
} from "./registry.js";
|
|
import type { AcpRuntime } from "./types.js";
|
|
|
|
function createRuntimeStub(): AcpRuntime {
|
|
return {
|
|
ensureSession: vi.fn(async (input) => ({
|
|
sessionKey: input.sessionKey,
|
|
backend: "stub",
|
|
runtimeSessionName: `${input.sessionKey}:runtime`,
|
|
})),
|
|
runTurn: vi.fn(async function* () {
|
|
// no-op stream
|
|
}),
|
|
cancel: vi.fn(async () => {}),
|
|
close: vi.fn(async () => {}),
|
|
};
|
|
}
|
|
|
|
describe("acp runtime registry", () => {
|
|
beforeEach(() => {
|
|
__testing.resetAcpRuntimeBackendsForTests();
|
|
});
|
|
|
|
it("registers and resolves backends by id", () => {
|
|
const runtime = createRuntimeStub();
|
|
registerAcpRuntimeBackend({ id: "acpx", runtime });
|
|
|
|
const backend = getAcpRuntimeBackend("acpx");
|
|
expect(backend?.id).toBe("acpx");
|
|
expect(backend?.runtime).toBe(runtime);
|
|
});
|
|
|
|
it("prefers a healthy backend when resolving without explicit id", () => {
|
|
const unhealthyRuntime = createRuntimeStub();
|
|
const healthyRuntime = createRuntimeStub();
|
|
|
|
registerAcpRuntimeBackend({
|
|
id: "unhealthy",
|
|
runtime: unhealthyRuntime,
|
|
healthy: () => false,
|
|
});
|
|
registerAcpRuntimeBackend({
|
|
id: "healthy",
|
|
runtime: healthyRuntime,
|
|
healthy: () => true,
|
|
});
|
|
|
|
const backend = getAcpRuntimeBackend();
|
|
expect(backend?.id).toBe("healthy");
|
|
});
|
|
|
|
it("throws a typed missing-backend error when no backend is registered", () => {
|
|
expect(() => requireAcpRuntimeBackend()).toThrowError(AcpRuntimeError);
|
|
expect(() => requireAcpRuntimeBackend()).toThrowError(/ACP runtime backend is not configured/i);
|
|
});
|
|
|
|
it("resolves the first healthy backend when requireAcpRuntimeBackend has no explicit id", () => {
|
|
const unhealthyRuntime = createRuntimeStub();
|
|
const healthyRuntime = createRuntimeStub();
|
|
|
|
registerAcpRuntimeBackend({
|
|
id: "unhealthy",
|
|
runtime: unhealthyRuntime,
|
|
healthy: () => false,
|
|
});
|
|
registerAcpRuntimeBackend({
|
|
id: "healthy",
|
|
runtime: healthyRuntime,
|
|
healthy: () => true,
|
|
});
|
|
|
|
const backend = requireAcpRuntimeBackend();
|
|
expect(backend.id).toBe("healthy");
|
|
expect(backend.runtime).toBe(healthyRuntime);
|
|
});
|
|
|
|
it("throws a typed unavailable error when the requested backend is unhealthy", () => {
|
|
registerAcpRuntimeBackend({
|
|
id: "acpx",
|
|
runtime: createRuntimeStub(),
|
|
healthy: () => false,
|
|
});
|
|
|
|
try {
|
|
requireAcpRuntimeBackend("acpx");
|
|
throw new Error("expected requireAcpRuntimeBackend to throw");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(AcpRuntimeError);
|
|
expect((err as AcpRuntimeError).code).toBe("ACP_BACKEND_UNAVAILABLE");
|
|
}
|
|
});
|
|
|
|
it("unregisters a backend by id", () => {
|
|
registerAcpRuntimeBackend({ id: "acpx", runtime: createRuntimeStub() });
|
|
unregisterAcpRuntimeBackend("acpx");
|
|
expect(getAcpRuntimeBackend("acpx")).toBeNull();
|
|
});
|
|
|
|
it("keeps backend state on a global registry for cross-loader access", () => {
|
|
const runtime = createRuntimeStub();
|
|
const sharedState = __testing.getAcpRuntimeRegistryGlobalStateForTests();
|
|
|
|
sharedState.backendsById.set("acpx", {
|
|
id: "acpx",
|
|
runtime,
|
|
});
|
|
|
|
const backend = getAcpRuntimeBackend("acpx");
|
|
expect(backend?.runtime).toBe(runtime);
|
|
});
|
|
});
|