mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
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:
+64
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user