mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-21 16:41:56 +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: "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", () => {
|
||||
it("parses unset values", () => {
|
||||
expect(
|
||||
@@ -35,25 +57,23 @@ describe("parseSetUnsetCommand", () => {
|
||||
|
||||
describe("parseSetUnsetCommandAction", () => {
|
||||
it("returns null for non set/unset actions", () => {
|
||||
const mappers = createActionMappers();
|
||||
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
||||
slash: "/config",
|
||||
action: "show",
|
||||
args: "",
|
||||
onSet: (path, value) => ({ action: "set", path, value }),
|
||||
onUnset: (path) => ({ action: "unset", path }),
|
||||
onError: (message) => ({ action: "error", message }),
|
||||
...mappers,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("maps parse errors through onError", () => {
|
||||
const mappers = createActionMappers();
|
||||
const result = parseSetUnsetCommandAction<ParsedSetUnsetAction>({
|
||||
slash: "/config",
|
||||
action: "set",
|
||||
args: "",
|
||||
onSet: (path, value) => ({ action: "set", path, value }),
|
||||
onUnset: (path) => ({ action: "unset", path }),
|
||||
onError: (message) => ({ action: "error", message }),
|
||||
...mappers,
|
||||
});
|
||||
expect(result).toEqual({ action: "error", message: "Usage: /config set path=value" });
|
||||
});
|
||||
@@ -61,57 +81,36 @@ describe("parseSetUnsetCommandAction", () => {
|
||||
|
||||
describe("parseSlashCommandWithSetUnset", () => {
|
||||
it("returns null when the input does not match the slash command", () => {
|
||||
const result = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
||||
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 }),
|
||||
});
|
||||
const result = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||
createSlashParams({ raw: "/debug show" }),
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers set/unset mapping and falls back to known actions", () => {
|
||||
const setResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
||||
raw: '/config set a.b={"ok":true}',
|
||||
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 }),
|
||||
});
|
||||
const setResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||
createSlashParams({
|
||||
raw: '/config set a.b={"ok":true}',
|
||||
}),
|
||||
);
|
||||
expect(setResult).toEqual({ action: "set", path: "a.b", value: { ok: true } });
|
||||
|
||||
const showResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
||||
raw: "/config show",
|
||||
slash: "/config",
|
||||
invalidMessage: "Invalid /config syntax.",
|
||||
usageMessage: "Usage: /config show|set|unset",
|
||||
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 }),
|
||||
});
|
||||
const showResult = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||
createSlashParams({
|
||||
raw: "/config show",
|
||||
onKnownAction: (action) =>
|
||||
action === "show" ? { action: "unset", path: "dummy" } : undefined,
|
||||
}),
|
||||
);
|
||||
expect(showResult).toEqual({ action: "unset", path: "dummy" });
|
||||
});
|
||||
|
||||
it("returns onError for unknown actions", () => {
|
||||
const unknownAction = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>({
|
||||
raw: "/config whoami",
|
||||
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 }),
|
||||
});
|
||||
const unknownAction = parseSlashCommandWithSetUnset<ParsedSetUnsetAction>(
|
||||
createSlashParams({
|
||||
raw: "/config whoami",
|
||||
}),
|
||||
);
|
||||
expect(unknownAction).toEqual({ action: "error", message: "Usage: /config show|set|unset" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,26 @@ const baseQueuedRun = (messageProvider = "whatsapp"): 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", () => {
|
||||
it("adds verbose auto-compaction notice and tracks count", async () => {
|
||||
const storePath = path.join(
|
||||
@@ -75,17 +95,10 @@ describe("createFollowupRunner compaction", () => {
|
||||
};
|
||||
const onBlockReply = vi.fn(async () => {});
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
||||
}) => {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
data: { phase: "end", willRetry: true },
|
||||
});
|
||||
return { payloads: [{ text: "final" }], meta: {} };
|
||||
},
|
||||
);
|
||||
mockCompactionRun({
|
||||
willRetry: true,
|
||||
result: { payloads: [{ text: "final" }], meta: {} },
|
||||
});
|
||||
|
||||
const runner = createFollowupRunner({
|
||||
opts: { onBlockReply },
|
||||
@@ -149,29 +162,22 @@ describe("createFollowupRunner compaction", () => {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
const onBlockReply = vi.fn(async () => {});
|
||||
|
||||
runEmbeddedPiAgentMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void;
|
||||
}) => {
|
||||
params.onAgentEvent?.({
|
||||
stream: "compaction",
|
||||
data: { phase: "end", willRetry: false },
|
||||
});
|
||||
return {
|
||||
payloads: [{ text: "done" }],
|
||||
meta: {
|
||||
agentMeta: {
|
||||
// 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",
|
||||
},
|
||||
mockCompactionRun({
|
||||
willRetry: false,
|
||||
result: {
|
||||
payloads: [{ text: "done" }],
|
||||
meta: {
|
||||
agentMeta: {
|
||||
// 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({
|
||||
opts: { onBlockReply },
|
||||
|
||||
@@ -56,6 +56,25 @@ import { deleteMessageTelegram, reactMessageTelegram, sendMessageTelegram } from
|
||||
describe("telegram proxy client", () => {
|
||||
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(() => {
|
||||
botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
|
||||
botApi.setMessageReaction.mockResolvedValue(undefined);
|
||||
@@ -69,56 +88,26 @@ describe("telegram proxy client", () => {
|
||||
});
|
||||
|
||||
it("uses proxy fetch for sendMessage", async () => {
|
||||
const proxyFetch = vi.fn();
|
||||
const fetchImpl = vi.fn();
|
||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
||||
const { fetchImpl } = prepareProxyFetch();
|
||||
|
||||
await sendMessageTelegram("123", "hi", { token: "tok", accountId: "foo" });
|
||||
|
||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
||||
}),
|
||||
);
|
||||
expectProxyClient(fetchImpl);
|
||||
});
|
||||
|
||||
it("uses proxy fetch for reactions", async () => {
|
||||
const proxyFetch = vi.fn();
|
||||
const fetchImpl = vi.fn();
|
||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
||||
const { fetchImpl } = prepareProxyFetch();
|
||||
|
||||
await reactMessageTelegram("123", "456", "✅", { token: "tok", accountId: "foo" });
|
||||
|
||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
||||
}),
|
||||
);
|
||||
expectProxyClient(fetchImpl);
|
||||
});
|
||||
|
||||
it("uses proxy fetch for deleteMessage", async () => {
|
||||
const proxyFetch = vi.fn();
|
||||
const fetchImpl = vi.fn();
|
||||
makeProxyFetch.mockReturnValue(proxyFetch as unknown as typeof fetch);
|
||||
resolveTelegramFetch.mockReturnValue(fetchImpl as unknown as typeof fetch);
|
||||
const { fetchImpl } = prepareProxyFetch();
|
||||
|
||||
await deleteMessageTelegram("123", "456", { token: "tok", accountId: "foo" });
|
||||
|
||||
expect(makeProxyFetch).toHaveBeenCalledWith(proxyUrl);
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(proxyFetch, { network: undefined });
|
||||
expect(botCtorSpy).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
client: expect.objectContaining({ fetch: fetchImpl }),
|
||||
}),
|
||||
);
|
||||
expectProxyClient(fetchImpl);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user