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/protocol"
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
53// TestMoveRejectionPreservesTransientPurgeAdmission proves a transient KillAll
54// admission rejection is not wrapped as a move rejection or leaked as an
55// internal detail: the command adapter keeps the typed sentinel it maps
56// retryably, and the palette adapter renders a retryable warning.
57func TestMoveRejectionPreservesTransientPurgeAdmission(t *testing.T) {
58 normalized := normalizeMoveRejection(errPurgeAdmissionClosed)
59 require.ErrorIs(t, normalized, errPurgeAdmissionClosed)
60 require.NotErrorIs(t, normalized, errMovePaneInvalid)
61 var rejection *moveRejection
62 require.False(t, errors.As(normalized, &rejection))
63
64 paletteErr := movePickerUserError(errPurgeAdmissionClosed)
65 var userErr *domain.UserError
66 require.ErrorAs(t, paletteErr, &userErr)
67 require.Equal(t, domain.NoticeSessionUnavailable, userErr.Code)
68 require.Equal(t, domain.NoticeWarn, userErr.Severity)
69 require.Equal(t, sessionKillRetryMessage, userErr.Msg)
70 require.ErrorIs(t, userErr.Err, errPurgeAdmissionClosed)
71}
72
73func TestMoveRejectionPresentationParity(t *testing.T) {
74 tests := []struct {
75 name string
76 err error
77 reason moveRejectionReason
78 paletteCode domain.NoticeCode
79 paletteSeverity domain.NoticeSeverity
80 paletteText string
81 commandText string
82 }{
83 {name: "no destination", err: errNoMoveDestination, reason: moveRejectionNoDestination, paletteCode: domain.NoticeSessionUnavailable, paletteSeverity: domain.NoticeWarn, paletteText: "No destination available.", commandText: "no move destination available"},
84 {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"},
85 {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"},
86 {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"},
87 {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"},
88 {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"},
89 {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"},
90 {name: "generic invalid", err: errMovePaneInvalid, reason: moveRejectionInvalid, paletteCode: domain.NoticeInternal, paletteSeverity: domain.NoticeError, paletteText: "Move failed.", commandText: "move request is invalid"},
91 }
92 for _, tt := range tests {
93 t.Run(tt.name, func(t *testing.T) {
94 descriptor, ok := moveRejectionDescriptorFor(tt.err)
95 require.True(t, ok)
96 require.Equal(t, tt.reason, descriptor.Reason)
97
98 paletteErr := movePickerUserError(tt.err)
99 var userErr *domain.UserError
100 require.ErrorAs(t, paletteErr, &userErr)
101 require.Equal(t, descriptor.PaletteCode, userErr.Code)
102 require.Equal(t, descriptor.PaletteSeverity, userErr.Severity)
103 require.Equal(t, descriptor.PaletteText, userErr.Msg)
104 require.Equal(t, tt.paletteCode, userErr.Code)
105 require.Equal(t, tt.paletteSeverity, userErr.Severity)
106 require.Equal(t, tt.paletteText, userErr.Msg)
107
108 result := moveCommandFailure(tt.err)
109 require.False(t, result.Outcome == protocol.CommandSucceeded)
110 require.Equal(t, descriptor.CommandCode, result.Code)
111 require.Equal(t, descriptor.CommandText, result.Text)
112 require.Equal(t, protocol.ErrNoSuchTarget, result.Code)
113 require.Equal(t, tt.commandText, result.Text)
114 })
115 }
116}