mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(terminal): preserve parser carry on empty snapshots
This commit is contained in:
@@ -2349,13 +2349,10 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
|
||||
source: serializedContent?.source,
|
||||
kittyKeyboardFlags: serializedContent?.kittyKeyboardFlags,
|
||||
oscLinks: serializedContent?.oscLinks,
|
||||
pendingEscapeTailAnsi: serializedContent?.pendingEscapeTailAnsi,
|
||||
pendingEscapeTailAnsi: serialized?.pendingEscapeTailAnsi,
|
||||
truncated: false,
|
||||
truncatedByByteBudget: serialized?.truncatedByByteBudget,
|
||||
// Why: `unavailable` means nobody answered, so the client retries and can
|
||||
// eventually banner. A serializer that DID answer with an empty buffer has
|
||||
// answered — reporting that as unavailable banners genuinely empty panes.
|
||||
// Empty content still suppresses the seq/source metadata below.
|
||||
// Why: only absence is retry-worthy; empty visual data still suppresses provenance.
|
||||
unavailable: serialized ? undefined : 'no-serializable-buffer',
|
||||
data: serializedContent?.data ?? ''
|
||||
})
|
||||
@@ -2696,10 +2693,8 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
|
||||
source: serializedContent?.source,
|
||||
kittyKeyboardFlags: serializedContent?.kittyKeyboardFlags,
|
||||
oscLinks: serializedContent?.oscLinks,
|
||||
pendingEscapeTailAnsi: serializedContent?.pendingEscapeTailAnsi,
|
||||
// Why: same rule as the tagged path — only an absent serializer answer is
|
||||
// "unavailable". A serializer that answered empty, with an empty retained
|
||||
// tail, is a genuinely empty pane and must not be reported as a failure.
|
||||
pendingEscapeTailAnsi: serialized?.pendingEscapeTailAnsi,
|
||||
// Why: only absence without retained tail is retry-worthy; empty answers are valid.
|
||||
unavailable:
|
||||
serialized || read.tail.length > 0 ? undefined : 'no-serializable-buffer',
|
||||
data:
|
||||
|
||||
@@ -20,6 +20,8 @@ type SerializedBuffer = {
|
||||
rows: number
|
||||
seq?: number
|
||||
source?: 'headless' | 'renderer'
|
||||
cwd?: string
|
||||
pendingEscapeTailAnsi?: string
|
||||
} | null
|
||||
|
||||
function makeRequest(method: string, params?: unknown): RpcRequest {
|
||||
@@ -121,7 +123,14 @@ describe('terminal multiplex initial snapshot fallback', () => {
|
||||
it('serves the retained tail when serialization returns an empty snapshot object', async () => {
|
||||
const snapshot = await subscribeSnapshot({
|
||||
connectionId: 'conn-empty-snapshot-tail',
|
||||
serialized: { data: '', cols: 120, rows: 40, seq: 42, source: 'renderer' },
|
||||
serialized: {
|
||||
data: '',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: 42,
|
||||
source: 'renderer',
|
||||
cwd: '/stale'
|
||||
},
|
||||
tail: ['retained line 1', 'retained line 2']
|
||||
})
|
||||
|
||||
@@ -129,6 +138,7 @@ describe('terminal multiplex initial snapshot fallback', () => {
|
||||
expect(snapshot.start.unavailable).toBeUndefined()
|
||||
expect(snapshot.start.seq).toBeUndefined()
|
||||
expect(snapshot.start.source).toBeUndefined()
|
||||
expect(snapshot.start.cwd).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps the retained-tail fallback when serialization returns null', async () => {
|
||||
@@ -165,4 +175,37 @@ describe('terminal multiplex initial snapshot fallback', () => {
|
||||
expect(snapshot.data).toBe('')
|
||||
expect(snapshot.start.unavailable).toBe('no-serializable-buffer')
|
||||
})
|
||||
|
||||
it('reports a successful empty snapshot when the serializer answered and no tail exists', async () => {
|
||||
const snapshot = await subscribeSnapshot({
|
||||
connectionId: 'conn-empty-snapshot-no-tail',
|
||||
serialized: { data: '', cols: 120, rows: 40, seq: 42, source: 'renderer' },
|
||||
tail: []
|
||||
})
|
||||
|
||||
expect(snapshot.data).toBe('')
|
||||
expect(snapshot.start.unavailable).toBeUndefined()
|
||||
expect(snapshot.start.seq).toBeUndefined()
|
||||
expect(snapshot.start.source).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves parser carry state when serialized visual data is empty', async () => {
|
||||
const snapshot = await subscribeSnapshot({
|
||||
connectionId: 'conn-empty-snapshot-parser-tail',
|
||||
serialized: {
|
||||
data: '',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: 42,
|
||||
source: 'renderer',
|
||||
pendingEscapeTailAnsi: '\x1b[38;2;255'
|
||||
},
|
||||
tail: []
|
||||
})
|
||||
|
||||
expect(snapshot.data).toBe('')
|
||||
expect(snapshot.start.pendingEscapeTailAnsi).toBe('\x1b[38;2;255')
|
||||
expect(snapshot.start.seq).toBeUndefined()
|
||||
expect(snapshot.start.source).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,7 +13,16 @@ import {
|
||||
encodeTerminalStreamJson
|
||||
} from '../../../shared/terminal-stream-protocol'
|
||||
|
||||
type SerializedBuffer = { data: string; scrollbackAnsi?: string; cols: number; rows: number } | null
|
||||
type SerializedBuffer = {
|
||||
data: string
|
||||
scrollbackAnsi?: string
|
||||
cols: number
|
||||
rows: number
|
||||
seq?: number
|
||||
cwd?: string
|
||||
source?: 'headless' | 'renderer'
|
||||
pendingEscapeTailAnsi?: string
|
||||
} | null
|
||||
type SnapshotStartPayload = Record<string, unknown>
|
||||
|
||||
// Why: 256 KiB is the pending-output budget, so this many 1 KiB chunks always trips the overflow guard.
|
||||
@@ -168,9 +177,7 @@ describe('requested terminal snapshot unavailability reasons', () => {
|
||||
expect(start.unavailable).toBeUndefined()
|
||||
})
|
||||
|
||||
// Why: `unavailable` drives client retries and eventually a loss banner. A serializer
|
||||
// that answered with an empty buffer HAS answered; a genuinely empty pane must not be
|
||||
// reported as a failure or every fresh pane banners. Only an absent answer is a failure.
|
||||
// Why: unavailable answers retry and eventually banner; empty answers are valid.
|
||||
it('does not report a reason when the serializer answered with an empty buffer', async () => {
|
||||
const { start, chunks } = await requestSnapshotReply({
|
||||
connectionId: 'conn-reason-empty',
|
||||
@@ -181,15 +188,37 @@ describe('requested terminal snapshot unavailability reasons', () => {
|
||||
expect(start.unavailable).toBeUndefined()
|
||||
})
|
||||
|
||||
// Why: an empty answer must still not publish seq/source metadata, or the client
|
||||
// anchors its stream to a snapshot that carries no content.
|
||||
it('omits seq and source metadata for an empty serialized answer', async () => {
|
||||
// Why: empty visual data cannot anchor stream provenance.
|
||||
it('omits seq, source, and cwd metadata for an empty serialized answer', async () => {
|
||||
const { start } = await requestSnapshotReply({
|
||||
connectionId: 'conn-reason-empty-metadata',
|
||||
serializeRequested: async () => ({ data: '', cols: 120, rows: 40 })
|
||||
serializeRequested: async () => ({
|
||||
data: '',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
seq: 42,
|
||||
source: 'renderer',
|
||||
cwd: '/stale'
|
||||
})
|
||||
})
|
||||
expect(start.seq).toBeUndefined()
|
||||
expect(start.source).toBeUndefined()
|
||||
expect(start.cwd).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves parser carry state when serialized visual data is empty', async () => {
|
||||
const { start, chunks } = await requestSnapshotReply({
|
||||
connectionId: 'conn-reason-empty-parser-tail',
|
||||
serializeRequested: async () => ({
|
||||
data: '',
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
pendingEscapeTailAnsi: '\x1b[38;2;255'
|
||||
})
|
||||
})
|
||||
expect(chunks).toBe('')
|
||||
expect(start.pendingEscapeTailAnsi).toBe('\x1b[38;2;255')
|
||||
expect(start.unavailable).toBeUndefined()
|
||||
})
|
||||
|
||||
it('accepts scrollback-only serialized content', async () => {
|
||||
|
||||
Reference in New Issue
Block a user