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

View File

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

View File

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