diff --git a/workspace-server/internal/handlers/workspace_dispatchers_test.go b/workspace-server/internal/handlers/workspace_dispatchers_test.go new file mode 100644 index 00000000..60380afc --- /dev/null +++ b/workspace-server/internal/handlers/workspace_dispatchers_test.go @@ -0,0 +1,164 @@ +package handlers + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/Molecule-AI/molecule-monorepo/platform/internal/models" +) + +// ==================== resolveDeliveryMode ==================== +// Covers workspace_dispatchers.go / registry.go:resolveDeliveryMode + +func TestResolveDeliveryMode_PayloadModeWins(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + ctx := context.Background() + for _, mode := range []string{models.DeliveryModePush, models.DeliveryModePoll} { + got, err := h.resolveDeliveryMode(ctx, "ws-any-id", mode) + if err != nil { + t.Errorf("resolveDeliveryMode(payloadMode=%q) unexpected error: %v", mode, err) + } + if got != mode { + t.Errorf("resolveDeliveryMode(payloadMode=%q) = %q, want %q", mode, got, mode) + } + } + + // DB must NOT have been queried when payloadMode is set. + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("DB expectations not met: %v", err) + } +} + +func TestResolveDeliveryMode_ExistingDeliveryMode(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + // Workspace row has existing delivery_mode = "poll" + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-poll"). + WillReturnRows(sqlmock.NewRows([]string{"delivery_mode", "runtime"}). + AddRow("poll", "langgraph")) + + ctx := context.Background() + got, err := h.resolveDeliveryMode(ctx, "ws-poll", "") + if err != nil { + t.Errorf("resolveDeliveryMode() unexpected error: %v", err) + } + if got != models.DeliveryModePoll { + t.Errorf("resolveDeliveryMode() = %q, want %q", got, models.DeliveryModePoll) + } +} + +func TestResolveDeliveryMode_ExternalRuntime_DefaultsToPoll(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + // Row exists but delivery_mode is NULL; runtime = "external" + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-external"). + WillReturnRows(sqlmock.NewRows([]string{"delivery_mode", "runtime"}). + AddRow(nil, "external")) + + ctx := context.Background() + got, err := h.resolveDeliveryMode(ctx, "ws-external", "") + if err != nil { + t.Errorf("resolveDeliveryMode() unexpected error: %v", err) + } + if got != models.DeliveryModePoll { + t.Errorf("resolveDeliveryMode() = %q, want %q (external runtime)", got, models.DeliveryModePoll) + } +} + +func TestResolveDeliveryMode_SelfHosted_DefaultsToPush(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + // Row exists; delivery_mode is NULL; runtime = "langgraph" + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-self-hosted"). + WillReturnRows(sqlmock.NewRows([]string{"delivery_mode", "runtime"}). + AddRow(nil, "langgraph")) + + ctx := context.Background() + got, err := h.resolveDeliveryMode(ctx, "ws-self-hosted", "") + if err != nil { + t.Errorf("resolveDeliveryMode() unexpected error: %v", err) + } + if got != models.DeliveryModePush { + t.Errorf("resolveDeliveryMode() = %q, want %q (self-hosted default)", got, models.DeliveryModePush) + } +} + +func TestResolveDeliveryMode_NotFound_DefaultsToPush(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + // Row not found → sql.ErrNoRows → default push + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-nonexistent"). + WillReturnError(sql.ErrNoRows) + + ctx := context.Background() + got, err := h.resolveDeliveryMode(ctx, "ws-nonexistent", "") + if err != nil { + t.Errorf("resolveDeliveryMode() unexpected error on no-rows: %v", err) + } + if got != models.DeliveryModePush { + t.Errorf("resolveDeliveryMode() = %q, want %q (not-found default)", got, models.DeliveryModePush) + } +} + +func TestResolveDeliveryMode_DBError_Propagated(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-error"). + WillReturnError(context.DeadlineExceeded) + + ctx := context.Background() + _, err := h.resolveDeliveryMode(ctx, "ws-error", "") + if err == nil { + t.Errorf("resolveDeliveryMode() expected error, got nil") + } +} + +func TestResolveDeliveryMode_ExistingDeliveryModeEmptyString(t *testing.T) { + // When the DB returns an empty (non-NULL) string for delivery_mode, + // it falls through to the runtime check (not the existing.Valid path). + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + h := NewRegistryHandler(broadcaster) + + // delivery_mode is explicitly empty string (not NULL), runtime = "langgraph" + // → falls through to runtime check → "push" for non-external + mock.ExpectQuery("SELECT delivery_mode, runtime FROM workspaces"). + WithArgs("ws-empty-mode"). + WillReturnRows(sqlmock.NewRows([]string{"delivery_mode", "runtime"}). + AddRow("", "langgraph")) + + ctx := context.Background() + got, err := h.resolveDeliveryMode(ctx, "ws-empty-mode", "") + if err != nil { + t.Errorf("resolveDeliveryMode() unexpected error: %v", err) + } + if got != models.DeliveryModePush { + t.Errorf("resolveDeliveryMode() = %q, want %q", got, models.DeliveryModePush) + } +} diff --git a/workspace-server/internal/models/workspace_delivery_mode_test.go b/workspace-server/internal/models/workspace_delivery_mode_test.go new file mode 100644 index 00000000..0b8a2dc4 --- /dev/null +++ b/workspace-server/internal/models/workspace_delivery_mode_test.go @@ -0,0 +1,100 @@ +package models + +import "testing" + +// ==================== IsValidDeliveryMode ==================== + +func TestIsValidDeliveryMode_Valid(t *testing.T) { + for _, mode := range []string{DeliveryModePush, DeliveryModePoll} { + if !IsValidDeliveryMode(mode) { + t.Errorf("IsValidDeliveryMode(%q) = false, want true", mode) + } + } +} + +func TestIsValidDeliveryMode_Invalid(t *testing.T) { + cases := []struct { + val string + want bool + }{ + {"", false}, // empty string is not valid — callers must resolve the default + {"pushx", false}, // typo + {"pollx", false}, // typo + {"PUSH", false}, // case-sensitive + {"PUSH ", false}, // trailing space + {"push ", false}, // trailing space + {"hybrid", false}, // non-existent mode + {"poll ", false}, // trailing space + } + for _, tc := range cases { + got := IsValidDeliveryMode(tc.val) + if got != tc.want { + t.Errorf("IsValidDeliveryMode(%q) = %v, want %v", tc.val, got, tc.want) + } + } +} + +// ==================== WorkspaceStatus ==================== + +func TestWorkspaceStatus_String(t *testing.T) { + statuses := []WorkspaceStatus{ + StatusProvisioning, + StatusOnline, + StatusOffline, + StatusDegraded, + StatusFailed, + StatusRemoved, + StatusPaused, + StatusHibernated, + StatusHibernating, + StatusAwaitingAgent, + } + for _, s := range statuses { + if got := s.String(); got != string(s) { + t.Errorf("WorkspaceStatus(%q).String() = %q, want %q", s, got, string(s)) + } + } +} + +func TestAllWorkspaceStatuses_Length(t *testing.T) { + // The const block has 10 statuses; AllWorkspaceStatuses must match. + if got := len(AllWorkspaceStatuses); got != 10 { + t.Errorf("len(AllWorkspaceStatuses) = %d, want 10", got) + } +} + +func TestAllWorkspaceStatuses_ContainsAllNamed(t *testing.T) { + // Verify every named const appears in AllWorkspaceStatuses exactly once. + named := []WorkspaceStatus{ + StatusProvisioning, + StatusOnline, + StatusOffline, + StatusDegraded, + StatusFailed, + StatusRemoved, + StatusPaused, + StatusHibernated, + StatusHibernating, + StatusAwaitingAgent, + } + set := make(map[WorkspaceStatus]bool, len(AllWorkspaceStatuses)) + for _, s := range AllWorkspaceStatuses { + set[s] = true + } + for _, s := range named { + if !set[s] { + t.Errorf("named status %q missing from AllWorkspaceStatuses", s) + } + } + if len(set) != len(named) { + t.Errorf("AllWorkspaceStatuses has %d unique entries, want %d", len(set), len(named)) + } +} + +func TestAllWorkspaceStatuses_NoEmpty(t *testing.T) { + for _, s := range AllWorkspaceStatuses { + if s == "" { + t.Errorf("AllWorkspaceStatuses contains empty string") + } + } +}