test(terminal): pin the carried renderer risk scan tail

The foreground renderer-risk scan splices the carried tail onto the
incoming chunk before classifying it, and nothing covered that ordering:
a pre-gate moved back above the concatenation would silently drop the
refresh for a background SGR split across ConPTY chunks.

Also pins the escape-free ASCII path and the shared global SGR pattern's
statelessness across calls, since the background hit returns mid-loop.
This commit is contained in:
Neil
2026-09-19 15:03:38 -07:00
parent abdf9f14fe
commit f0de5d9258
2 changed files with 75 additions and 0 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest'
import { bindFreshSpawnFollowReset } from './fresh-spawn-follow-reset'
import type { ConnectPanePtySession } from './connect-pane-pty-session'
/**
* The foreground renderer-risk scan classifies the carried tail SPLICED onto the
* incoming chunk, not the raw chunk. ConPTY splits a background SGR mid-sequence
* routinely, and the continuation carries no escape of its own — so any cheap
* pre-gate that inspects `data` instead of `scanData` silently drops the refresh
* for exactly the redraws this path exists to catch.
*/
function buildSession(): ConnectPanePtySession {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the bag names every field the renderer-risk scan reads; the cast only supplies the rest of the session shape, which this suite never reaches.
const session = {
pane: { id: 'pane-1', terminal: {} },
foregroundRefreshRiskScanTail: ''
} as unknown as ConnectPanePtySession
bindFreshSpawnFollowReset(session)
return session
}
describe('foregroundRendererRiskOutputPrefersRenderRefresh', () => {
it('refreshes a background SGR split across chunks', () => {
const session = buildSession()
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('\x1b[4')).toBe(false)
expect(session.foregroundRefreshRiskScanTail).toBe('\x1b[4')
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('1m x')).toBe(true)
expect(session.foregroundRefreshRiskScanTail).toBe('')
})
it('carries a bare escape split before the CSI introducer', () => {
const session = buildSession()
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('\x1b')).toBe(false)
expect(session.foregroundRefreshRiskScanTail).toBe('\x1b')
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('[104m x')).toBe(true)
})
it('does not refresh the continuation chunk on its own', () => {
// The discriminator for the splice: '1m x' is escape-free ASCII.
expect(buildSession().foregroundRendererRiskOutputPrefersRenderRefresh('1m x')).toBe(false)
})
it('does not carry a completed sequence into the next chunk', () => {
const session = buildSession()
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('\x1b[32mplain green')).toBe(
false
)
expect(session.foregroundRefreshRiskScanTail).toBe('')
})
it('keeps an empty chunk from dropping the pending tail', () => {
const session = buildSession()
session.foregroundRendererRiskOutputPrefersRenderRefresh('\x1b[4')
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('')).toBe(false)
expect(session.foregroundRefreshRiskScanTail).toBe('\x1b[4')
expect(session.foregroundRendererRiskOutputPrefersRenderRefresh('8;5;33m x')).toBe(true)
})
})
@@ -76,6 +76,17 @@ describe('terminalOutputPrefersRenderRefresh', () => {
false
)
})
it('skips the SGR scan for escape-free ASCII output', () => {
expect(terminalOutputPrefersRenderRefresh('plain ascii progress 42%')).toBe(false)
})
it('keeps the shared SGR scan stateless across calls', () => {
// Why: the module-level pattern is global and the background hit returns
// mid-loop, so a dropped lastIndex reset would resume past the next match.
expect(terminalOutputPrefersRenderRefresh('\x1b[41m selected \x1b[0m')).toBe(true)
expect(terminalOutputPrefersRenderRefresh('\x1b[44m x\x1b[0m')).toBe(true)
})
})
describe('terminalOutputContainsEastAsianRendererRisk', () => {