Files
orca/src/main/native-chat/ssh-transcript-host-routing.test.ts
T

808 lines
28 KiB
TypeScript

import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { FileRangeReadResult, FileStat, IFilesystemProvider } from '../providers/types'
import { MAX_FILE_RANGE_READ_BYTES } from '../../shared/file-range-read'
const mocks = vi.hoisted(() => ({
getProvider: vi.fn<(connectionId: string) => IFilesystemProvider | undefined>(),
generation: 1,
changeListeners: new Set<(connectionId: string, generation: number) => void>(),
ownerListeners: new Set<() => void>(),
statusRows: [] as Record<string, unknown>[]
}))
vi.mock('../agent-hooks/server', () => ({
agentHookServer: {
getStatusSnapshot: () => mocks.statusRows,
getStatusSnapshotForPane: (paneKey: string) =>
mocks.statusRows.filter((row) => row.paneKey === paneKey),
subscribeProviderSessionChanges: (listener: () => void) => {
mocks.ownerListeners.add(listener)
return () => mocks.ownerListeners.delete(listener)
}
}
}))
vi.mock('../providers/ssh-filesystem-dispatch', () => ({
getSshFilesystemProvider: mocks.getProvider,
getSshFilesystemProviderSnapshot: (connectionId: string) => {
const provider = mocks.getProvider(connectionId)
return provider ? { provider, generation: mocks.generation } : null
},
onSshFilesystemProviderRegistered: () => () => undefined,
onSshFilesystemProviderChanged: (
listener: (connectionId: string, generation: number) => void
) => {
mocks.changeListeners.add(listener)
return () => mocks.changeListeners.delete(listener)
},
SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE: 'Remote connection dropped.'
}))
import {
nativeChatLineDecoderForAgent,
readNativeChatTranscriptTail
} from './transcript-tail-reader'
import { getActiveNativeChatWatcherCount, subscribeNativeChatTranscript } from './transcript-watch'
import { readIncrementalTranscriptMessages } from './transcript-incremental-reader'
import { TranscriptRangeReadInvalidatedError, type TranscriptRangeFs } from './transcript-range-fs'
const SESSION_ID = 'session-ssh-owner'
const PANE_KEY = 'tab-owner:11111111-1111-4111-8111-111111111111'
function claudeLine(id: string, text: string): string {
return `${JSON.stringify({
type: 'assistant',
uuid: id,
message: { role: 'assistant', content: [{ type: 'text', text }] }
})}\n`
}
function memoryProvider(files: Map<string, Buffer>) {
const readFile = vi.fn(async () => {
throw new Error('whole-file reads are forbidden for transcript tailing')
})
const readFileRange = vi.fn(
async (filePath: string, position: number, length: number): Promise<FileRangeReadResult> => {
const bytes = files.get(filePath)?.subarray(position, position + length)
if (!bytes) {
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${filePath}'`), {
code: 'ENOENT'
})
}
return { bytes, bytesRead: bytes.length }
}
)
const stat = vi.fn(async (filePath: string): Promise<FileStat> => {
const bytes = files.get(filePath)
if (!bytes) {
throw Object.assign(new Error(`ENOENT: no such file or directory, stat '${filePath}'`), {
code: 'ENOENT'
})
}
return { size: bytes.length, type: 'file', mtime: 1, mtimeMs: 1, dev: 7, ino: 11 }
})
const provider = {
stat,
readFile,
readFileRange,
async supportsFileRangeRead() {
return true
}
} as unknown as IFilesystemProvider
return { provider, readFile, readFileRange, stat }
}
function routedArgs(transcriptPath: string): {
agent: 'claude'
sessionId: string
transcriptPath: string
paneKey: string
} {
setOwner('ssh-owner', transcriptPath)
return {
agent: 'claude' as const,
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY
}
}
function setOwner(connectionId: string | null, transcriptPath?: string): void {
setStatusRows([
{
paneKey: PANE_KEY,
agentType: 'claude',
connectionId,
providerSession: {
key: 'session_id',
id: SESSION_ID,
...(transcriptPath ? { transcriptPath } : {})
}
}
])
}
function setStatusRows(rows: Record<string, unknown>[]): void {
mocks.statusRows = rows
for (const listener of mocks.ownerListeners) {
listener()
}
}
function changeProvider(provider: IFilesystemProvider | undefined): void {
mocks.getProvider.mockReturnValue(provider)
mocks.generation++
for (const listener of mocks.changeListeners) {
listener('ssh-owner', mocks.generation)
}
}
let tempRoots: string[] = []
beforeEach(() => {
mocks.getProvider.mockReset()
mocks.generation = 1
mocks.changeListeners.clear()
mocks.ownerListeners.clear()
mocks.statusRows = []
tempRoots = []
})
afterEach(async () => {
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
})
describe('native chat SSH transcript host routing (#13663)', () => {
it('reads the owning provider while rejecting a desktop same-path lookalike', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-route-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('desktop-poison', 'wrong host'))
const files = new Map([[transcriptPath, Buffer.from(claudeLine('remote-real', 'right host'))]])
const { provider, readFile, readFileRange, stat } = memoryProvider(files)
mocks.getProvider.mockReturnValue(provider)
const controller = new AbortController()
const result = await readNativeChatTranscriptTail(
{
...routedArgs(transcriptPath),
limit: 40
} as Parameters<typeof readNativeChatTranscriptTail>[0],
controller.signal
)
expect(result).toMatchObject({ messages: [{ id: 'remote-real' }], hasMore: false })
expect(result).not.toMatchObject({ messages: [{ id: 'desktop-poison' }] })
expect(mocks.getProvider).toHaveBeenCalledWith('ssh-owner')
expect(readFile).not.toHaveBeenCalled()
expect(readFileRange).toHaveBeenCalled()
expect(stat).toHaveBeenCalledWith(transcriptPath, { signal: controller.signal })
})
it('ignores a caller transcript path that contradicts the hook-owned path', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-path-authority-'))
tempRoots.push(root)
const callerPath = join(root, 'caller.jsonl')
const ownedPath = '/remote/owned.jsonl'
await writeFile(callerPath, claudeLine('caller-poison', 'wrong path'))
const remote = memoryProvider(
new Map([[ownedPath, Buffer.from(claudeLine('hook-owned', 'right path'))]])
)
mocks.getProvider.mockReturnValue(remote.provider)
setOwner('ssh-owner', ownedPath)
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath: callerPath,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'hook-owned' }] })
expect(remote.stat).toHaveBeenCalledWith(ownedPath, expect.anything())
expect(remote.stat).not.toHaveBeenCalledWith(callerPath, expect.anything())
})
it('retries consecutive ranged snapshots when the remote file is replaced mid-read', async () => {
const transcriptPath = '/tmp/replaced-mid-read.jsonl'
const oldBytes = Buffer.from(
`${claudeLine('old-large', 'a'.repeat(80_000))}${claudeLine('old-tail', 'old')}`
)
const middleBytes = Buffer.from(
`${claudeLine('middle-large', 'b'.repeat(80_000))}${claudeLine('middle-tail', 'middle')}`
)
const newBytes = Buffer.from(
`${claudeLine('new-large', 'c'.repeat(80_000))}${claudeLine('new-tail', 'new')}`
)
let current = oldBytes
let inode = 11
let rangeReadCount = 0
const readFileRange = vi.fn(
async (_filePath: string, position: number, length: number): Promise<FileRangeReadResult> => {
const bytes = current.subarray(position, position + length)
rangeReadCount++
if (rangeReadCount === 3) {
current = middleBytes
inode = 12
} else if (rangeReadCount === 6) {
current = newBytes
inode = 13
}
return { bytes, bytesRead: bytes.length }
}
)
const provider = {
async stat(): Promise<FileStat> {
return {
size: current.length,
type: 'file',
mtime: inode,
mtimeMs: inode,
dev: 7,
ino: inode
}
},
readFile: vi.fn(async () => {
throw new Error('whole-file reads are forbidden for transcript tailing')
}),
readFileRange,
async supportsFileRangeRead() {
return true
}
} as unknown as IFilesystemProvider
mocks.getProvider.mockReturnValue(provider)
const result = await readNativeChatTranscriptTail({
...routedArgs(transcriptPath),
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'new-large' }, { id: 'new-tail' }] })
expect(result).not.toMatchObject({ messages: [{ id: 'old-large' }, { id: 'old-tail' }] })
expect(readFileRange.mock.calls.length).toBeGreaterThan(7)
})
it('withholds remote append batches and restores the cursor after invalidation', async () => {
const bytes = Buffer.from(
Array.from({ length: 50 }, (_, index) => claudeLine(`stale-${index}`, `stale ${index}`)).join(
''
)
)
const onBatch = vi.fn()
const state = {
offset: 0,
pendingChunks: [] as Buffer[],
pendingStart: 0,
pendingBytes: 0,
droppingOversizedRecord: false
}
const rangeFs: TranscriptRangeFs = {
async stat() {
return {
size: bytes.length,
identity: '1:7:11',
identityReliable: true,
mtimeMs: 1,
ctimeMs: 1
}
},
async read(_filePath, position, length) {
return bytes.subarray(position, position + length)
},
async assertStable() {
throw new TranscriptRangeReadInvalidatedError()
}
}
await expect(
readIncrementalTranscriptMessages(
'/tmp/invalidated-append.jsonl',
state,
nativeChatLineDecoderForAgent('claude')!,
onBatch,
undefined,
undefined,
undefined,
rangeFs
)
).rejects.toBeInstanceOf(TranscriptRangeReadInvalidatedError)
expect(onBatch).not.toHaveBeenCalled()
expect(state).toMatchObject({ offset: 0, pendingStart: 0, pendingBytes: 0 })
})
it('emits the remote snapshot and later tail without whole-file polling', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-tail-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('desktop-poison', 'wrong host'))
const files = new Map([[transcriptPath, Buffer.from(claudeLine('remote-first', 'first'))]])
const { provider, readFile, readFileRange } = memoryProvider(files)
mocks.getProvider.mockReturnValue(provider)
const snapshots: string[][] = []
const appended: string[] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: (messages) => snapshots.push(messages.map((message) => message.id)),
onAppend: (messages) => appended.push(...messages.map((message) => message.id)),
debounceMs: 0,
reconciliationIntervalMs: 20
} as Parameters<typeof subscribeNativeChatTranscript>[0])
try {
await vi.waitFor(() => expect(snapshots).toContainEqual(['remote-first']))
files.set(
transcriptPath,
Buffer.from(`${claudeLine('remote-first', 'first')}${claudeLine('remote-next', 'next')}`)
)
await vi.waitFor(() => expect(appended).toContain('remote-next'))
expect(snapshots.flat()).not.toContain('desktop-poison')
expect(readFile).not.toHaveBeenCalled()
expect(readFileRange).toHaveBeenCalled()
} finally {
subscription.unsubscribe()
}
})
it('never falls back locally when ownership is missing', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-unknown-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('desktop-poison', 'wrong host'))
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toEqual({ error: 'Transcript unverifiable on the remote host' })
expect(mocks.getProvider).not.toHaveBeenCalled()
})
it('never installs a local watcher when subscription ownership is missing', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-unknown-watch-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('desktop-poison', 'wrong host'))
const before = getActiveNativeChatWatcherCount()
const snapshots: { ids: string[]; error?: string }[] = []
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY,
onInitialSnapshot: (messages, _hasMore, _beforeOffset, error) =>
snapshots.push({ ids: messages.map((message) => message.id), error }),
onAppend: () => {}
})
expect(snapshots).toEqual([{ ids: [], error: 'Transcript unverifiable on the remote host' }])
expect(getActiveNativeChatWatcherCount()).toBe(before)
expect(mocks.getProvider).not.toHaveBeenCalled()
subscription.unsubscribe()
})
it('keeps stamped local sessions on the existing local reader', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-local-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('local-real', 'local host'))
setOwner(null, transcriptPath)
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'local-real' }] })
expect(mocks.getProvider).not.toHaveBeenCalled()
})
it('treats WSL hook provenance as local transcript ownership', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-wsl-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('wsl-local', 'local WSL bridge'))
setOwner('wsl:Ubuntu', transcriptPath)
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'wsl-local' }] })
expect(mocks.getProvider).not.toHaveBeenCalled()
})
it('keeps locator-free legacy requests host-local for older clients', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-legacy-local-'))
tempRoots.push(root)
const transcriptPath = join(root, 'session.jsonl')
await writeFile(transcriptPath, claudeLine('legacy-local', 'older client'))
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'legacy-local' }] })
expect(mocks.getProvider).not.toHaveBeenCalled()
})
it('selects the requested pane when two hosts expose the same session id and path', async () => {
const transcriptPath = '/tmp/same-session.jsonl'
const wrong = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('wrong-remote', 'wrong host'))]])
)
const right = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('right-remote', 'right host'))]])
)
mocks.getProvider.mockImplementation((connectionId) =>
connectionId === 'ssh-owner' ? right.provider : wrong.provider
)
setStatusRows([
{
paneKey: 'other-pane:11111111-1111-4111-8111-111111111111',
agentType: 'claude',
connectionId: 'wrong-owner',
providerSession: { key: 'session_id', id: SESSION_ID, transcriptPath }
},
{
paneKey: PANE_KEY,
agentType: 'claude',
connectionId: 'ssh-owner',
providerSession: { key: 'session_id', id: SESSION_ID, transcriptPath }
}
])
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'right-remote' }] })
expect(right.readFileRange).toHaveBeenCalled()
expect(wrong.readFileRange).not.toHaveBeenCalled()
})
it('does not use a caller path to disambiguate locator-free ownership', async () => {
const requestedPath = '/tmp/requested.jsonl'
setStatusRows([
{
paneKey: PANE_KEY,
agentType: 'claude',
connectionId: 'ssh-owner',
providerSession: { key: 'session_id', id: SESSION_ID, transcriptPath: requestedPath }
},
{
paneKey: 'other-pane:11111111-1111-4111-8111-111111111111',
agentType: 'claude',
connectionId: 'wrong-owner',
providerSession: {
key: 'session_id',
id: SESSION_ID,
transcriptPath: '/tmp/other.jsonl'
}
}
])
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
transcriptPath: requestedPath,
limit: 40
})
expect(result).toEqual({ error: 'Transcript unverifiable on the remote host' })
expect(mocks.getProvider).not.toHaveBeenCalled()
})
it('does not run desktop id discovery when an SSH locator has no transcript path', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-host-no-path-'))
tempRoots.push(root)
const projectDir = join(root, 'project')
await mkdir(projectDir, { recursive: true })
await writeFile(join(projectDir, `${SESSION_ID}.jsonl`), claudeLine('desktop-id-hit', 'wrong'))
setOwner('ssh-owner')
const result = await readNativeChatTranscriptTail({
agent: 'claude',
sessionId: SESSION_ID,
claudeProjectsDir: root,
paneKey: PANE_KEY,
limit: 40
})
expect(result).toMatchObject({ notFound: true })
expect(result).not.toHaveProperty('messages')
const snapshots: { ids: string[]; error?: string }[] = []
const before = getActiveNativeChatWatcherCount()
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
sessionId: SESSION_ID,
claudeProjectsDir: root,
paneKey: PANE_KEY,
onInitialSnapshot: (messages, _hasMore, _beforeOffset, error) =>
snapshots.push({ ids: messages.map((message) => message.id), error }),
onAppend: () => {}
})
expect(snapshots).toEqual([{ ids: [], error: 'Transcript unavailable' }])
expect(getActiveNativeChatWatcherCount()).toBe(before)
subscription.unsubscribe()
})
it('rearms on provider replacement and emits the new remote snapshot', async () => {
const transcriptPath = '/tmp/reconnected-session.jsonl'
const first = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('before-reconnect', 'first'))]])
)
const second = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('after-reconnect', 'second'))]])
)
mocks.getProvider.mockReturnValue(first.provider)
const snapshots: string[][] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: (messages) => snapshots.push(messages.map((message) => message.id)),
onAppend: () => {},
debounceMs: 0,
reconciliationIntervalMs: 20
})
try {
await vi.waitFor(() => expect(snapshots).toContainEqual(['before-reconnect']))
changeProvider(second.provider)
await vi.waitFor(() => expect(snapshots).toContainEqual(['after-reconnect']))
expect(first.readFile).not.toHaveBeenCalled()
expect(second.readFile).not.toHaveBeenCalled()
} finally {
subscription.unsubscribe()
}
})
it('fails closed when the owner row disappears and rebinds when it returns', async () => {
const transcriptPath = '/tmp/rebound-owner-session.jsonl'
const remote = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('remote-owner', 'remote'))]])
)
mocks.getProvider.mockReturnValue(remote.provider)
const snapshots: { ids: string[]; error?: string }[] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: (messages, _hasMore, _beforeOffset, error) =>
snapshots.push({ ids: messages.map((message) => message.id), error }),
onAppend: () => {},
debounceMs: 0,
reconciliationIntervalMs: 20
})
try {
await vi.waitFor(() =>
expect(snapshots).toContainEqual({ ids: ['remote-owner'], error: undefined })
)
setStatusRows([])
await vi.waitFor(() =>
expect(snapshots).toContainEqual({
ids: [],
error: 'Transcript unverifiable on the remote host'
})
)
setOwner('ssh-owner', transcriptPath)
await vi.waitFor(() =>
expect(snapshots.filter((snapshot) => snapshot.ids.includes('remote-owner'))).toHaveLength(
2
)
)
} finally {
subscription.unsubscribe()
}
})
it('recovers from provider loss with one fresh watcher and no stale snapshot', async () => {
const transcriptPath = '/work/folder-workspace/session.jsonl'
const first = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('before-loss', 'first'))]])
)
const restored = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('after-restore', 'second'))]])
)
mocks.getProvider.mockReturnValue(first.provider)
const before = getActiveNativeChatWatcherCount()
const snapshots: { ids: string[]; error?: string }[] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: (messages, _hasMore, _beforeOffset, error) =>
snapshots.push({ ids: messages.map((message) => message.id), error }),
onAppend: () => {},
debounceMs: 0,
reconciliationIntervalMs: 20
})
try {
await vi.waitFor(() =>
expect(snapshots).toContainEqual({ ids: ['before-loss'], error: undefined })
)
expect(getActiveNativeChatWatcherCount()).toBe(before + 1)
changeProvider(undefined)
await vi.waitFor(() =>
expect(snapshots).toContainEqual({
ids: [],
error: 'Transcript unverifiable on the remote host'
})
)
expect(getActiveNativeChatWatcherCount()).toBe(before)
changeProvider(restored.provider)
await vi.waitFor(() =>
expect(snapshots).toContainEqual({ ids: ['after-restore'], error: undefined })
)
expect(getActiveNativeChatWatcherCount()).toBe(before + 1)
expect(snapshots.some((snapshot) => snapshot.ids.includes('before-loss'))).toBe(true)
expect(snapshots.at(-1)?.ids).toEqual(['after-restore'])
const firstOutageFrames = snapshots.filter(
(snapshot) => snapshot.error === 'Transcript unverifiable on the remote host'
).length
changeProvider(undefined)
await vi.waitFor(() =>
expect(
snapshots.filter(
(snapshot) => snapshot.error === 'Transcript unverifiable on the remote host'
)
).toHaveLength(firstOutageFrames + 1)
)
expect(getActiveNativeChatWatcherCount()).toBe(before)
} finally {
subscription.unsubscribe()
}
expect(getActiveNativeChatWatcherCount()).toBe(before)
})
it('keeps retrying when an unverifiable advisory subscriber throws', async () => {
const transcriptPath = '/tmp/throwing-subscriber-session.jsonl'
const restored = memoryProvider(
new Map([[transcriptPath, Buffer.from(claudeLine('recovered-after-throw', 'second'))]])
)
mocks.getProvider.mockReturnValue(undefined)
const snapshots: string[][] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: (messages, _hasMore, _beforeOffset, error) => {
if (error) {
throw new Error('subscriber is closing')
}
snapshots.push(messages.map((message) => message.id))
},
onAppend: () => {},
debounceMs: 0,
reconciliationIntervalMs: 20
})
try {
changeProvider(restored.provider)
await vi.waitFor(() => expect(snapshots).toContainEqual(['recovered-after-throw']))
} finally {
subscription.unsubscribe()
}
})
it('emits one advisory while a registered provider remains unreachable', async () => {
const transcriptPath = '/tmp/unreachable-provider-session.jsonl'
const { provider, stat } = memoryProvider(new Map())
stat.mockRejectedValue(Object.assign(new Error('connection lost'), { code: 'CONNECTION_LOST' }))
mocks.getProvider.mockReturnValue(provider)
const errors: (string | undefined)[] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
onInitialSnapshot: (_messages, _hasMore, _beforeOffset, error) => errors.push(error),
onAppend: () => {},
resolvePollIntervalMs: 5
})
try {
await vi.waitFor(() => expect(stat.mock.calls.length).toBeGreaterThanOrEqual(3))
expect(errors).toEqual(['Transcript unverifiable on the remote host'])
} finally {
subscription.unsubscribe()
}
})
it('keeps provider methods bound to their owning instance', async () => {
const transcriptPath = '/tmp/bound-provider-session.jsonl'
const provider = {
files: new Map([[transcriptPath, Buffer.from(claudeLine('bound-remote', 'right host'))]]),
async stat(this: { files: Map<string, Buffer> }, filePath: string): Promise<FileStat> {
const bytes = this.files.get(filePath)!
return { size: bytes.length, type: 'file', mtime: 1, mtimeMs: 1 }
},
async readFileRange(
this: { files: Map<string, Buffer> },
filePath: string,
position: number,
length: number
): Promise<FileRangeReadResult> {
const bytes = this.files.get(filePath)!.subarray(position, position + length)
return { bytes, bytesRead: bytes.length }
},
async supportsFileRangeRead() {
return true
}
} as unknown as IFilesystemProvider
mocks.getProvider.mockReturnValue(provider)
const result = await readNativeChatTranscriptTail({
...routedArgs(transcriptPath),
limit: 40
})
expect(result).toMatchObject({ messages: [{ id: 'bound-remote' }] })
})
it('pages a large append in bounded ranges without rereading the prefix', async () => {
const transcriptPath = '/tmp/large-append-session.jsonl'
const initial = Buffer.from(claudeLine('initial', 'first'))
const files = new Map([[transcriptPath, initial]])
const { provider, readFile, readFileRange } = memoryProvider(files)
mocks.getProvider.mockReturnValue(provider)
const appended: string[] = []
const subscription = await subscribeNativeChatTranscript({
...routedArgs(transcriptPath),
initialLimit: 40,
onInitialSnapshot: () => {},
onAppend: (messages) => appended.push(...messages.map((message) => message.id)),
debounceMs: 0,
reconciliationIntervalMs: 20
})
try {
await vi.waitFor(() => expect(readFileRange).toHaveBeenCalled())
readFileRange.mockClear()
const append = Buffer.from(
Array.from({ length: 3_000 }, (_, index) =>
claudeLine(`append-${index}`, `bounded remote append ${index}`)
).join('')
)
expect(append.length).toBeGreaterThan(MAX_FILE_RANGE_READ_BYTES)
files.set(transcriptPath, Buffer.concat([initial, append]))
await vi.waitFor(() => expect(appended).toContain('append-2999'))
const requestedLengths = readFileRange.mock.calls.map((call) => call[2])
expect(Math.max(...requestedLengths)).toBeLessThanOrEqual(MAX_FILE_RANGE_READ_BYTES)
expect(requestedLengths.reduce((sum, length) => sum + length, 0)).toBeLessThanOrEqual(
append.length + 256
)
expect(readFile).not.toHaveBeenCalled()
} finally {
subscription.unsubscribe()
}
})
})