test(messaging): dedupe parser/proxy/followup test scaffolding

This commit is contained in:
Peter Steinberger
2026-02-19 07:23:51 +00:00
parent c085c9e6d0
commit 9ac6f46735
3 changed files with 109 additions and 115 deletions

View File

@@ -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" });
}); });
}); });

View File

@@ -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 },

View File

@@ -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 }),
}),
);
}); });
}); });