mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 12:11:41 +00:00
test(telegram): move preview-finalization cases to lane unit tests
This commit is contained in:
218
src/telegram/lane-delivery.test.ts
Normal file
218
src/telegram/lane-delivery.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { createLaneTextDeliverer, type DraftLaneState, type LaneName } from "./lane-delivery.js";
|
||||
|
||||
type MockStreamState = {
|
||||
stream: NonNullable<DraftLaneState["stream"]>;
|
||||
setMessageId: (value: number | undefined) => void;
|
||||
};
|
||||
|
||||
function createMockStream(initialMessageId?: number): MockStreamState {
|
||||
let messageId = initialMessageId;
|
||||
const stream = {
|
||||
update: vi.fn(),
|
||||
flush: vi.fn().mockResolvedValue(undefined),
|
||||
messageId: vi.fn().mockImplementation(() => messageId),
|
||||
clear: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
forceNewMessage: vi.fn(),
|
||||
previewMode: vi.fn().mockReturnValue("message"),
|
||||
previewRevision: vi.fn().mockReturnValue(0),
|
||||
} as unknown as NonNullable<DraftLaneState["stream"]>;
|
||||
return {
|
||||
stream,
|
||||
setMessageId: (value) => {
|
||||
messageId = value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHarness(params?: {
|
||||
answerMessageId?: number;
|
||||
draftMaxChars?: number;
|
||||
answerMessageIdAfterStop?: number;
|
||||
}) {
|
||||
const answer = createMockStream(params?.answerMessageId);
|
||||
const reasoning = createMockStream();
|
||||
const lanes: Record<LaneName, DraftLaneState> = {
|
||||
answer: { stream: answer.stream, lastPartialText: "", hasStreamedMessage: false },
|
||||
reasoning: { stream: reasoning.stream, lastPartialText: "", hasStreamedMessage: false },
|
||||
};
|
||||
const sendPayload = vi.fn().mockResolvedValue(true);
|
||||
const flushDraftLane = vi.fn().mockImplementation(async (lane: DraftLaneState) => {
|
||||
await lane.stream?.flush();
|
||||
});
|
||||
const stopDraftLane = vi.fn().mockImplementation(async (lane: DraftLaneState) => {
|
||||
if (lane === lanes.answer && params?.answerMessageIdAfterStop !== undefined) {
|
||||
answer.setMessageId(params.answerMessageIdAfterStop);
|
||||
}
|
||||
await lane.stream?.stop();
|
||||
});
|
||||
const editPreview = vi.fn().mockResolvedValue(undefined);
|
||||
const deletePreviewMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const log = vi.fn();
|
||||
const markDelivered = vi.fn();
|
||||
const finalizedPreviewByLane: Record<LaneName, boolean> = { answer: false, reasoning: false };
|
||||
const archivedAnswerPreviews: Array<{ messageId: number; textSnapshot: string }> = [];
|
||||
|
||||
const deliverLaneText = createLaneTextDeliverer({
|
||||
lanes,
|
||||
archivedAnswerPreviews,
|
||||
finalizedPreviewByLane,
|
||||
draftMaxChars: params?.draftMaxChars ?? 4_096,
|
||||
applyTextToPayload: (payload: ReplyPayload, text: string) => ({ ...payload, text }),
|
||||
sendPayload,
|
||||
flushDraftLane,
|
||||
stopDraftLane,
|
||||
editPreview,
|
||||
deletePreviewMessage,
|
||||
log,
|
||||
markDelivered,
|
||||
});
|
||||
|
||||
return {
|
||||
deliverLaneText,
|
||||
lanes,
|
||||
answer,
|
||||
sendPayload,
|
||||
flushDraftLane,
|
||||
stopDraftLane,
|
||||
editPreview,
|
||||
log,
|
||||
markDelivered,
|
||||
};
|
||||
}
|
||||
|
||||
describe("createLaneTextDeliverer", () => {
|
||||
it("finalizes text-only replies by editing an existing preview message", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Hello final",
|
||||
payload: { text: "Hello final" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("preview-finalized");
|
||||
expect(harness.editPreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
laneName: "answer",
|
||||
messageId: 999,
|
||||
text: "Hello final",
|
||||
context: "final",
|
||||
}),
|
||||
);
|
||||
expect(harness.sendPayload).not.toHaveBeenCalled();
|
||||
expect(harness.stopDraftLane).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("primes stop-created previews with final text before editing", async () => {
|
||||
const harness = createHarness({ answerMessageIdAfterStop: 777 });
|
||||
harness.lanes.answer.lastPartialText = "no";
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "no problem",
|
||||
payload: { text: "no problem" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("preview-finalized");
|
||||
expect(harness.answer.stream.update).toHaveBeenCalledWith("no problem");
|
||||
expect(harness.editPreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
laneName: "answer",
|
||||
messageId: 777,
|
||||
text: "no problem",
|
||||
}),
|
||||
);
|
||||
expect(harness.sendPayload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats stop-created preview edit failures as delivered", async () => {
|
||||
const harness = createHarness({ answerMessageIdAfterStop: 777 });
|
||||
harness.editPreview.mockRejectedValue(new Error("500: edit failed after stop flush"));
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Short final",
|
||||
payload: { text: "Short final" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("preview-finalized");
|
||||
expect(harness.editPreview).toHaveBeenCalledTimes(1);
|
||||
expect(harness.sendPayload).not.toHaveBeenCalled();
|
||||
expect(harness.log).toHaveBeenCalledWith(expect.stringContaining("treating as delivered"));
|
||||
});
|
||||
|
||||
it("falls back to normal delivery when editing an existing preview fails", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
harness.editPreview.mockRejectedValue(new Error("500: preview edit failed"));
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Hello final",
|
||||
payload: { text: "Hello final" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.editPreview).toHaveBeenCalledTimes(1);
|
||||
expect(harness.sendPayload).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ text: "Hello final" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to normal delivery when stop-created preview has no message id", async () => {
|
||||
const harness = createHarness();
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Short final",
|
||||
payload: { text: "Short final" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.editPreview).not.toHaveBeenCalled();
|
||||
expect(harness.sendPayload).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ text: "Short final" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps existing preview when final text regresses", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
harness.lanes.answer.lastPartialText = "Recovered final answer.";
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Recovered final answer",
|
||||
payload: { text: "Recovered final answer" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("preview-finalized");
|
||||
expect(harness.editPreview).not.toHaveBeenCalled();
|
||||
expect(harness.sendPayload).not.toHaveBeenCalled();
|
||||
expect(harness.markDelivered).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to normal delivery when final text exceeds preview edit limit", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999, draftMaxChars: 20 });
|
||||
const longText = "x".repeat(50);
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: longText,
|
||||
payload: { text: longText },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.editPreview).not.toHaveBeenCalled();
|
||||
expect(harness.sendPayload).toHaveBeenCalledWith(expect.objectContaining({ text: longText }));
|
||||
expect(harness.log).toHaveBeenCalledWith(expect.stringContaining("preview final too long"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user