Files
kandev/apps/backend/internal/clarification/canceller_test.go
T
2026-04-16 15:58:01 +01:00

120 lines
3.7 KiB
Go

package clarification
import (
"context"
"errors"
"testing"
"time"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events/bus"
taskmodels "github.com/kandev/kandev/internal/task/models"
)
type stubMessageStore struct {
messages map[string]*taskmodels.Message
updated []*taskmodels.Message
}
func (s *stubMessageStore) GetTaskSession(context.Context, string) (*taskmodels.TaskSession, error) {
return nil, errors.New("not implemented")
}
func (s *stubMessageStore) FindMessageByPendingID(_ context.Context, pendingID string) (*taskmodels.Message, error) {
m, ok := s.messages[pendingID]
if !ok {
return nil, errors.New("not found")
}
return m, nil
}
func (s *stubMessageStore) UpdateMessage(_ context.Context, m *taskmodels.Message) error {
s.updated = append(s.updated, m)
return nil
}
type stubEventBus struct {
events []*bus.Event
}
func (s *stubEventBus) Publish(_ context.Context, _ string, ev *bus.Event) error {
s.events = append(s.events, ev)
return nil
}
func newTestCanceller(t *testing.T, msgs map[string]*taskmodels.Message) (*Canceller, *stubMessageStore, *stubEventBus) {
t.Helper()
store := NewStore(time.Minute)
repo := &stubMessageStore{messages: msgs}
eventBus := &stubEventBus{}
return NewCanceller(store, repo, eventBus, logger.Default()), repo, eventBus
}
// TestCanceller_MarksStatusExpiredOnDisconnect verifies that when the agent's
// turn ends with a pending clarification, the message is marked status=expired.
// This is what causes the frontend overlay to close — without it, the overlay
// stays interactive and a user clicking X triggers an unintended new turn via
// the event fallback path in the respond handler.
func TestCanceller_MarksStatusExpiredOnDisconnect(t *testing.T) {
msgs := map[string]*taskmodels.Message{}
c, repo, _ := newTestCanceller(t, msgs)
pendingID := c.store.CreateRequest(&Request{SessionID: "s1"})
msgs[pendingID] = &taskmodels.Message{
ID: "m1",
TaskSessionID: "s1",
Metadata: map[string]any{
"status": "pending",
},
}
cancelled := c.CancelSessionAndNotify(context.Background(), "s1")
if cancelled != 1 {
t.Fatalf("expected 1 cancelled, got %d", cancelled)
}
if len(repo.updated) != 1 {
t.Fatalf("expected 1 message update, got %d", len(repo.updated))
}
updated := repo.updated[0]
if got, _ := updated.Metadata["agent_disconnected"].(bool); !got {
t.Errorf("expected agent_disconnected=true, got %v", updated.Metadata["agent_disconnected"])
}
if got, _ := updated.Metadata["status"].(string); got != "expired" {
t.Errorf("expected status=expired, got %q", got)
}
}
// TestCanceller_NoMessagesToUpdate confirms that a cancel with no pending
// clarifications returns 0 and does not touch the repo.
func TestCanceller_NoMessagesToUpdate(t *testing.T) {
c, repo, _ := newTestCanceller(t, map[string]*taskmodels.Message{})
if got := c.CancelSessionAndNotify(context.Background(), "nonexistent"); got != 0 {
t.Errorf("expected 0 cancelled, got %d", got)
}
if len(repo.updated) != 0 {
t.Errorf("expected no message updates, got %d", len(repo.updated))
}
}
// TestCanceller_PublishesMessageUpdatedEvent confirms the canceller publishes
// a message.updated event so the frontend refreshes the message in place.
func TestCanceller_PublishesMessageUpdatedEvent(t *testing.T) {
msgs := map[string]*taskmodels.Message{}
c, _, eventBus := newTestCanceller(t, msgs)
pendingID := c.store.CreateRequest(&Request{SessionID: "s1"})
msgs[pendingID] = &taskmodels.Message{
ID: "m1",
TaskSessionID: "s1",
Metadata: map[string]any{"status": "pending"},
}
c.CancelSessionAndNotify(context.Background(), "s1")
if len(eventBus.events) != 1 {
t.Fatalf("expected 1 event, got %d", len(eventBus.events))
}
}