1package daemon
2
3import (
4 "errors"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/usecase/layout"
12)
13
14func TestMoveRejectionCategoriesPreserveLegacyIdentity(t *testing.T) {
15 tests := []struct {
16 name string
17 err error
18 reason moveRejectionReason
19 legacy error
20 }{
21 {name: "no destination", err: errNoMoveDestination, reason: moveRejectionNoDestination},
22 {name: "warming", err: errMoveFloatingWarming, reason: moveRejectionFloatingWarming},
23 {name: "installed final source floating", err: errMoveFinalSourceFloating, reason: moveRejectionFinalSourceFloating, legacy: errMovePaneFloatingSibling},
24 {name: "stale target", err: errMoveStaleTarget, reason: moveRejectionStaleTarget, legacy: errMovePaneInvalid},
25 {name: "too small", err: errMoveTooSmall, reason: moveRejectionTooSmall, legacy: layout.ErrTooSmall},
26 {name: "pane ID exhaustion", err: errMovePaneIDExhausted, reason: moveRejectionPaneIDExhausted},
27 {name: "generic invalid", err: errMovePaneInvalid, reason: moveRejectionInvalid},
28 }
29 for _, tt := range tests {
30 t.Run(tt.name, func(t *testing.T) {
31 var rejection *moveRejection
32 require.ErrorAs(t, tt.err, &rejection)
33 require.Equal(t, tt.reason, rejection.Reason)
34 if tt.legacy != nil {
35 require.True(t, errors.Is(tt.err, tt.legacy))
36 }
37 })
38 }
39 require.NotErrorIs(t, errMoveFloatingWarming, errMoveFinalSourceFloating)
40}
41
42func TestNormalizeMoveRejectionDoesNotLeakInternalErrors(t *testing.T) {
43 internal := errors.New("layout implementation detail")
44 got := normalizeMoveRejection(internal)
45 var rejection *moveRejection
46 require.ErrorAs(t, got, &rejection)
47 require.Equal(t, moveRejectionInvalid, rejection.Reason)
48 require.Equal(t, "move request is invalid", got.Error())
49 require.ErrorIs(t, got, internal)
50 require.ErrorIs(t, got, errMovePaneInvalid)
51}
52
53func TestMoveRejectionPresentationParity(t *testing.T) {
54 tests := []struct {
55 name string
56 err error
57 reason moveRejectionReason
58 paletteCode domain.NoticeCode
59 paletteSeverity domain.NoticeSeverity
60 paletteText string
61 commandText string
62 }{
63 {name: "no destination", err: errNoMoveDestination, reason: moveRejectionNoDestination, paletteCode: domain.NoticeSessionUnavailable, paletteSeverity: domain.NoticeWarn, paletteText: "No destination available.", commandText: "no move destination available"},
64 {name: "warming", err: errMoveFloatingWarming, reason: moveRejectionFloatingWarming, paletteCode: domain.NoticeSessionUnavailable, paletteSeverity: domain.NoticeWarn, paletteText: "Wait for the floating pane to finish opening.", commandText: "wait for the floating pane to finish opening"},
65 {name: "installed final source floating", err: errMoveFinalSourceFloating, reason: moveRejectionFinalSourceFloating, paletteCode: domain.NoticeLayoutTooSmall, paletteSeverity: domain.NoticeWarn, paletteText: "Close the floating pane or move the whole tab.", commandText: "close the floating pane or move the whole tab"},
66 {name: "too small", err: errMoveTooSmall, reason: moveRejectionTooSmall, paletteCode: domain.NoticeLayoutTooSmall, paletteSeverity: domain.NoticeWarn, paletteText: "Not enough space in destination tab.", commandText: "not enough space in destination tab"},
67 {name: "stale", err: errMoveStaleTarget, reason: moveRejectionStaleTarget, paletteCode: domain.NoticeSessionUnavailable, paletteSeverity: domain.NoticeWarn, paletteText: "Destination is no longer available.", commandText: "move target is no longer available"},
68 {name: "lifecycle target", err: errMoveLifecycleUnavailable, reason: moveRejectionStaleTarget, paletteCode: domain.NoticeSessionUnavailable, paletteSeverity: domain.NoticeWarn, paletteText: "Destination is no longer available.", commandText: "move target is no longer available"},
69 {name: "pane ID exhaustion", err: errMovePaneIDExhausted, reason: moveRejectionPaneIDExhausted, paletteCode: domain.NoticeInternal, paletteSeverity: domain.NoticeError, paletteText: "Destination pane IDs are exhausted.", commandText: "destination pane IDs are exhausted"},
70 {name: "generic invalid", err: errMovePaneInvalid, reason: moveRejectionInvalid, paletteCode: domain.NoticeInternal, paletteSeverity: domain.NoticeError, paletteText: "Move failed.", commandText: "move request is invalid"},
71 }
72 for _, tt := range tests {
73 t.Run(tt.name, func(t *testing.T) {
74 descriptor, ok := moveRejectionDescriptorFor(tt.err)
75 require.True(t, ok)
76 require.Equal(t, tt.reason, descriptor.Reason)
77
78 paletteErr := movePickerUserError(tt.err)
79 var userErr *domain.UserError
80 require.ErrorAs(t, paletteErr, &userErr)
81 require.Equal(t, descriptor.PaletteCode, userErr.Code)
82 require.Equal(t, descriptor.PaletteSeverity, userErr.Severity)
83 require.Equal(t, descriptor.PaletteText, userErr.Msg)
84 require.Equal(t, tt.paletteCode, userErr.Code)
85 require.Equal(t, tt.paletteSeverity, userErr.Severity)
86 require.Equal(t, tt.paletteText, userErr.Msg)
87
88 result := moveCommandFailure(tt.err)
89 require.False(t, result.OK)
90 require.Equal(t, descriptor.CommandCode, result.Code)
91 require.Equal(t, descriptor.CommandText, result.Text)
92 require.Equal(t, ports.ErrNoSuchTarget, result.Code)
93 require.Equal(t, tt.commandText, result.Text)
94 })
95 }
96}