import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import type { ChannelDirectoryEntry } from "../../channels/plugins/types.js" ;
import type { OpenClawConfig } from "../../config/config.js" ;
type TargetResolverModule = typeof import ("./target-resolver.js" );
let resetDirectoryCache: TargetResolverModule["resetDirectoryCache" ];
let resolveMessagingTarget: TargetResolverModule["resolveMessagingTarget" ];
let formatTargetDisplay: TargetResolverModule["formatTargetDisplay" ];
const mocks = vi.hoisted(() => ({
listPeers: vi.fn(),
listPeersLive: vi.fn(),
listGroups: vi.fn(),
listGroupsLive: vi.fn(),
resolveTarget: vi.fn(),
getChannelPlugin: vi.fn(),
getActivePluginChannelRegistryVersion: vi.fn(() => 1 ),
}));
vi.mock("../../channels/plugins/index.js" , () => ({
getLoadedChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
getChannelPlugin: (...args: unknown[]) => mocks.getChannelPlugin(...args),
normalizeChannelId: (value: string) => value,
}));
vi.mock("../../channels/plugins/registry-loaded-read.js" , () => ({
getLoadedChannelPluginForRead: (...args: unknown[]) => mocks.getChannelPlugin(...args),
}));
vi.mock("../../plugins/runtime.js" , () => ({
getActivePluginChannelRegistry: () => null ,
getActivePluginRegistry: () => null ,
getActivePluginChannelRegistryVersion: () => mocks.getActivePluginChannelRegistryVersion(),
}));
beforeAll(async () => {
({ resetDirectoryCache, resolveMessagingTarget, formatTargetDisplay } =
await import ("./target-resolver.js" ));
});
beforeEach(() => {
mocks.listPeers.mockReset();
mocks.listPeersLive.mockReset();
mocks.listGroups.mockReset();
mocks.listGroupsLive.mockReset();
mocks.resolveTarget.mockReset();
mocks.getChannelPlugin.mockReset();
mocks.getActivePluginChannelRegistryVersion.mockReset();
mocks.getActivePluginChannelRegistryVersion.mockReturnValue(1 );
resetDirectoryCache();
});
async function expectOkResolution(
params: Parameters<typeof resolveMessagingTarget>[0 ],
): Promise<Extract<Awaited<ReturnType<typeof resolveMessagingTarget>>, { ok: true }>> {
const result = await resolveMessagingTarget(params);
expect(result.ok).toBe(true );
if (!result.ok) {
throw new Error("expected successful target resolution" );
}
return result;
}
describe("resolveMessagingTarget (directory fallback)" , () => {
const cfg = {} as OpenClawConfig;
beforeEach(() => {
resetDirectoryCache();
mocks.getChannelPlugin.mockReturnValue({
directory: {
listPeers: mocks.listPeers,
listPeersLive: mocks.listPeersLive,
listGroups: mocks.listGroups,
listGroupsLive: mocks.listGroupsLive,
},
messaging: {
targetResolver: {
resolveTarget: mocks.resolveTarget,
},
},
});
});
it("uses live directory fallback and caches the result" , async () => {
const entry: ChannelDirectoryEntry = { kind: "group" , id: "123456789" , name: "support" };
mocks.listGroups.mockResolvedValue([]);
mocks.listGroupsLive.mockResolvedValue([entry]);
const first = await expectOkResolution({
cfg,
channel: "richchat" ,
input: "support" ,
});
expect(first.target.source).toBe("directory" );
expect(first.target.to).toBe("123456789" );
expect(mocks.listGroups).toHaveBeenCalledTimes(1 );
expect(mocks.listGroupsLive).toHaveBeenCalledTimes(1 );
const second = await expectOkResolution({
cfg,
channel: "richchat" ,
input: "support" ,
});
expect(second.target.to).toBe("123456789" );
expect(mocks.listGroups).toHaveBeenCalledTimes(1 );
expect(mocks.listGroupsLive).toHaveBeenCalledTimes(1 );
});
it("skips directory lookup for direct ids" , async () => {
const result = await expectOkResolution({
cfg,
channel: "richchat" ,
input: "123456789" ,
});
expect(result.target.source).toBe("normalized" );
expect(result.target.to).toBe("123456789" );
expect(mocks.listGroups).not.toHaveBeenCalled();
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
});
it("lets plugins override id-like target resolution before falling back to raw ids" , async () => {
mocks.getChannelPlugin.mockReturnValue({
messaging: {
targetResolver: {
looksLikeId: () => true ,
resolveTarget: mocks.resolveTarget,
},
},
});
mocks.resolveTarget.mockResolvedValue({
to: "user:dm-user-id" ,
kind: "user" ,
source: "directory" ,
});
const result = await expectOkResolution({
cfg,
channel: "workspace" ,
input: "dthcxgoxhifn3pwh65cut3ud3w" ,
});
expect(result.target).toEqual({
to: "user:dm-user-id" ,
kind: "user" ,
source: "directory" ,
display: undefined,
});
expect(mocks.resolveTarget).toHaveBeenCalledWith(
expect.objectContaining({
input: "dthcxgoxhifn3pwh65cut3ud3w" ,
}),
);
expect(mocks.listGroups).not.toHaveBeenCalled();
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
});
it("uses plugin chat-type inference for directory lookups and plugin fallback on miss" , async () => {
mocks.getChannelPlugin.mockReturnValue({
directory: {
listPeers: mocks.listPeers,
listPeersLive: mocks.listPeersLive,
},
messaging: {
inferTargetChatType: () => "direct" ,
targetResolver: {
looksLikeId: () => false ,
resolveTarget: mocks.resolveTarget,
},
},
});
mocks.listPeers.mockResolvedValue([]);
mocks.listPeersLive.mockResolvedValue([]);
mocks.resolveTarget.mockResolvedValue({
to: "+15551234567" ,
kind: "user" ,
source: "normalized" ,
});
const result = await expectOkResolution({
cfg,
channel: "localchat" ,
input: "+15551234567" ,
});
expect(result.target).toEqual({
to: "+15551234567" ,
kind: "user" ,
source: "normalized" ,
display: undefined,
});
expect(mocks.listPeers).toHaveBeenCalledTimes(1 );
expect(mocks.listPeersLive).toHaveBeenCalledTimes(1 );
expect(mocks.listGroups).not.toHaveBeenCalled();
expect(mocks.resolveTarget).toHaveBeenCalledWith(
expect.objectContaining({
input: "+15551234567" ,
}),
);
});
it("keeps plugin-owned id casing when resolver returns a normalized target" , async () => {
mocks.getChannelPlugin.mockReturnValue({
messaging: {
targetResolver: {
looksLikeId: () => true ,
resolveTarget: mocks.resolveTarget,
},
},
});
mocks.resolveTarget.mockResolvedValue({
to: "channel:C123ABC" ,
kind: "group" ,
source: "normalized" ,
});
const result = await expectOkResolution({
cfg,
channel: "workspace" ,
input: "#C123ABC" ,
});
expect(result.target.to).toBe("channel:C123ABC" );
expect(result.target.display).toBeUndefined();
});
it("defers target display formatting to the plugin when available" , () => {
mocks.getChannelPlugin.mockReturnValue({
messaging: {
formatTargetDisplay: ({ target }: { target: string }) => target.replace(/^forum:/i, "" ),
},
});
expect(formatTargetDisplay({ channel: "forum" , target: "forum:12345" })).toBe("12345" );
});
});
Messung V0.5 in Prozent C=94 H=92 G=92
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland