Skip to content

Commit d3612f4

Browse files
committed
fix(telegram): materialize dm draft final to avoid duplicates
1 parent bd25182 commit d3612f4

File tree

2 files changed

+83
-12
lines changed

2 files changed

+83
-12
lines changed

src/telegram/lane-delivery.test.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ describe("createLaneTextDeliverer", () => {
215215
expect(harness.log).toHaveBeenCalledWith(expect.stringContaining("preview final too long"));
216216
});
217217

218-
it("sends a final message after DM draft streaming even when text is unchanged", async () => {
219-
const answerStream = createTestDraftStream({ previewMode: "draft" });
218+
it("materializes DM draft streaming final even when text is unchanged", async () => {
219+
const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 321 });
220+
answerStream.materialize.mockResolvedValue(321);
220221
answerStream.update.mockImplementation(() => {});
221222
const harness = createHarness({
222223
answerStream: answerStream as DraftLaneState["stream"],
@@ -231,18 +232,17 @@ describe("createLaneTextDeliverer", () => {
231232
infoKind: "final",
232233
});
233234

234-
expect(result).toBe("sent");
235+
expect(result).toBe("preview-finalized");
235236
expect(harness.flushDraftLane).toHaveBeenCalled();
236-
expect(harness.stopDraftLane).toHaveBeenCalled();
237-
expect(harness.sendPayload).toHaveBeenCalledWith(
238-
expect.objectContaining({ text: "Hello final" }),
239-
);
240-
expect(harness.markDelivered).not.toHaveBeenCalled();
237+
expect(answerStream.materialize).toHaveBeenCalledTimes(1);
238+
expect(harness.sendPayload).not.toHaveBeenCalled();
239+
expect(harness.markDelivered).toHaveBeenCalledTimes(1);
241240
});
242241

243-
it("sends a final message after DM draft streaming when revision changes", async () => {
242+
it("materializes DM draft streaming final when revision changes", async () => {
244243
let previewRevision = 3;
245-
const answerStream = createTestDraftStream({ previewMode: "draft" });
244+
const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 654 });
245+
answerStream.materialize.mockResolvedValue(654);
246246
answerStream.previewRevision.mockImplementation(() => previewRevision);
247247
answerStream.update.mockImplementation(() => {});
248248
answerStream.flush.mockImplementation(async () => {
@@ -261,11 +261,36 @@ describe("createLaneTextDeliverer", () => {
261261
infoKind: "final",
262262
});
263263

264+
expect(result).toBe("preview-finalized");
265+
expect(answerStream.materialize).toHaveBeenCalledTimes(1);
266+
expect(harness.sendPayload).not.toHaveBeenCalled();
267+
expect(harness.markDelivered).toHaveBeenCalledTimes(1);
268+
});
269+
270+
it("falls back to normal send when draft materialize returns no message id", async () => {
271+
const answerStream = createTestDraftStream({ previewMode: "draft" });
272+
answerStream.materialize.mockResolvedValue(undefined);
273+
const harness = createHarness({
274+
answerStream: answerStream as DraftLaneState["stream"],
275+
answerHasStreamedMessage: true,
276+
answerLastPartialText: "Hello final",
277+
});
278+
279+
const result = await harness.deliverLaneText({
280+
laneName: "answer",
281+
text: "Hello final",
282+
payload: { text: "Hello final" },
283+
infoKind: "final",
284+
});
285+
264286
expect(result).toBe("sent");
287+
expect(answerStream.materialize).toHaveBeenCalledTimes(1);
265288
expect(harness.sendPayload).toHaveBeenCalledWith(
266-
expect.objectContaining({ text: "Final answer" }),
289+
expect.objectContaining({ text: "Hello final" }),
290+
);
291+
expect(harness.log).toHaveBeenCalledWith(
292+
expect.stringContaining("draft preview materialize produced no message id"),
267293
);
268-
expect(harness.markDelivered).not.toHaveBeenCalled();
269294
});
270295

271296
it("does not use DM draft final shortcut for media payloads", async () => {

src/telegram/lane-delivery.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,41 @@ function resolvePreviewTarget(params: ResolvePreviewTargetParams): PreviewTarget
156156
export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
157157
const getLanePreviewText = (lane: DraftLaneState) => lane.lastPartialText;
158158
const isDraftPreviewLane = (lane: DraftLaneState) => lane.stream?.previewMode?.() === "draft";
159+
const canMaterializeDraftFinal = (
160+
lane: DraftLaneState,
161+
previewButtons?: TelegramInlineButtons,
162+
) => {
163+
const hasPreviewButtons = Boolean(previewButtons && previewButtons.length > 0);
164+
return (
165+
isDraftPreviewLane(lane) &&
166+
!hasPreviewButtons &&
167+
typeof lane.stream?.materialize === "function"
168+
);
169+
};
170+
171+
const tryMaterializeDraftPreviewForFinal = async (args: {
172+
lane: DraftLaneState;
173+
laneName: LaneName;
174+
text: string;
175+
}): Promise<boolean> => {
176+
const stream = args.lane.stream;
177+
if (!stream || !isDraftPreviewLane(args.lane)) {
178+
return false;
179+
}
180+
// Draft previews have no message_id to edit; materialize the final text
181+
// into a real message and treat that as the finalized delivery.
182+
stream.update(args.text);
183+
const materializedMessageId = await stream.materialize?.();
184+
if (typeof materializedMessageId !== "number") {
185+
params.log(
186+
`telegram: ${args.laneName} draft preview materialize produced no message id; falling back to standard send`,
187+
);
188+
return false;
189+
}
190+
args.lane.lastPartialText = args.text;
191+
params.markDelivered();
192+
return true;
193+
};
159194

160195
const tryEditPreviewMessage = async (args: {
161196
laneName: LaneName;
@@ -363,6 +398,17 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
363398
return archivedResultAfterFlush;
364399
}
365400
}
401+
if (canMaterializeDraftFinal(lane, previewButtons)) {
402+
const materialized = await tryMaterializeDraftPreviewForFinal({
403+
lane,
404+
laneName,
405+
text,
406+
});
407+
if (materialized) {
408+
params.finalizedPreviewByLane[laneName] = true;
409+
return "preview-finalized";
410+
}
411+
}
366412
const finalized = await tryUpdatePreviewForLane({
367413
lane,
368414
laneName,

0 commit comments

Comments
 (0)