Merge upstream changes

This commit is contained in:
dj-oyu 2026-03-09 02:32:27 +09:00
parent f60e2aea37
commit 7bfd5cc658
2 changed files with 50 additions and 43 deletions

View file

@ -633,26 +633,43 @@ func (m *Manager) handleTaskStatusSend(ctx context.Context, name string, w *chan
taskKey := taskStatusKey(name, msg.ChatID, msg.TaskID) taskKey := taskStatusKey(name, msg.ChatID, msg.TaskID)
// Final message: send as permanent (non-draft) message so it persists. // Final message: reuse the existing bubble when possible to avoid
// Drafts are ephemeral and disappear after a short time; the completion // duplicate messages. If a permanent message (messageID) is tracked,
// message must survive. Clear the draft tracking and send via SendWithID // edit it in-place. If a draft (draftID) is tracked, update it with
// or regular Send, which creates a permanent Telegram message. // the completion content (the draft persists in Telegram and serves
// as the visible message; sending a separate permanent message would
// create a duplicate).
if msg.Final { if msg.Final {
if v, loaded := m.taskMsgIDs.LoadAndDelete(taskKey); loaded { v, loaded := m.taskMsgIDs.LoadAndDelete(taskKey)
if entry, ok := v.(statusMsgEntry); ok && entry.draftID != 0 { m.statusEditTimes.Delete(taskKey)
if drafter, ok := w.ch.(DraftSender); ok {
if err := drafter.SendDraft(ctx, msg.ChatID, entry.draftID, ""); err != nil { if loaded {
logger.WarnCF("channels", "Failed to dismiss task draft before final message", map[string]any{ if entry, ok := v.(statusMsgEntry); ok {
"task_id": taskKey, // Path A: a permanent message exists — edit it in-place.
"chat_id": msg.ChatID, if entry.messageID != "" {
"draft_id": entry.draftID, if editor, ok := w.ch.(MessageEditor); ok {
"error": err.Error(), if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
}) return
}
} }
// Edit failed — fall through to send a new message.
}
// Path B: a draft exists — update it with the final
// content. Drafts persist visibly in Telegram, so do NOT
// send a separate permanent message (that causes duplicates).
if entry.draftID != 0 {
if drafter, ok := w.ch.(DraftSender); ok {
if err := drafter.SendDraft(ctx, msg.ChatID, entry.draftID, msg.Content); err == nil {
return
}
}
// Draft update failed — fall through to send permanent.
} }
} }
} }
m.statusEditTimes.Delete(taskKey)
// No existing bubble to reuse — send a new permanent message.
if sender, ok := w.ch.(MessageSenderWithID); ok { if sender, ok := w.ch.(MessageSenderWithID); ok {
if msgID, err := sender.SendWithID(ctx, msg.ChatID, msg.Content); err == nil && msgID != "" { if msgID, err := sender.SendWithID(ctx, msg.ChatID, msg.Content); err == nil && msgID != "" {
return return

View file

@ -1439,39 +1439,32 @@ func TestHandleTaskStatusSend_UsesDraftSender(t *testing.T) {
} }
} }
func TestHandleTaskStatusSend_Final_DismissesDraftBeforePermanentMessage(t *testing.T) { func TestHandleTaskStatusSend_Final_UpdatesDraftInPlace(t *testing.T) {
m := newTestManager() m := newTestManager()
var dismissCalled bool var draftUpdateCalled bool
var dismissDraftID int var draftUpdateDraftID int
var dismissContent string var draftUpdateContent string
var finalSendWithIDCalled bool
ch := &mockDraftSender{ ch := &mockDraftSender{
mockChannel: mockChannel{ mockChannel: mockChannel{
sendFn: func(_ context.Context, _ bus.OutboundMessage) error { sendFn: func(_ context.Context, _ bus.OutboundMessage) error {
t.Fatal("Send should not be called when SendWithID succeeds") t.Fatal("Send should not be called when draft update succeeds")
return nil return nil
}, },
}, },
draftFn: func(_ context.Context, chatID string, draftID int, content string) error { draftFn: func(_ context.Context, chatID string, draftID int, content string) error {
dismissCalled = true draftUpdateCalled = true
dismissDraftID = draftID draftUpdateDraftID = draftID
dismissContent = content draftUpdateContent = content
if chatID != "123" { if chatID != "123" {
t.Fatalf("expected dismiss chatID 123, got %s", chatID) t.Fatalf("expected chatID 123, got %s", chatID)
} }
return nil return nil
}, },
editFn: func(_ context.Context, _, _, _ string) error { return nil }, editFn: func(_ context.Context, _, _, _ string) error { return nil },
sendWithID: func(_ context.Context, chatID, content string) (string, error) { sendWithID: func(_ context.Context, _, _ string) (string, error) {
finalSendWithIDCalled = true t.Fatal("SendWithID should not be called when draft update succeeds")
if chatID != "123" { return "", nil
t.Fatalf("expected final chatID 123, got %s", chatID)
}
if content != "task completed" {
t.Fatalf("expected final content 'task completed', got %s", content)
}
return "task-final-1", nil
}, },
} }
@ -1490,17 +1483,14 @@ func TestHandleTaskStatusSend_Final_DismissesDraftBeforePermanentMessage(t *test
} }
m.handleTaskStatusSend(context.Background(), "test", w, msg) m.handleTaskStatusSend(context.Background(), "test", w, msg)
if !dismissCalled { if !draftUpdateCalled {
t.Fatal("expected SendDraft dismiss call for final task status") t.Fatal("expected SendDraft to update draft with final content")
} }
if dismissDraftID != 42 { if draftUpdateDraftID != 42 {
t.Fatalf("expected dismiss draftID 42, got %d", dismissDraftID) t.Fatalf("expected draftID 42, got %d", draftUpdateDraftID)
} }
if dismissContent != "" { if draftUpdateContent != "task completed" {
t.Fatalf("expected empty dismiss content, got %q", dismissContent) t.Fatalf("expected draft content 'task completed', got %q", draftUpdateContent)
}
if !finalSendWithIDCalled {
t.Fatal("expected final SendWithID to be called")
} }
if _, loaded := m.taskMsgIDs.Load(taskStatusKey("test", "123", "task-final")); loaded { if _, loaded := m.taskMsgIDs.Load(taskStatusKey("test", "123", "task-final")); loaded {
t.Fatal("expected taskMsgIDs entry to be deleted for final task status") t.Fatal("expected taskMsgIDs entry to be deleted for final task status")