Files
kandev/apps/backend/internal/office/dashboard/session_termination_test.go
T
2026-05-16 20:07:57 +01:00

107 lines
3.7 KiB
Go

package dashboard_test
import (
"context"
"testing"
"github.com/kandev/kandev/internal/office/dashboard"
)
// recordingTerminator captures TerminateOfficeSession calls so tests can
// verify the dashboard service forwards reassignments / participant removals.
type recordingTerminator struct {
calls []termCall
}
type termCall struct {
taskID, agentID, reason string
}
func (r *recordingTerminator) TerminateOfficeSession(_ context.Context, taskID, agentID, reason string) error {
r.calls = append(r.calls, termCall{taskID: taskID, agentID: agentID, reason: reason})
return nil
}
func TestRemoveTaskReviewer_TerminatesSession(t *testing.T) {
deps := newTestDeps(t)
rt := &recordingTerminator{}
deps.svc.SetSessionTerminator(rt)
insertTestTask(t, deps.db, "task-rev", "ws-1", "Review", "todo", 2)
if err := deps.svc.AddTaskReviewer(context.Background(), "", "task-rev", "agent-rev"); err != nil {
t.Fatalf("add reviewer: %v", err)
}
if len(rt.calls) != 0 {
t.Errorf("add must not terminate, got %d calls", len(rt.calls))
}
if err := deps.svc.RemoveTaskReviewer(context.Background(), "", "task-rev", "agent-rev"); err != nil {
t.Fatalf("remove reviewer: %v", err)
}
if len(rt.calls) != 1 {
t.Fatalf("expected 1 terminate call, got %d", len(rt.calls))
}
got := rt.calls[0]
if got.taskID != "task-rev" || got.agentID != "agent-rev" {
t.Errorf("term call: got %+v", got)
}
}
// TestSetSessionTerminator_NilTolerated keeps the wiring optional: when no
// terminator is registered, removal still succeeds (dashboard tests run
// without the orchestrator wired in).
func TestSetSessionTerminator_NilTolerated(t *testing.T) {
deps := newTestDeps(t)
insertTestTask(t, deps.db, "task-nil", "ws-1", "Nil", "todo", 2)
if err := deps.svc.AddTaskReviewer(context.Background(), "", "task-nil", "agent-x"); err != nil {
t.Fatalf("add: %v", err)
}
if err := deps.svc.RemoveTaskReviewer(context.Background(), "", "task-nil", "agent-x"); err != nil {
t.Fatalf("remove without terminator: %v", err)
}
}
// staticDashboardSessionTerminator is the test-side type alias used to
// cross-check the interface satisfaction at compile time.
var _ dashboard.SessionTerminator = (*recordingTerminator)(nil)
// recordingReactivity is a stub ReactivityApplier that returns a fixed result
// so the dashboard's runReactivityForAssigneeChange path runs end-to-end in
// tests without bringing the scheduler into the picture.
type recordingReactivity struct {
result *dashboard.TaskReactivityResult
err error
}
func (r *recordingReactivity) ApplyTaskMutation(_ context.Context, _ string, _ string, _ dashboard.TaskReactivityChange) (*dashboard.TaskReactivityResult, error) {
return r.result, r.err
}
// TestSetTaskAssignee_TerminatesPrevSession exercises B5a: changing the
// assignee fires the terminator on the previous agent's (task, agent) pair.
func TestSetTaskAssignee_TerminatesPrevSession(t *testing.T) {
deps := newTestDeps(t)
rt := &recordingTerminator{}
deps.svc.SetSessionTerminator(rt)
deps.svc.SetReactivityApplier(&recordingReactivity{result: &dashboard.TaskReactivityResult{}})
insertTestTask(t, deps.db, "task-r", "ws-r", "Reassign", "todo", 2)
// Seed prev assignee directly via the underlying repo update.
if err := deps.repo.UpdateTaskAssignee(context.Background(), "task-r", "agent-prev"); err != nil {
t.Fatalf("seed prev assignee: %v", err)
}
if err := deps.svc.SetTaskAssigneeAsAgent(context.Background(), "", "task-r", "agent-new"); err != nil {
t.Fatalf("set assignee: %v", err)
}
if len(rt.calls) != 1 {
t.Fatalf("expected 1 terminate call, got %d (%+v)", len(rt.calls), rt.calls)
}
got := rt.calls[0]
if got.taskID != "task-r" || got.agentID != "agent-prev" {
t.Errorf("term call: got %+v", got)
}
}