mirror of https://github.com/openclaw/openclaw.git
test: add target normalization helper coverage
This commit is contained in:
parent
bf6da81028
commit
146cba46ca
|
|
@ -0,0 +1,59 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
emitHeartbeatEvent,
|
||||
getLastHeartbeatEvent,
|
||||
onHeartbeatEvent,
|
||||
resolveIndicatorType,
|
||||
} from "./heartbeat-events.js";
|
||||
|
||||
describe("resolveIndicatorType", () => {
|
||||
it("maps heartbeat statuses to indicator types", () => {
|
||||
expect(resolveIndicatorType("ok-empty")).toBe("ok");
|
||||
expect(resolveIndicatorType("ok-token")).toBe("ok");
|
||||
expect(resolveIndicatorType("sent")).toBe("alert");
|
||||
expect(resolveIndicatorType("failed")).toBe("error");
|
||||
expect(resolveIndicatorType("skipped")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("heartbeat events", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-09T12:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("stores the last event and timestamps emitted payloads", () => {
|
||||
emitHeartbeatEvent({ status: "sent", to: "+123", preview: "ping" });
|
||||
|
||||
expect(getLastHeartbeatEvent()).toEqual({
|
||||
ts: 1767960000000,
|
||||
status: "sent",
|
||||
to: "+123",
|
||||
preview: "ping",
|
||||
});
|
||||
});
|
||||
|
||||
it("delivers events to listeners, isolates listener failures, and supports unsubscribe", () => {
|
||||
const seen: string[] = [];
|
||||
const unsubscribeFirst = onHeartbeatEvent((evt) => {
|
||||
seen.push(`first:${evt.status}`);
|
||||
});
|
||||
onHeartbeatEvent(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
const unsubscribeThird = onHeartbeatEvent((evt) => {
|
||||
seen.push(`third:${evt.status}`);
|
||||
});
|
||||
|
||||
emitHeartbeatEvent({ status: "ok-empty" });
|
||||
unsubscribeFirst();
|
||||
unsubscribeThird();
|
||||
emitHeartbeatEvent({ status: "failed" });
|
||||
|
||||
expect(seen).toEqual(["first:ok-empty", "third:ok-empty"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const normalizeChannelIdMock = vi.hoisted(() => vi.fn());
|
||||
const getChannelPluginMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../channels/plugins/index.js", () => ({
|
||||
normalizeChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args),
|
||||
getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistryVersion: (...args: unknown[]) =>
|
||||
getActivePluginRegistryVersionMock(...args),
|
||||
}));
|
||||
|
||||
import {
|
||||
buildTargetResolverSignature,
|
||||
normalizeChannelTargetInput,
|
||||
normalizeTargetForProvider,
|
||||
} from "./target-normalization.js";
|
||||
|
||||
describe("normalizeChannelTargetInput", () => {
|
||||
it("trims raw target input", () => {
|
||||
expect(normalizeChannelTargetInput(" channel:C1 ")).toBe("channel:C1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeTargetForProvider", () => {
|
||||
beforeEach(() => {
|
||||
normalizeChannelIdMock.mockReset();
|
||||
getChannelPluginMock.mockReset();
|
||||
getActivePluginRegistryVersionMock.mockReset();
|
||||
});
|
||||
|
||||
it("returns undefined for missing or blank raw input", () => {
|
||||
expect(normalizeTargetForProvider("telegram")).toBeUndefined();
|
||||
expect(normalizeTargetForProvider("telegram", " ")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("falls back to trimmed input when the provider is unknown or has no normalizer", () => {
|
||||
normalizeChannelIdMock.mockReturnValueOnce(null);
|
||||
expect(normalizeTargetForProvider("unknown", " raw-id ")).toBe("raw-id");
|
||||
|
||||
normalizeChannelIdMock.mockReturnValueOnce("telegram");
|
||||
getActivePluginRegistryVersionMock.mockReturnValueOnce(1);
|
||||
getChannelPluginMock.mockReturnValueOnce(undefined);
|
||||
expect(normalizeTargetForProvider("telegram", " raw-id ")).toBe("raw-id");
|
||||
});
|
||||
|
||||
it("uses the cached target normalizer until the plugin registry version changes", () => {
|
||||
const firstNormalizer = vi.fn((raw: string) => raw.trim().toUpperCase());
|
||||
const secondNormalizer = vi.fn((raw: string) => `next:${raw.trim()}`);
|
||||
normalizeChannelIdMock.mockReturnValue("telegram");
|
||||
getActivePluginRegistryVersionMock
|
||||
.mockReturnValueOnce(10)
|
||||
.mockReturnValueOnce(10)
|
||||
.mockReturnValueOnce(11);
|
||||
getChannelPluginMock
|
||||
.mockReturnValueOnce({
|
||||
messaging: { normalizeTarget: firstNormalizer },
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
messaging: { normalizeTarget: secondNormalizer },
|
||||
});
|
||||
|
||||
expect(normalizeTargetForProvider("telegram", " abc ")).toBe("ABC");
|
||||
expect(normalizeTargetForProvider("telegram", " def ")).toBe("DEF");
|
||||
expect(normalizeTargetForProvider("telegram", " ghi ")).toBe("next:ghi");
|
||||
|
||||
expect(getChannelPluginMock).toHaveBeenCalledTimes(2);
|
||||
expect(firstNormalizer).toHaveBeenCalledTimes(2);
|
||||
expect(secondNormalizer).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns undefined when the provider normalizer resolves to an empty value", () => {
|
||||
normalizeChannelIdMock.mockReturnValueOnce("telegram");
|
||||
getActivePluginRegistryVersionMock.mockReturnValueOnce(20);
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
normalizeTarget: () => "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalizeTargetForProvider("telegram", " raw-id ")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTargetResolverSignature", () => {
|
||||
beforeEach(() => {
|
||||
getChannelPluginMock.mockReset();
|
||||
});
|
||||
|
||||
it("builds stable signatures from resolver hint and looksLikeId source", () => {
|
||||
const looksLikeId = (value: string) => value.startsWith("C");
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
looksLikeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const first = buildTargetResolverSignature("slack");
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
looksLikeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
const second = buildTargetResolverSignature("slack");
|
||||
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it("changes when resolver metadata changes", () => {
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use channel id",
|
||||
looksLikeId: (value: string) => value.startsWith("C"),
|
||||
},
|
||||
},
|
||||
});
|
||||
const first = buildTargetResolverSignature("slack");
|
||||
|
||||
getChannelPluginMock.mockReturnValueOnce({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
hint: "Use user id",
|
||||
looksLikeId: (value: string) => value.startsWith("U"),
|
||||
},
|
||||
},
|
||||
});
|
||||
const second = buildTargetResolverSignature("slack");
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { extractToolPayload } from "./tool-payload.js";
|
||||
|
||||
describe("extractToolPayload", () => {
|
||||
it("prefers explicit details payloads", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
details: { ok: true },
|
||||
content: [{ type: "text", text: '{"ignored":true}' }],
|
||||
} as never),
|
||||
).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("parses JSON text blocks from tool content", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [
|
||||
{ type: "image", url: "https://example.com/a.png" },
|
||||
{ type: "text", text: '{"ok":true,"count":2}' },
|
||||
],
|
||||
} as never),
|
||||
).toEqual({ ok: true, count: 2 });
|
||||
});
|
||||
|
||||
it("falls back to raw text, then content, then the whole result", () => {
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content: [{ type: "text", text: "not json" }],
|
||||
} as never),
|
||||
).toBe("not json");
|
||||
|
||||
const content = [{ type: "image", url: "https://example.com/a.png" }];
|
||||
expect(
|
||||
extractToolPayload({
|
||||
content,
|
||||
} as never),
|
||||
).toBe(content);
|
||||
|
||||
const result = { status: "ok" };
|
||||
expect(extractToolPayload(result as never)).toBe(result);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue