|
| @@ -257,21 +257,23 @@ func TestPTYReaderSyncVisibilityTransitions(t *testing.T) { |
| 257 | | func TestPTYReaderRestoresPrimaryScreenImmediatelyAfterDEC1049Exit(t *testing.T) { | 257 | | func TestPTYReaderRestoresPrimaryScreenImmediatelyAfterDEC1049Exit(t *testing.T) { |
| 258 | | for _, tc := range []struct { | 258 | | for _, tc := range []struct { |
| 259 | | name string | 259 | | name string |
| | | 260 | + | window uint64 |
| 260 | | exitBeforeACK bool | 261 | | exitBeforeACK bool |
| 261 | | exitSuffix string | 262 | | exitSuffix string |
| 262 | | overlay bool | 263 | | overlay bool |
| 263 | | }{ | 264 | | }{ |
| 264 | - | {name: "exit waits for local ACK"}, | 265 | + | {name: "datagram output window (1): exit waits for ACK", window: 1}, |
| 265 | - | {name: "exit arrives before local ACK", exitBeforeACK: true}, | 266 | + | {name: "datagram output window (1): exit and prompt arrive before ACK", window: 1, exitBeforeACK: true, exitSuffix: "PROMPT> "}, |
| 266 | - | {name: "prompt bytes follow exit before local ACK", exitBeforeACK: true, exitSuffix: "PROMPT> "}, | 267 | + | {name: "local default output window (8): exit waits for ACK", window: maxUnackedOutputStates}, |
| 267 | - | {name: "palette overlay with bars", overlay: true}, | 268 | + | {name: "local default output window (8): exit and prompt arrive before ACK", window: maxUnackedOutputStates, exitBeforeACK: true, exitSuffix: "PROMPT> "}, |
| | | 269 | + | {name: "local default output window (8): palette overlay with bars", window: maxUnackedOutputStates, overlay: true}, |
| 268 | | } { | 270 | | } { |
| 269 | | t.Run(tc.name, func(t *testing.T) { | 271 | | t.Run(tc.name, func(t *testing.T) { |
| 270 | | pty, steps, processed := newChannelPTY(t) | 272 | | pty, steps, processed := newChannelPTY(t) |
| 271 | | d, sess, ac, sends := newManualSessionWithPTYs(t, pty) | 273 | | d, sess, ac, sends := newManualSessionWithPTYs(t, pty) |
| 272 | - | // A one-frame local output window forces the replay to exercise both | 274 | + | // Datagram clients use a one-frame window; local clients retain the |
| 273 | - | // ACK orderings without relying on elapsed time. | 275 | + | // default eight-frame window. Both ACK orderings stay deterministic. |
| 274 | - | ac.output.maxOutstanding = 1 | 276 | + | ac.output.maxOutstanding = tc.window |
| 275 | | clock := newCoordinatorMockClock(t, 8) | 277 | | clock := newCoordinatorMockClock(t, 8) |
| 276 | | d.clock = clock.clock | 278 | | d.clock = clock.clock |
| 277 | | rc := d.attachCoordinator(sess, nil, ac, true) | 279 | | rc := d.attachCoordinator(sess, nil, ac, true) |
|
| @@ -315,19 +317,28 @@ func TestPTYReaderRestoresPrimaryScreenImmediatelyAfterDEC1049Exit(t *testing.T) |
| 315 | | pane.mu.Unlock() | 317 | | pane.mu.Unlock() |
| 316 | | | 318 | | |
| 317 | | exitData := []byte("\x1b[?1049l" + tc.exitSuffix) | 319 | | exitData := []byte("\x1b[?1049l" + tc.exitSuffix) |
| | | 320 | + | var exit ports.Output |
| 318 | | if tc.exitBeforeACK { | 321 | | if tc.exitBeforeACK { |
| 319 | | steps <- channelPTYStep{data: exitData} | 322 | | steps <- channelPTYStep{data: exitData} |
| 320 | | awaitPTYReadProcessed(t, processed) | 323 | | awaitPTYReadProcessed(t, processed) |
| 321 | | fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), minOutputRenderDeadline) | 324 | | fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), minOutputRenderDeadline) |
| 322 | - | requireNoCoordinatorOutputFrame(t, sends) | 325 | + | if tc.window == 1 { |
| 323 | - | acknowledge(alternate) | 326 | + | requireNoCoordinatorOutputFrame(t, sends) |
| | | 327 | + | acknowledge(alternate) |
| | | 328 | + | exit = mustApplyOutput(t, client, awaitOutputFrameWithoutSleep(t, sends)) |
| | | 329 | + | } else { |
| | | 330 | + | exit = mustApplyOutput(t, client, awaitOutputFrameWithoutSleep(t, sends)) |
| | | 331 | + | require.Equal(t, alternate.NewStateNum, exit.BaseStateNum, |
| | | 332 | + | "local exit must be rendered before the alternate-frame ACK") |
| | | 333 | + | acknowledge(alternate) |
| | | 334 | + | } |
| 324 | | } else { | 335 | | } else { |
| 325 | | acknowledge(alternate) | 336 | | acknowledge(alternate) |
| 326 | | steps <- channelPTYStep{data: exitData} | 337 | | steps <- channelPTYStep{data: exitData} |
| 327 | | awaitPTYReadProcessed(t, processed) | 338 | | awaitPTYReadProcessed(t, processed) |
| 328 | | fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), minOutputRenderDeadline) | 339 | | fireCoordinatorTimer(t, rc, drainCoordinatorTimers(clock), minOutputRenderDeadline) |
| | | 340 | + | exit = mustApplyOutput(t, client, awaitOutputFrameWithoutSleep(t, sends)) |
| 329 | | } | 341 | | } |
| 330 | - | exit := mustApplyOutput(t, client, awaitOutputFrameWithoutSleep(t, sends)) | | | |
| 331 | | require.NotEmpty(t, exit.Data, "the first post-exit output must actively remove alternate cells") | 342 | | require.NotEmpty(t, exit.Data, "the first post-exit output must actively remove alternate cells") |
| 332 | | rendered := strings.Join(frameRows(client.Frame), "\n") | 343 | | rendered := strings.Join(frameRows(client.Frame), "\n") |
| 333 | | require.NotContains(t, rendered, "MATRIX", "the first post-exit frame must not retain alternate cells") | 344 | | require.NotContains(t, rendered, "MATRIX", "the first post-exit frame must not retain alternate cells") |