mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): invalidate rewritten SSH ranges
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
nativeChatLineDecoderForAgent,
|
||||
readNativeChatTranscriptTail
|
||||
} from './transcript-tail-reader'
|
||||
import { createSshTranscriptRangeFs } from './ssh-transcript-range-fs'
|
||||
import { getActiveNativeChatWatcherCount, subscribeNativeChatTranscript } from './transcript-watch'
|
||||
import { readIncrementalTranscriptMessages } from './transcript-incremental-reader'
|
||||
import { TranscriptRangeReadInvalidatedError, type TranscriptRangeFs } from './transcript-range-fs'
|
||||
@@ -266,6 +267,61 @@ describe('native chat SSH transcript host routing (#13663)', () => {
|
||||
expect(readFileRange.mock.calls.length).toBeGreaterThan(7)
|
||||
})
|
||||
|
||||
it('invalidates a same-inode ranged snapshot rewritten in place', async () => {
|
||||
const transcriptPath = '/tmp/same-inode-rewrite.jsonl'
|
||||
const files = new Map([[transcriptPath, Buffer.from('old generation')]])
|
||||
const remote = memoryProvider(files)
|
||||
let mtimeMs = 1
|
||||
remote.stat.mockImplementation(
|
||||
async (filePath: string): Promise<FileStat> => ({
|
||||
size: files.get(filePath)!.length,
|
||||
type: 'file',
|
||||
mtime: mtimeMs,
|
||||
mtimeMs,
|
||||
dev: 7,
|
||||
ino: 11
|
||||
})
|
||||
)
|
||||
mocks.getProvider.mockReturnValue(remote.provider)
|
||||
const rangeFs = await createSshTranscriptRangeFs('ssh-owner')
|
||||
const openingStamp = await rangeFs.stat(transcriptPath)
|
||||
|
||||
await rangeFs.read(transcriptPath, 0, 3)
|
||||
files.set(transcriptPath, Buffer.from('new generation'))
|
||||
mtimeMs = 2
|
||||
await rangeFs.read(transcriptPath, 3, 3)
|
||||
|
||||
await expect(rangeFs.assertStable(transcriptPath, openingStamp)).rejects.toBeInstanceOf(
|
||||
TranscriptRangeReadInvalidatedError
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps a ranged snapshot valid across an ordinary append', async () => {
|
||||
const transcriptPath = '/tmp/concurrent-append.jsonl'
|
||||
const files = new Map([[transcriptPath, Buffer.from('opening')]])
|
||||
const remote = memoryProvider(files)
|
||||
let mtimeMs = 1
|
||||
remote.stat.mockImplementation(
|
||||
async (filePath: string): Promise<FileStat> => ({
|
||||
size: files.get(filePath)!.length,
|
||||
type: 'file',
|
||||
mtime: mtimeMs,
|
||||
mtimeMs,
|
||||
dev: 7,
|
||||
ino: 11
|
||||
})
|
||||
)
|
||||
mocks.getProvider.mockReturnValue(remote.provider)
|
||||
const rangeFs = await createSshTranscriptRangeFs('ssh-owner')
|
||||
const openingStamp = await rangeFs.stat(transcriptPath)
|
||||
|
||||
await rangeFs.read(transcriptPath, 0, openingStamp.size)
|
||||
files.set(transcriptPath, Buffer.from('opening append'))
|
||||
mtimeMs = 2
|
||||
|
||||
await expect(rangeFs.assertStable(transcriptPath, openingStamp)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
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(
|
||||
|
||||
@@ -84,7 +84,10 @@ export async function createSshTranscriptRangeFs(
|
||||
const identityChanged = closingStamp.identity !== openingStamp.identity
|
||||
const unpinnedVersionChanged =
|
||||
!openingStamp.identityReliable && closingStamp.mtimeMs !== openingStamp.mtimeMs
|
||||
if (identityChanged || unpinnedVersionChanged) {
|
||||
const nonAppendSnapshotChanged =
|
||||
closingStamp.size < openingStamp.size ||
|
||||
(closingStamp.size === openingStamp.size && closingStamp.mtimeMs !== openingStamp.mtimeMs)
|
||||
if (identityChanged || unpinnedVersionChanged || nonAppendSnapshotChanged) {
|
||||
throw new TranscriptRangeReadInvalidatedError()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user