detached 3217e93

test(protocol): cover malformed command boundaries

commit
3217e93d25233e0b85ffef4438046637ea0bc94c
author
brice · · 2026-08-28 14:19 UTC
parent
f08a628
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +44 -16
M internal/app/daemon_integration_test.go +44 -16
@@ -138,24 +138,52 @@ func killAll(dir string) error {
138138 }
139139
140140 func TestIntegration_MalformedCommandPreservesVersionAndRequestID(t *testing.T) {
141- dir, _ := startDaemon(t)
142- tr, err := ipc.DialContext(context.Background(), dir)
141+ incompatibleVersion := make([]byte, 10)
142+ binary.BigEndian.PutUint16(incompatibleVersion[:2], protocol.Version+1)
143+ binary.BigEndian.PutUint64(incompatibleVersion[2:], 42)
144+ valid, err := wire.MarshalCommandRequest(protocol.CommandRequest{
145+ Version: protocol.Version, RequestID: 43, Slug: "list-sessions",
146+ })
143147 require.NoError(t, err)
144- defer func() { _ = tr.Close() }()
145-
146- payload := make([]byte, 10)
147- binary.BigEndian.PutUint16(payload[:2], protocol.Version+1)
148- binary.BigEndian.PutUint64(payload[2:], 42)
149- require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgCommand, Payload: payload}))
150148
151- frame, err := tr.Recv()
152- require.NoError(t, err)
153- require.Equal(t, wire.MsgCommandResult, frame.Type)
154- result, err := wire.UnmarshalCommandResult(frame.Payload)
155- require.NoError(t, err)
156- require.False(t, result.OK)
157- require.Equal(t, uint64(42), result.RequestID)
158- require.Equal(t, protocol.ErrVersionMismatch, result.Code)
149+ tests := []struct {
150+ name string
151+ payload []byte
152+ want protocol.CommandResult
153+ }{
154+ {
155+ name: "incompatible version",
156+ payload: incompatibleVersion,
157+ want: protocol.CommandResult{RequestID: 42, Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"},
158+ },
159+ {
160+ name: "truncated version prefix",
161+ payload: []byte{0},
162+ want: protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"},
163+ },
164+ {
165+ name: "trailing garbage",
166+ payload: append(append([]byte(nil), valid...), 0xff),
167+ want: protocol.CommandResult{RequestID: 43, Code: protocol.ErrInternal, Text: "malformed command request"},
168+ },
169+ }
170+ for _, tt := range tests {
171+ t.Run(tt.name, func(t *testing.T) {
172+ dir, _ := startDaemon(t)
173+ tr, err := ipc.DialContext(context.Background(), dir)
174+ require.NoError(t, err)
175+ defer func() { _ = tr.Close() }()
176+
177+ require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgCommand, Payload: tt.payload}))
178+ frame, err := tr.Recv()
179+ require.NoError(t, err)
180+ require.Equal(t, wire.MsgCommandResult, frame.Type)
181+ require.Equal(t, wire.MarshalCommandResult(tt.want), frame.Payload)
182+ result, err := wire.UnmarshalCommandResult(frame.Payload)
183+ require.NoError(t, err)
184+ require.Equal(t, tt.want, result)
185+ })
186+ }
159187 }
160188
161189 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once
@@ -138,24 +138,52 @@ func killAll(dir string) error {
138 }138 }
139 139
140 func TestIntegration_MalformedCommandPreservesVersionAndRequestID(t *testing.T) {140 func TestIntegration_MalformedCommandPreservesVersionAndRequestID(t *testing.T) {
141- dir, _ := startDaemon(t)141+ incompatibleVersion := make([]byte, 10)
142- tr, err := ipc.DialContext(context.Background(), dir)142+ binary.BigEndian.PutUint16(incompatibleVersion[:2], protocol.Version+1)
143+ binary.BigEndian.PutUint64(incompatibleVersion[2:], 42)
144+ valid, err := wire.MarshalCommandRequest(protocol.CommandRequest{
145+ Version: protocol.Version, RequestID: 43, Slug: "list-sessions",
146+ })
143 require.NoError(t, err)147 require.NoError(t, err)
144- defer func() { _ = tr.Close() }()
145-
146- payload := make([]byte, 10)
147- binary.BigEndian.PutUint16(payload[:2], protocol.Version+1)
148- binary.BigEndian.PutUint64(payload[2:], 42)
149- require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgCommand, Payload: payload}))
150 148
151- frame, err := tr.Recv()149+ tests := []struct {
152- require.NoError(t, err)150+ name string
153- require.Equal(t, wire.MsgCommandResult, frame.Type)151+ payload []byte
154- result, err := wire.UnmarshalCommandResult(frame.Payload)152+ want protocol.CommandResult
155- require.NoError(t, err)153+ }{
156- require.False(t, result.OK)154+ {
157- require.Equal(t, uint64(42), result.RequestID)155+ name: "incompatible version",
158- require.Equal(t, protocol.ErrVersionMismatch, result.Code)156+ payload: incompatibleVersion,
157+ want: protocol.CommandResult{RequestID: 42, Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"},
158+ },
159+ {
160+ name: "truncated version prefix",
161+ payload: []byte{0},
162+ want: protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"},
163+ },
164+ {
165+ name: "trailing garbage",
166+ payload: append(append([]byte(nil), valid...), 0xff),
167+ want: protocol.CommandResult{RequestID: 43, Code: protocol.ErrInternal, Text: "malformed command request"},
168+ },
169+ }
170+ for _, tt := range tests {
171+ t.Run(tt.name, func(t *testing.T) {
172+ dir, _ := startDaemon(t)
173+ tr, err := ipc.DialContext(context.Background(), dir)
174+ require.NoError(t, err)
175+ defer func() { _ = tr.Close() }()
176+
177+ require.NoError(t, tr.Send(wire.Frame{Type: wire.MsgCommand, Payload: tt.payload}))
178+ frame, err := tr.Recv()
179+ require.NoError(t, err)
180+ require.Equal(t, wire.MsgCommandResult, frame.Type)
181+ require.Equal(t, wire.MarshalCommandResult(tt.want), frame.Payload)
182+ result, err := wire.UnmarshalCommandResult(frame.Payload)
183+ require.NoError(t, err)
184+ require.Equal(t, tt.want, result)
185+ })
186+ }
159 }187 }
160 188
161 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once189 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close