From 1485aa4de2e1b60110fbd0fb37a7c2bc142ec372 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:50:58 -0700 Subject: [PATCH] test(pty): pin terminal query-reply order; align one renderer call site (#16862) * fix(pty): preserve renderer query reply ordering * docs(pty): explain renderer query ordering --- ...tion-hidden-backlog-reconciliation.test.ts | 8 ++++-- .../pty-connection/live-data-callback.ts | 11 ++++---- .../terminal-capability-replies.test.ts | 28 +++++++++++++++++++ src/shared/terminal-query-reply.test.ts | 19 +++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts index d821de65fd1..c08315150ac 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-hidden-backlog-reconciliation.test.ts @@ -356,10 +356,12 @@ describe('connectPanePty', () => { await flushAsyncTicks(8) const replies = transport.sendInputImmediate.mock.calls.map((call) => String(call[0])) - expect(replies.some((reply) => reply.startsWith('\x1b]11;rgb:'))).toBe(true) // oxlint-disable-next-line no-control-regex -- the ESC byte IS the payload: this matches the CPR reply - expect(replies.some((reply) => /^\u001b\[\d+;\d+R$/.test(reply))).toBe(true) - expect(replies).toContain(DEFAULT_DA1_RESPONSE) + const cprReply = replies.find((reply) => /^\u001b\[\d+;\d+R$/.test(reply)) + const oscReply = replies.find((reply) => reply.startsWith('\x1b]11;rgb:')) + expect(oscReply).toBeDefined() + expect(cprReply).toBeDefined() + expect(replies).toEqual([oscReply, cprReply, DEFAULT_DA1_RESPONSE]) const written = writtenFloodData(pane) expect(written).not.toContain('aaaa') expect(written).not.toContain('bbbb') diff --git a/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts b/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts index 90a31924b98..ac50af346cd 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/live-data-callback.ts @@ -123,11 +123,7 @@ export function bindLiveDataCallback(session: ConnectPanePtySession): void { session.schedulePendingStartupCommandDelivery() return } - if (pendingForegroundQuery?.statelessQueryData) { - session.writePtyOutputToXterm(pendingForegroundQuery.statelessQueryData, true, { - hiddenStartupRendererQuery: true - }) - } + // Keep source order aligned with sibling producers; xterm's async write buffer made the old inversion latent. if (pendingForegroundQuery?.oscColorQueryData) { sendTerminalOscColorQueryReplies( pendingForegroundQuery.oscColorQueryData, @@ -136,6 +132,11 @@ export function bindLiveDataCallback(session: ConnectPanePtySession): void { session.sendDesktopQueryReplyImmediate ) } + if (pendingForegroundQuery?.statelessQueryData) { + session.writePtyOutputToXterm(pendingForegroundQuery.statelessQueryData, true, { + hiddenStartupRendererQuery: true + }) + } const restoreAppliesToCurrentPty = session.hiddenOutputRestorePtyId !== null && session.transport.getPtyId() === session.hiddenOutputRestorePtyId diff --git a/src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts b/src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts index 0cda3ca8120..3db71636bed 100644 --- a/src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-capability-replies.test.ts @@ -88,6 +88,34 @@ describe('installTerminalCapabilityReplyHandlers', () => { } }) + it.each([ + ['OSC 11 then CPR', '\x1b]11;?\x1b\\\x1b[6n', ['osc', 'cpr']], + ['CPR then OSC 11', '\x1b[6n\x1b]11;?\x1b\\', ['cpr', 'osc']] + ] as const)('preserves combined query order (%s)', async (_name, input, expectedKinds) => { + const term = new Terminal({ cols: 80, rows: 24, allowProposedApi: true }) + term.options.theme = { background: '#ffffff' } + const replies: string[] = [] + const disposable = installTerminalCapabilityReplyHandlers({ + terminal: term as never, + parser: term.parser, + sendInput: (data) => { + replies.push(data) + }, + isReplaying: () => false + }) + const onData = term.onData((data) => replies.push(data)) + + try { + await writeTerminal(term, input) + const kinds = replies.map((reply) => (reply.startsWith('\x1b]11;') ? 'osc' : 'cpr')) + expect(kinds).toEqual(expectedKinds) + } finally { + onData.dispose() + disposable.dispose() + term.dispose() + } + }) + it('answers OSC color queries for active rgba and modern rgb theme colors', async () => { const term = new Terminal({ cols: 80, rows: 24, allowProposedApi: true }) term.options.theme = { diff --git a/src/shared/terminal-query-reply.test.ts b/src/shared/terminal-query-reply.test.ts index 0e78bc83dca..980db641926 100644 --- a/src/shared/terminal-query-reply.test.ts +++ b/src/shared/terminal-query-reply.test.ts @@ -176,6 +176,25 @@ describe('query reply ordering (termenv OSC-then-CPR)', () => { vi.useRealTimers() }) + it('preserves reverse query order when CPR arrives before the color query', async () => { + vi.useFakeTimers() + const pty: string[] = [] + const ingress = new PtyStartupIngress({ + ownerBackend: 'posix-pty', + write: (data) => pty.push(data), + onEmission: () => {} + }) + const write = hostWrites(ingress, pty) + + write(CPR_REPLY) + write(OSC_11_REPLY) + await vi.advanceTimersByTimeAsync(200) + + expect(pty).toEqual([CPR_REPLY, OSC_11_REPLY]) + ingress.drainAndClose() + vi.useRealTimers() + }) + it('keeps a CPR immediate when no color reply is deferred', () => { vi.useFakeTimers() const pty: string[] = []