Files
openclaw/src/gateway/server-channels.approval-bootstrap.test.ts
Gustavo Madeira Santana d78512b09d Refactor: centralize native approval lifecycle assembly (#62135)
Merged via squash.

Prepared head SHA: b7c20a7398
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-04-07 14:40:26 -04:00

214 lines
6.7 KiB
TypeScript

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { type ChannelId, type ChannelPlugin } from "../channels/plugins/types.js";
import {
createSubsystemLogger,
runtimeForLogger,
type SubsystemLogger,
} from "../logging/subsystem.js";
import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
import { createRuntimeChannel } from "../plugins/runtime/runtime-channel.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
import type { RuntimeEnv } from "../runtime.js";
const hoisted = vi.hoisted(() => ({
startChannelApprovalHandlerBootstrap: vi.fn(async () => async () => {}),
}));
vi.mock("../infra/approval-handler-bootstrap.js", () => ({
startChannelApprovalHandlerBootstrap: hoisted.startChannelApprovalHandlerBootstrap,
}));
function createDeferred() {
let resolvePromise = () => {};
const promise = new Promise<void>((resolve) => {
resolvePromise = resolve;
});
return { promise, resolve: resolvePromise };
}
function createTestPlugin(params: {
startAccount: NonNullable<NonNullable<ChannelPlugin["gateway"]>["startAccount"]>;
}): ChannelPlugin {
return {
id: "discord",
meta: {
id: "discord",
label: "Discord",
selectionLabel: "Discord",
docsPath: "/channels/discord",
blurb: "test stub",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
resolveAccount: () => ({ enabled: true, configured: true }),
isEnabled: () => true,
describeAccount: () => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: true,
configured: true,
}),
},
approvalCapability: {
nativeRuntime: {
availability: {
isConfigured: vi.fn().mockReturnValue(true),
shouldHandle: vi.fn().mockReturnValue(true),
},
presentation: {
buildPendingPayload: vi.fn(),
buildResolvedResult: vi.fn(),
buildExpiredResult: vi.fn(),
},
transport: {
prepareTarget: vi.fn(),
deliverPending: vi.fn(),
},
},
},
gateway: {
startAccount: params.startAccount,
},
};
}
function installTestRegistry(plugin: ChannelPlugin) {
const registry = createEmptyPluginRegistry();
registry.channels.push({
pluginId: plugin.id,
source: "test",
plugin,
});
setActivePluginRegistry(registry);
}
function createManager(
createChannelManager: typeof import("./server-channels.js").createChannelManager,
options?: {
channelRuntime?: PluginRuntime["channel"];
},
) {
const log = createSubsystemLogger("gateway/server-channels-approval-bootstrap-test");
const channelLogs = { discord: log } as Record<ChannelId, SubsystemLogger>;
const runtime = runtimeForLogger(log);
const channelRuntimeEnvs = { discord: runtime } as unknown as Record<ChannelId, RuntimeEnv>;
return createChannelManager({
loadConfig: () => ({}),
channelLogs,
channelRuntimeEnvs,
...(options?.channelRuntime ? { channelRuntime: options.channelRuntime } : {}),
});
}
describe("server-channels approval bootstrap", () => {
let previousRegistry: PluginRegistry | null = null;
let createChannelManager: typeof import("./server-channels.js").createChannelManager;
beforeAll(async () => {
({ createChannelManager } = await import("./server-channels.js"));
});
beforeEach(() => {
previousRegistry = getActivePluginRegistry();
hoisted.startChannelApprovalHandlerBootstrap.mockReset();
});
afterEach(() => {
setActivePluginRegistry(previousRegistry ?? createEmptyPluginRegistry());
});
it("starts and stops the shared approval bootstrap with the channel lifecycle", async () => {
const channelRuntime = createRuntimeChannel();
const stopApprovalBootstrap = vi.fn(async () => {});
hoisted.startChannelApprovalHandlerBootstrap.mockResolvedValue(stopApprovalBootstrap);
const started = createDeferred();
const stopped = createDeferred();
const startAccount = vi.fn(
async ({
abortSignal,
channelRuntime,
}: Parameters<NonNullable<NonNullable<ChannelPlugin["gateway"]>["startAccount"]>>[0]) => {
channelRuntime?.runtimeContexts.register({
channelId: "discord",
accountId: DEFAULT_ACCOUNT_ID,
capability: "approval.native",
context: { token: "tracked" },
});
started.resolve();
await new Promise<void>((resolve) => {
abortSignal.addEventListener(
"abort",
() => {
stopped.resolve();
resolve();
},
{ once: true },
);
});
},
);
installTestRegistry(createTestPlugin({ startAccount }));
const manager = createManager(createChannelManager, { channelRuntime });
await manager.startChannels();
await started.promise;
expect(hoisted.startChannelApprovalHandlerBootstrap).toHaveBeenCalledWith(
expect.objectContaining({
plugin: expect.objectContaining({ id: "discord" }),
cfg: {},
accountId: DEFAULT_ACCOUNT_ID,
channelRuntime: expect.objectContaining({
runtimeContexts: expect.any(Object),
}),
}),
);
expect(
channelRuntime.runtimeContexts.get({
channelId: "discord",
accountId: DEFAULT_ACCOUNT_ID,
capability: "approval.native",
}),
).toEqual({ token: "tracked" });
await manager.stopChannel("discord", DEFAULT_ACCOUNT_ID);
await stopped.promise;
expect(stopApprovalBootstrap).toHaveBeenCalledTimes(1);
expect(
channelRuntime.runtimeContexts.get({
channelId: "discord",
accountId: DEFAULT_ACCOUNT_ID,
capability: "approval.native",
}),
).toBeUndefined();
});
it("keeps the account stopped when approval bootstrap startup fails", async () => {
const channelRuntime = createRuntimeChannel();
const startAccount = vi.fn(async () => {});
hoisted.startChannelApprovalHandlerBootstrap.mockRejectedValue(new Error("boom"));
installTestRegistry(createTestPlugin({ startAccount }));
const manager = createManager(createChannelManager, { channelRuntime });
await manager.startChannels();
expect(startAccount).not.toHaveBeenCalled();
const accountSnapshot =
manager.getRuntimeSnapshot().channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
expect(accountSnapshot).toEqual(
expect.objectContaining({
accountId: DEFAULT_ACCOUNT_ID,
running: false,
restartPending: false,
lastError: "boom",
}),
);
});
});