Merge pull request #24 from dj-oyu/fix/telegram-group-duplicate-status

fix(telegram): avoid duplicate group status replies on draft fallback
This commit is contained in:
dj-oyu 2026-03-04 06:13:02 +09:00 committed by GitHub
commit d45f3dc6a7
2 changed files with 101 additions and 4 deletions

View file

@ -534,12 +534,15 @@ func (m *Manager) handleStatusSend(ctx context.Context, name string, w *channelW
}
if did == 0 {
did = generateDraftID(key)
}
if err := drafter.SendDraft(ctx, msg.ChatID, did, msg.Content); err == nil {
// Track draft only after successful send. If draft fails (e.g. group
// main thread), keep existing messageID entry so fallback edits can
// reuse the same status bubble instead of creating duplicates.
m.statusMsgIDs.Store(key, statusMsgEntry{
draftID: did,
createdAt: time.Now(),
})
}
if err := drafter.SendDraft(ctx, msg.ChatID, did, msg.Content); err == nil {
return
}
// Draft failed — fall through to edit-based approach
@ -628,12 +631,14 @@ func (m *Manager) handleTaskStatusSend(ctx context.Context, name string, w *chan
}
if did == 0 {
did = generateDraftID(taskKey)
}
if err := drafter.SendDraft(ctx, msg.ChatID, did, msg.Content); err == nil {
// Track draft only after successful send to avoid clobbering an
// existing messageID entry when drafts are unsupported.
m.taskMsgIDs.Store(taskKey, statusMsgEntry{
draftID: did,
createdAt: time.Now(),
})
}
if err := drafter.SendDraft(ctx, msg.ChatID, did, msg.Content); err == nil {
return
}
// Draft failed — fall through to edit-based approach

View file

@ -1355,6 +1355,49 @@ func TestHandleStatusSend_DraftFails_FallsToEdit(t *testing.T) {
}
}
func TestHandleStatusSend_DraftFailure_DoesNotClobberTrackedMessageID(t *testing.T) {
m := newTestManager()
var sendWithIDCount int
var editCount int
var editedMessageID string
ch := &mockDraftSender{
mockChannel: mockChannel{
sendFn: func(_ context.Context, _ bus.OutboundMessage) error { return nil },
},
draftFn: func(_ context.Context, _ string, _ int, _ string) error {
return fmt.Errorf("draft unsupported")
},
editFn: func(_ context.Context, _, messageID, _ string) error {
editCount++
editedMessageID = messageID
return nil
},
sendWithID: func(_ context.Context, _, _ string) (string, error) {
sendWithIDCount++
return "msg-1", nil
},
}
w := &channelWorker{ch: ch, limiter: rate.NewLimiter(rate.Inf, 1)}
msg := bus.OutboundMessage{Channel: "test", ChatID: "group-main", Content: "preview-1", IsStatus: true}
m.handleStatusSend(context.Background(), "test", w, msg)
msg.Content = "preview-2"
m.handleStatusSend(context.Background(), "test", w, msg)
if sendWithIDCount != 1 {
t.Fatalf("expected SendWithID to be called once, got %d", sendWithIDCount)
}
if editCount != 1 {
t.Fatalf("expected EditMessage to be called once, got %d", editCount)
}
if editedMessageID != "msg-1" {
t.Fatalf("expected EditMessage target msg-1, got %s", editedMessageID)
}
}
func TestHandleTaskStatusSend_UsesDraftSender(t *testing.T) {
m := newTestManager()
var draftCalled bool
@ -1393,6 +1436,55 @@ func TestHandleTaskStatusSend_UsesDraftSender(t *testing.T) {
}
}
func TestHandleTaskStatusSend_DraftFailure_DoesNotClobberTrackedMessageID(t *testing.T) {
m := newTestManager()
var sendWithIDCount int
var editCount int
var editedMessageID string
ch := &mockDraftSender{
mockChannel: mockChannel{
sendFn: func(_ context.Context, _ bus.OutboundMessage) error { return nil },
},
draftFn: func(_ context.Context, _ string, _ int, _ string) error {
return fmt.Errorf("draft unsupported")
},
editFn: func(_ context.Context, _, messageID, _ string) error {
editCount++
editedMessageID = messageID
return nil
},
sendWithID: func(_ context.Context, _, _ string) (string, error) {
sendWithIDCount++
return "task-msg-1", nil
},
}
w := &channelWorker{ch: ch, limiter: rate.NewLimiter(rate.Inf, 1)}
msg := bus.OutboundMessage{
Channel: "test",
ChatID: "group-main",
Content: "task-10%",
IsTaskStatus: true,
TaskID: "task-1",
}
m.handleTaskStatusSend(context.Background(), "test", w, msg)
msg.Content = "task-20%"
m.handleTaskStatusSend(context.Background(), "test", w, msg)
if sendWithIDCount != 1 {
t.Fatalf("expected SendWithID to be called once, got %d", sendWithIDCount)
}
if editCount != 1 {
t.Fatalf("expected EditMessage to be called once, got %d", editCount)
}
if editedMessageID != "task-msg-1" {
t.Fatalf("expected EditMessage target task-msg-1, got %s", editedMessageID)
}
}
func TestPreSend_ClearsDraftState(t *testing.T) {
m := newTestManager()