Merge upstream changes

This commit is contained in:
dj-oyu 2026-03-09 02:32:27 +09:00
parent b5d6fbe6ab
commit 42314b495a
3 changed files with 52 additions and 44 deletions

View file

@ -33,7 +33,8 @@
"Bash(golangci-lint run:*)", "Bash(golangci-lint run:*)",
"WebFetch(domain:core.telegram.org)", "WebFetch(domain:core.telegram.org)",
"Bash(go doc:*)", "Bash(go doc:*)",
"Bash(go install:*)" "Bash(go install:*)",
"WebFetch(domain:raw.githubusercontent.com)"
] ]
}, },
"remote": { "remote": {

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 {
if drafter, ok := w.ch.(DraftSender); ok {
if err := drafter.SendDraft(ctx, msg.ChatID, entry.draftID, ""); err != nil {
logger.WarnCF("channels", "Failed to dismiss task draft before final message", map[string]any{
"task_id": taskKey,
"chat_id": msg.ChatID,
"draft_id": entry.draftID,
"error": err.Error(),
})
}
}
}
}
m.statusEditTimes.Delete(taskKey) m.statusEditTimes.Delete(taskKey)
if loaded {
if entry, ok := v.(statusMsgEntry); ok {
// Path A: a permanent message exists — edit it in-place.
if entry.messageID != "" {
if editor, ok := w.ch.(MessageEditor); ok {
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.
}
}
}
// 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")