mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 10:32:43 +00:00
test(messaging): dedupe parser/proxy/followup test scaffolding
This commit is contained in:
@@ -11,6 +11,28 @@ type ParsedSetUnsetAction =
|
|||||||
| { action: "unset"; path: string }
|
| { action: "unset"; path: string }
|
||||||
| { action: "error"; message: string };
|
| { action: "error"; message: string };
|
||||||
|
|
||||||
|
function createActionMappers() {
|
||||||
|
return {
|
||||||
|
onSet: (path: string, value: unknown): ParsedSetUnsetAction => ({ action: "set", path, value }),
|
||||||
|
onUnset: (path: string): ParsedSetUnsetAction => ({ action: "unset", path }),
|
||||||
|
onError: (message: string): ParsedSetUnsetAction => ({ action: "error", message }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSlashParams(params: {
|
||||||
|
raw: string;
|
||||||
|
onKnownAction?: (action: string) => ParsedSetUnsetAction | undefined;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
raw: params.raw,
|
||||||
|
slash: "/config",
|
||||||
|
invalidMessage: "Invalid /config syntax.",
|
||||||
|
usageMessage: "Usage: /config show|set|unset",
|
||||||
|
onKnownAction: params.onKnownAction ?? (() => undefined),
|
||||||
|
...createActionMappers(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("parseSetUnsetCommand", () => {
|
describe("parseSetUnsetCommand", () => {
|
||||||
it("parses unset values", () => {
|
it("parses unset values", () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -35,25 +57,23 @@ describe("parseSetUnsetCommand", () => {
|
|||||||
|
|
||||||
describe("parseSetUnsetCommandAction", () => {
|
describe("parseSetUnsetCommandAction", () => {
|
||||||
it("returns null for non set/unset actions", () => {
|
it("returns null for non set/unset actions", () => {
|
||||||
|
const mappers = createActionMappers();
|
||||||
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
||||||
slash: "/config",
|
slash: "/config",
|
||||||
action: "show",
|
action: "show",
|
||||||
args: "",
|
args: "",
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
...mappers,
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
});
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps parse errors through onError", () => {
|
it("maps parse errors through onError", () => {
|
||||||
|
const mappers = createActionMappers();
|
||||||
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
||||||
slash: "/config",
|
slash: "/config",
|
||||||
action: "set",
|
action: "set",
|
||||||
args: "",
|
args: "",
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
...mappers,
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
});
|
||||||
expect(result).toEqual({ action: "error", message: "Usage: /config set path=value" });
|
expect(result).toEqual({ action: "error", message: "Usage: /config set path=value" });
|
||||||
});
|
});
|
||||||
@@ -61,57 +81,36 @@ describe("parseSetUnsetCommandAction", () => {
|
|||||||
|
|
||||||
describe("parseSlashCommandWithSetUnset", () => {
|
describe("parseSlashCommandWithSetUnset", () => {
|
||||||
it("returns null when the input does not match the slash command", () => {
|
it("returns null when the input does not match the slash command", () => {
|
||||||
const result = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
const result = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||||
raw: "/debug show",
|
createSlashParams({ raw: "/debug show" }),
|
||||||
slash: "/config",
|
);
|
||||||
invalidMessage: "Invalid /config syntax.",
|
|
||||||
usageMessage: "Usage: /config show|set|unset",
|
|
||||||
onKnownAction: () => undefined,
|
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers set/unset mapping and falls back to known actions", () => {
|
it("prefers set/unset mapping and falls back to known actions", () => {
|
||||||
const setResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
const setResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||||
raw: '/config set a.b={"ok":true}',
|
createSlashParams({
|
||||||
slash: "/config",
|
raw: '/config set a.b={"ok":true}',
|
||||||
invalidMessage: "Invalid /config syntax.",
|
}),
|
||||||
usageMessage: "Usage: /config show|set|unset",
|
);
|
||||||
onKnownAction: () => undefined,
|
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
|
||||||
expect(setResult).toEqual({ action: "set", path: "a.b", value: { ok: true } });
|
expect(setResult).toEqual({ action: "set", path: "a.b", value: { ok: true } });
|
||||||
|
|
||||||
const showResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
const showResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||||
raw: "/config show",
|
createSlashParams({
|
||||||
slash: "/config",
|
raw: "/config show",
|
||||||
invalidMessage: "Invalid /config syntax.",
|
onKnownAction: (action) =>
|
||||||
usageMessage: "Usage: /config show|set|unset",
|
action === "show" ? { action: "unset", path: "dummy" } : undefined,
|
||||||
onKnownAction: (action) =>
|
}),
|
||||||
action === "show" ? { action: "unset", path: "dummy" } : undefined,
|
);
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
|
||||||
expect(showResult).toEqual({ action: "unset", path: "dummy" });
|
expect(showResult).toEqual({ action: "unset", path: "dummy" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns onError for unknown actions", () => {
|
it("returns onError for unknown actions", () => {
|
||||||
const unknownAction = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
const unknownAction = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||||
raw: "/config whoami",
|
createSlashParams({
|
||||||
slash: "/config",
|
raw: "/config whoami",
|
||||||
invalidMessage: "Invalid /config syntax.",
|
}),
|
||||||
usageMessage: "Usage: /config show|set|unset",
|
);
|
||||||
onKnownAction: () => undefined,
|
|
||||||
onSet: (path, value) => ({ action: "set", path, value }),
|
|
||||||
onUnset: (path) => ({ action: "unset", path }),
|
|
||||||
onError: (message) => ({ action: "error", message }),
|
|
||||||
});
|
|
||||||
expect(unknownAction).toEqual({ action: "error", message: "Usage: /config show|set|unset" });
|
expect(unknownAction).toEqual({ action: "error", message: "Usage: /config show|set|unset" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ const baseQueuedRun = (messageProvider = "whatsapp"): FollowupRun =>
|
|||||||
},
|
},
|
||||||
}) as FollowupRun;
|
}) as FollowupRun;
|
||||||
|
|
||||||
|
function mockCompactionRun(params: {
|
||||||
|
willRetry: boolean;
|
||||||
|
result: {
|
||||||
|
payloads: Array<{ text: string }>;
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||||
|
async (args: {
|
||||||
|
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
||||||
|
}) => {
|
||||||
|
args.onAgentEvent?.({
|
||||||
|
stream: "compaction",
|
||||||
|
data: { phase: "end", willRetry: params.willRetry },
|
||||||
|
});
|
||||||
|
return params.result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe("createFollowupRunner compaction", () => {
|
describe("createFollowupRunner compaction", () => {
|
||||||
it("adds verbose auto-compaction notice and tracks count", async () => {
|
it("adds verbose auto-compaction notice and tracks count", async () => {
|
||||||
const storePath = path.join(
|
const storePath = path.join(
|
||||||
@@ -75,17 +95,10 @@ describe("createFollowupRunner compaction", () => {
|
|||||||
};
|
};
|
||||||
const onBlockReply = vi.fn(async () => {});
|
const onBlockReply = vi.fn(async () => {});
|
||||||
|
|
||||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
mockCompactionRun({
|
||||||
async (params: {
|
willRetry: true,
|
||||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
result: { payloads: [{ text: "final" }], meta: {} },
|
||||||
}) => {
|
});
|
||||||
params.onAgentEvent?.({
|
|
||||||
stream: "compaction",
|
|
||||||
data: { phase: "end", willRetry: true },
|
|
||||||
});
|
|
||||||
return { payloads: [{ text: "final" }], meta: {} };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const runner = createFollowupRunner({
|
const runner = createFollowupRunner({
|
||||||
opts: { onBlockReply },
|
opts: { onBlockReply },
|
||||||
@@ -149,29 +162,22 @@ describe("createFollowupRunner compaction", () => {
|
|||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
const onBlockReply = vi.fn(async () => {});
|
const onBlockReply = vi.fn(async () => {});
|
||||||
|
|
||||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
mockCompactionRun({
|
||||||
async (params: {
|
willRetry: false,
|
||||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
result: {
|
||||||
}) => {
|
payloads: [{ text: "done" }],
|
||||||
params.onAgentEvent?.({
|
meta: {
|
||||||
stream: "compaction",
|
agentMeta: {
|
||||||
data: { phase: "end", willRetry: false },
|
// Accumulated usage across pre+post compaction calls.
|
||||||
});
|
usage: { input: 190_000, output: 8_000, total: 198_000 },
|
||||||
return {
|
// Last call usage reflects post-compaction context.
|
||||||
payloads: [{ text: "done" }],
|
lastCallUsage: { input: 11_000, output: 2_000, total: 13_000 },
|
||||||
meta: {
|
model: "claude-opus-4-5",
|
||||||
agentMeta: {
|
provider: "anthropic",
|
||||||
// Accumulated usage across pre+post compaction calls.
|
|
||||||
usage: { input: 190_000, output: 8_000, total: 198_000 },
|
|
||||||
// Last call usage reflects post-compaction context.
|
|
||||||
lastCallUsage: { input: 11_000, output: 2_000, total: 13_000 },
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
provider: "anthropic",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
const runner = createFollowupRunner({
|
const runner = createFollowupRunner({
|
||||||
opts: { onBlockReply },
|
opts: { onBlockReply },
|
||||||
|
|||||||
@@ -56,6 +56,25 @@ import { deleteMessageTelegram, reactMessageTelegram, sendMessageTelegram } from
|
|||||||
describe("telegram proxy client", () => {
|
describe("telegram proxy client", () => {
|
||||||
const proxyUrl = "http://proxy.test:8080";
|
const proxyUrl = "http://proxy.test:8080";
|
||||||
|
|
||||||
|
const prepareProxyFetch = () => {
|
||||||
|
const proxyFetch = vi.fn();
|
||||||
|
const fetchImpl = vi.fn();
|
||||||
|
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
||||||
|
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
||||||
|
return { proxyFetch, fetchImpl };
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectProxyClient = (fetchImpl: ReturnType<typeof vi.fn>) => {
|
||||||
|
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
||||||
|
expect(resolveTelegramFetch).toHaveBeenCalledWith(expect.any(Function), { network: undefined });
|
||||||
|
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||||
|
"tok",
|
||||||
|
expect.objectContaining({
|
||||||
|
client: expect.objectContaining({ fetch: fetchImpl }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
||||||
botApi.setMessageReaction.mockResolvedValue(undefined);
|
botApi.setMessageReaction.mockResolvedValue(undefined);
|
||||||
@@ -69,56 +88,26 @@ describe("telegram proxy client", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("uses proxy fetch for sendMessage", async () => {
|
it("uses proxy fetch for sendMessage", async () => {
|
||||||
const proxyFetch = vi.fn();
|
const { fetchImpl } = prepareProxyFetch();
|
||||||
const fetchImpl = vi.fn();
|
|
||||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
|
||||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
|
||||||
|
|
||||||
await sendMessageTelegram("123", "hi", { token: "tok", accountId: "foo" });
|
await sendMessageTelegram("123", "hi", { token: "tok", accountId: "foo" });
|
||||||
|
|
||||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
expectProxyClient(fetchImpl);
|
||||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
|
||||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
|
||||||
"tok",
|
|
||||||
expect.objectContaining({
|
|
||||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses proxy fetch for reactions", async () => {
|
it("uses proxy fetch for reactions", async () => {
|
||||||
const proxyFetch = vi.fn();
|
const { fetchImpl } = prepareProxyFetch();
|
||||||
const fetchImpl = vi.fn();
|
|
||||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
|
||||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
|
||||||
|
|
||||||
await reactMessageTelegram("123", "456", "✅", { token: "tok", accountId: "foo" });
|
await reactMessageTelegram("123", "456", "✅", { token: "tok", accountId: "foo" });
|
||||||
|
|
||||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
expectProxyClient(fetchImpl);
|
||||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
|
||||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
|
||||||
"tok",
|
|
||||||
expect.objectContaining({
|
|
||||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses proxy fetch for deleteMessage", async () => {
|
it("uses proxy fetch for deleteMessage", async () => {
|
||||||
const proxyFetch = vi.fn();
|
const { fetchImpl } = prepareProxyFetch();
|
||||||
const fetchImpl = vi.fn();
|
|
||||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
|
||||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
|
||||||
|
|
||||||
await deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" });
|
await deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" });
|
||||||
|
|
||||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
expectProxyClient(fetchImpl);
|
||||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
|
||||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
|
||||||
"tok",
|
|
||||||
expect.objectContaining({
|
|
||||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user