test(app): cover command palette integration
- commit
- 46fa9b3160128786799336efe5795f90b7303b3c
- author
- brice · · 2026-07-03 05:47 UTC
- committer
- brice · · 2026-07-03 07:35 UTC
- parent
- e431dff
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +61 -2
M internal/app/daemon_integration_test.go +61 -2
| @@ -86,6 +86,13 @@ func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) | |||
| 86 | 86 | // awaitText decodes MsgOutput frames into a fresh VT screen and returns once | |
| 87 | 87 | // the reconstructed grid contains want. | |
| 88 | 88 | func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | |
| 89 | + | t.Helper() | |
| 90 | + | _ = awaitScreenText(t, p, sz, want) | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | // awaitScreenText is like awaitText, but returns the reconstructed screen text | |
| 94 | + | // at the point the wanted text appears so callers can make additional checks. | |
| 95 | + | func awaitScreenText(t *testing.T, p *pump, sz domain.Size, want string) string { | |
| 89 | 96 | t.Helper() | |
| 90 | 97 | screen := vt.NewScreen(sz.Cols, sz.Rows) | |
| 91 | 98 | timeout := time.After(5 * time.Second) | |
| @@ -99,8 +106,9 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | |||
| 99 | 106 | o, err := ports.UnmarshalOutput(f.Payload) | |
| 100 | 107 | require.NoError(t, err) | |
| 101 | 108 | screen.Write(o.Data) | |
| 102 | - | if strings.Contains(screenText(screen), want) { | |
| 103 | - | return | |
| 109 | + | text := screenText(screen) | |
| 110 | + | if strings.Contains(text, want) { | |
| 111 | + | return text | |
| 104 | 112 | } | |
| 105 | 113 | } | |
| 106 | 114 | case <-timeout: | |
| @@ -109,6 +117,28 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | |||
| 109 | 117 | } | |
| 110 | 118 | } | |
| 111 | 119 | ||
| 120 | + | func assertNoTextAfterInput(t *testing.T, p *pump, sz domain.Size, absent string) { | |
| 121 | + | t.Helper() | |
| 122 | + | screen := vt.NewScreen(sz.Cols, sz.Rows) | |
| 123 | + | timeout := time.After(300 * time.Millisecond) | |
| 124 | + | for { | |
| 125 | + | select { | |
| 126 | + | case f, ok := <-p.ch: | |
| 127 | + | if !ok { | |
| 128 | + | return | |
| 129 | + | } | |
| 130 | + | if f.Type == ports.MsgOutput { | |
| 131 | + | o, err := ports.UnmarshalOutput(f.Payload) | |
| 132 | + | require.NoError(t, err) | |
| 133 | + | screen.Write(o.Data) | |
| 134 | + | require.NotContains(t, screenText(screen), absent) | |
| 135 | + | } | |
| 136 | + | case <-timeout: | |
| 137 | + | return | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 112 | 142 | func screenText(s *vt.Screen) string { | |
| 113 | 143 | var b strings.Builder | |
| 114 | 144 | for y := range s.Frame.Height { | |
| @@ -144,6 +174,35 @@ func TestIntegration_InputRoundtrip(t *testing.T) { | |||
| 144 | 174 | awaitText(t, p, sz, "PINGPONG") | |
| 145 | 175 | } | |
| 146 | 176 | ||
| 177 | + | func TestIntegration_CommandPaletteCreatesTab(t *testing.T) { | |
| 178 | + | sz := domain.Size{Cols: 80, Rows: 24} | |
| 179 | + | dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | |
| 180 | + | ||
| 181 | + | tr, p := attach(t, dir, ports.IntentEphemeral, "", sz) | |
| 182 | + | defer func() { _ = tr.Close() }() | |
| 183 | + | awaitText(t, p, sz, "READY") | |
| 184 | + | ||
| 185 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})})) | |
| 186 | + | awaitText(t, p, sz, "Commands") | |
| 187 | + | ||
| 188 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})})) | |
| 189 | + | text := awaitScreenText(t, p, sz, " 1 2 ") | |
| 190 | + | require.Contains(t, text, " 1 2 ") | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) { | |
| 194 | + | sz := domain.Size{Cols: 80, Rows: 24} | |
| 195 | + | dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | |
| 196 | + | ||
| 197 | + | tr, p := attach(t, dir, ports.IntentEphemeral, "", sz) | |
| 198 | + | defer func() { _ = tr.Close() }() | |
| 199 | + | text := awaitScreenText(t, p, sz, "READY") | |
| 200 | + | require.NotContains(t, text, " 1 2 ") | |
| 201 | + | ||
| 202 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})})) | |
| 203 | + | assertNoTextAfterInput(t, p, sz, " 1 2 ") | |
| 204 | + | } | |
| 205 | + | ||
| 147 | 206 | func TestIntegration_EphemeralGoneOnDetach(t *testing.T) { | |
| 148 | 207 | sz := domain.Size{Cols: 80, Rows: 24} | |
| 149 | 208 | dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | |
| @@ -86,6 +86,13 @@ func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) | |||||
| 86 | // awaitText decodes MsgOutput frames into a fresh VT screen and returns once | 86 | // awaitText decodes MsgOutput frames into a fresh VT screen and returns once | ||
| 87 | // the reconstructed grid contains want. | 87 | // the reconstructed grid contains want. | ||
| 88 | func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | 88 | func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | ||
| 89 | + | t.Helper() | |||
| 90 | + | _ = awaitScreenText(t, p, sz, want) | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | // awaitScreenText is like awaitText, but returns the reconstructed screen text | |||
| 94 | + | // at the point the wanted text appears so callers can make additional checks. | |||
| 95 | + | func awaitScreenText(t *testing.T, p *pump, sz domain.Size, want string) string { | |||
| 89 | t.Helper() | 96 | t.Helper() | ||
| 90 | screen := vt.NewScreen(sz.Cols, sz.Rows) | 97 | screen := vt.NewScreen(sz.Cols, sz.Rows) | ||
| 91 | timeout := time.After(5 * time.Second) | 98 | timeout := time.After(5 * time.Second) | ||
| @@ -99,8 +106,9 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | |||||
| 99 | o, err := ports.UnmarshalOutput(f.Payload) | 106 | o, err := ports.UnmarshalOutput(f.Payload) | ||
| 100 | require.NoError(t, err) | 107 | require.NoError(t, err) | ||
| 101 | screen.Write(o.Data) | 108 | screen.Write(o.Data) | ||
| 102 | - | if strings.Contains(screenText(screen), want) { | 109 | + | text := screenText(screen) |
| 103 | - | return | 110 | + | if strings.Contains(text, want) { |
| 111 | + | return text | |||
| 104 | } | 112 | } | ||
| 105 | } | 113 | } | ||
| 106 | case <-timeout: | 114 | case <-timeout: | ||
| @@ -109,6 +117,28 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) { | |||||
| 109 | } | 117 | } | ||
| 110 | } | 118 | } | ||
| 111 | 119 | ||||
| 120 | + | func assertNoTextAfterInput(t *testing.T, p *pump, sz domain.Size, absent string) { | |||
| 121 | + | t.Helper() | |||
| 122 | + | screen := vt.NewScreen(sz.Cols, sz.Rows) | |||
| 123 | + | timeout := time.After(300 * time.Millisecond) | |||
| 124 | + | for { | |||
| 125 | + | select { | |||
| 126 | + | case f, ok := <-p.ch: | |||
| 127 | + | if !ok { | |||
| 128 | + | return | |||
| 129 | + | } | |||
| 130 | + | if f.Type == ports.MsgOutput { | |||
| 131 | + | o, err := ports.UnmarshalOutput(f.Payload) | |||
| 132 | + | require.NoError(t, err) | |||
| 133 | + | screen.Write(o.Data) | |||
| 134 | + | require.NotContains(t, screenText(screen), absent) | |||
| 135 | + | } | |||
| 136 | + | case <-timeout: | |||
| 137 | + | return | |||
| 138 | + | } | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 112 | func screenText(s *vt.Screen) string { | 142 | func screenText(s *vt.Screen) string { | ||
| 113 | var b strings.Builder | 143 | var b strings.Builder | ||
| 114 | for y := range s.Frame.Height { | 144 | for y := range s.Frame.Height { | ||
| @@ -144,6 +174,35 @@ func TestIntegration_InputRoundtrip(t *testing.T) { | |||||
| 144 | awaitText(t, p, sz, "PINGPONG") | 174 | awaitText(t, p, sz, "PINGPONG") | ||
| 145 | } | 175 | } | ||
| 146 | 176 | ||||
| 177 | + | func TestIntegration_CommandPaletteCreatesTab(t *testing.T) { | |||
| 178 | + | sz := domain.Size{Cols: 80, Rows: 24} | |||
| 179 | + | dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | |||
| 180 | + | ||||
| 181 | + | tr, p := attach(t, dir, ports.IntentEphemeral, "", sz) | |||
| 182 | + | defer func() { _ = tr.Close() }() | |||
| 183 | + | awaitText(t, p, sz, "READY") | |||
| 184 | + | ||||
| 185 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})})) | |||
| 186 | + | awaitText(t, p, sz, "Commands") | |||
| 187 | + | ||||
| 188 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})})) | |||
| 189 | + | text := awaitScreenText(t, p, sz, " 1 2 ") | |||
| 190 | + | require.Contains(t, text, " 1 2 ") | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) { | |||
| 194 | + | sz := domain.Size{Cols: 80, Rows: 24} | |||
| 195 | + | dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | |||
| 196 | + | ||||
| 197 | + | tr, p := attach(t, dir, ports.IntentEphemeral, "", sz) | |||
| 198 | + | defer func() { _ = tr.Close() }() | |||
| 199 | + | text := awaitScreenText(t, p, sz, "READY") | |||
| 200 | + | require.NotContains(t, text, " 1 2 ") | |||
| 201 | + | ||||
| 202 | + | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})})) | |||
| 203 | + | assertNoTextAfterInput(t, p, sz, " 1 2 ") | |||
| 204 | + | } | |||
| 205 | + | ||||
| 147 | func TestIntegration_EphemeralGoneOnDetach(t *testing.T) { | 206 | func TestIntegration_EphemeralGoneOnDetach(t *testing.T) { | ||
| 148 | sz := domain.Size{Cols: 80, Rows: 24} | 207 | sz := domain.Size{Cols: 80, Rows: 24} | ||
| 149 | dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | 208 | dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"})) | ||