fix(telegram): only retain on sendMayHaveLanded when prior preview is visible

When no prior message exists (first preview creation), prefer fallback
over silence — a duplicate is better than the user seeing nothing.
Gate sendMayHaveLanded retention on lane.hasStreamedMessage.
This commit is contained in:
hougangdev 2026-03-10 23:47:02 +08:00 committed by Ayaan Zaidi
parent 7aa96de998
commit 8bfe7db27a
2 changed files with 36 additions and 10 deletions

View File

@ -365,13 +365,9 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
context,
});
if (typeof previewTargetAfterStop.previewMessageId !== "number") {
if (lane.stream?.sendMayHaveLanded?.()) {
params.log(
`telegram: ${laneName} preview send may have landed despite missing message id; keeping to avoid duplicate`,
);
params.markDelivered();
return "retained";
}
// This is the first preview creation — no prior visible message exists.
// Even if sendMayHaveLanded, prefer fallback over silence: a duplicate
// is better than the user seeing nothing at all.
return "fallback";
}
return finalizePreview(previewTargetAfterStop.previewMessageId, true, false);
@ -386,7 +382,9 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
context,
});
if (typeof previewTargetAfterStop.previewMessageId !== "number") {
if (lane.stream?.sendMayHaveLanded?.()) {
// Only retain when a prior preview is already visible to the user —
// otherwise falling back is safer than silence.
if (lane.hasStreamedMessage && lane.stream?.sendMayHaveLanded?.()) {
params.log(
`telegram: ${laneName} preview send may have landed despite missing message id; keeping to avoid duplicate`,
);

View File

@ -538,10 +538,10 @@ describe("createLaneTextDeliverer", () => {
);
});
it("retains preview when sendMayHaveLanded is true and no messageId", async () => {
it("falls back when sendMayHaveLanded is true but no prior visible preview exists", async () => {
const stream = createTestDraftStream();
stream.sendMayHaveLanded.mockReturnValue(true);
// No messageId → resolvePreviewTarget returns undefined
// No messageId and no prior preview → nothing visible for user to see
const harness = createHarness({ answerStream: stream });
const result = await harness.deliverLaneText({
@ -551,6 +551,34 @@ describe("createLaneTextDeliverer", () => {
infoKind: "final",
});
// Prefer fallback (possible duplicate) over silence (no message at all)
expect(result).toBe("sent");
expect(harness.sendPayload).toHaveBeenCalledWith(
expect.objectContaining({ text: "Hello final" }),
);
});
it("retains when sendMayHaveLanded is true and a prior preview was visible", async () => {
// Stream has a messageId (visible preview) but loses it after stop
const stream = createTestDraftStream({ messageId: 999 });
stream.sendMayHaveLanded.mockReturnValue(true);
const harness = createHarness({
answerStream: stream,
answerHasStreamedMessage: true,
});
// Simulate messageId lost after stop (e.g. forceNewMessage or timeout)
harness.stopDraftLane.mockImplementation(async (lane: DraftLaneState) => {
stream.setMessageId(undefined);
await lane.stream?.stop();
});
const result = await harness.deliverLaneText({
laneName: "answer",
text: "Hello final",
payload: { text: "Hello final" },
infoKind: "final",
});
expect(result).toBe("preview-retained");
expect(harness.sendPayload).not.toHaveBeenCalled();
expect(harness.log).toHaveBeenCalledWith(