mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
* Autosave relies on the guarded write instead of pausing restored or moved tabs Delete the pendingDiskBaselineVerification and pendingLiveDiskVerification pause flags. Every editor save is already a guarded write; a refusal raises the changed-on-disk banner, which is what stops autosave until an explicit save. A guarded write now succeeds when disk changed but already holds exactly the text being saved (local/host compare is byte-exact; client-side fallbacks compare decoded text and refuse when it holds U+FFFD), so a restored draft that matches disk goes clean instead of showing a banner. The restore scan only raises the banner early for tabs that arrive dirty; a watch resync no longer re-checks dirty tabs. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): skip guarded saves when disk is already identical Return a compatible optional no-op result from hosts and client fallbacks, clean the tab, and clear its provisional self-write stamp without rewriting identical bytes. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): refuse lossy automatic saves, keep identical-skip on older hosts, flag dirty tabs on resync - Autosave and pre-restart saves refuse when disk text can't be decoded losslessly, so a baseline that decoded to U+FFFD can't authorize overwriting different bytes. - New files.write.identical-noop.v1 capability; for hosts with only the v1 guarded write the client compares first, skips identical saves, then delegates to the host's guarded write. - Watch resync compares dirty tabs against disk and shows the changed-on-disk banner. - Local guarded writes check for identical content before the write-only regular-file probe. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): tell non-UTF-8 files apart from disk changes, and quiet false resync banners - Local guarded writes judge losslessness by bytes, so a valid UTF-8 file containing U+FFFD autosaves again. A file that really isn't UTF-8 is refused with its own reason, 'not-utf8' (older clients read it as 'changed'), which pauses autosave for that file with one notice until an explicit save converts it. - A watch resync whose disk read fails leaves dirty tabs alone; the guarded save still catches a real conflict. - Resync and restored-tab checks treat disk holding the tab's own just-written save as an echo. - On hosts with only the v1 guarded write, only automatic saves read the file first; explicit saves go straight to the host's guard. Co-Authored-By: Claude <noreply@anthropic.com> * fix(editor): lift the non-UTF-8 autosave pause when its edits end, and report disk changes first - The pause is released once no dirty tab of the file remains (close, reload from disk, discard), so a reopened or reloaded file's next autosave asks the host again and notifies. - Client-side precondition checks compare the signature before looking for U+FFFD, so an external rewrite shows the changed-on-disk banner rather than the non-UTF-8 notice. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
130 lines
5.8 KiB
TypeScript
130 lines
5.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
diskBaselineSignatureLength,
|
|
getDiskBaselineSignature,
|
|
utf8ByteSizeCanDecodeToLength
|
|
} from './disk-baseline-signature'
|
|
import {
|
|
compareDiskWritePrecondition,
|
|
diskConflictFromReadFailure,
|
|
parseFileWriteResult
|
|
} from './file-write-disk-precondition'
|
|
|
|
describe('disk write precondition', () => {
|
|
it('compares decoded disk text against the baseline signature', () => {
|
|
const precondition = { expectedSignature: getDiskBaselineSignature('base') }
|
|
expect(compareDiskWritePrecondition(precondition, 'base', 'mine')).toBeNull()
|
|
expect(compareDiskWritePrecondition(precondition, 'newer', 'mine')).toBe('changed')
|
|
expect(compareDiskWritePrecondition(precondition, undefined, 'mine')).toBe('changed')
|
|
expect(compareDiskWritePrecondition(precondition, null, 'mine')).toBe('missing')
|
|
expect(
|
|
compareDiskWritePrecondition({ ...precondition, allowMissing: true }, null, 'mine')
|
|
).toBeNull()
|
|
})
|
|
|
|
it('accepts a changed disk that already holds exactly the text being saved', () => {
|
|
const precondition = { expectedSignature: getDiskBaselineSignature('base') }
|
|
expect(compareDiskWritePrecondition(precondition, 'mine', 'mine')).toBe('identical')
|
|
expect(compareDiskWritePrecondition(precondition, 'base', 'base')).toBe('identical')
|
|
expect(compareDiskWritePrecondition(precondition, 'mine\r\n', 'mine\n')).toBe('changed')
|
|
expect(compareDiskWritePrecondition(precondition, undefined, 'mine')).toBe('changed')
|
|
// A replacement character may stand for bytes the decode dropped, so it proves nothing.
|
|
expect(compareDiskWritePrecondition(precondition, 'a�', 'a�')).toBe('changed')
|
|
})
|
|
|
|
it('refuses ambiguous automatic baseline matches but preserves explicit saves', () => {
|
|
const decoded = Buffer.from([0x61, 0xfe]).toString('utf8')
|
|
const current = Buffer.from([0x61, 0xff]).toString('utf8')
|
|
const precondition = { expectedSignature: getDiskBaselineSignature(decoded) }
|
|
expect(
|
|
compareDiskWritePrecondition({ ...precondition, requireLosslessUtf8: true }, current, decoded)
|
|
).toBe('not-utf8')
|
|
expect(compareDiskWritePrecondition(precondition, current, decoded)).toBeNull()
|
|
})
|
|
|
|
it('reports an external rewrite as changed before checking for U+FFFD', () => {
|
|
expect(
|
|
compareDiskWritePrecondition(
|
|
{ expectedSignature: getDiskBaselineSignature('original'), requireLosslessUtf8: true },
|
|
'rewritten �',
|
|
'mine'
|
|
)
|
|
).toBe('changed')
|
|
})
|
|
|
|
it('treats anything but an explicit refusal from a host as a completed write', () => {
|
|
expect(parseFileWriteResult({ ok: false, diskConflict: 'not-utf8' })).toEqual({
|
|
ok: false,
|
|
diskConflict: 'not-utf8'
|
|
})
|
|
expect(parseFileWriteResult({ ok: false, diskConflict: 'later' })).toEqual({
|
|
ok: false,
|
|
diskConflict: 'changed'
|
|
})
|
|
expect(parseFileWriteResult(undefined)).toEqual({ ok: true })
|
|
expect(parseFileWriteResult({ ok: true })).toEqual({ ok: true })
|
|
expect(parseFileWriteResult({ ok: true, unchanged: true })).toEqual({
|
|
ok: true,
|
|
unchanged: true
|
|
})
|
|
expect(parseFileWriteResult({ ok: false, diskConflict: 'missing' })).toEqual({
|
|
ok: false,
|
|
diskConflict: 'missing'
|
|
})
|
|
expect(parseFileWriteResult({ ok: false })).toEqual({ ok: false, diskConflict: 'changed' })
|
|
})
|
|
|
|
it('bounds the UTF-8 size a baseline can decode from', () => {
|
|
const content = 'a€𝄞'
|
|
const length = diskBaselineSignatureLength(getDiskBaselineSignature(content))
|
|
expect(length).toBe(content.length)
|
|
expect(utf8ByteSizeCanDecodeToLength(Buffer.byteLength(content), length ?? -1)).toBe(true)
|
|
expect(utf8ByteSizeCanDecodeToLength(content.length - 1, content.length)).toBe(false)
|
|
expect(utf8ByteSizeCanDecodeToLength(content.length * 3 + 1, content.length)).toBe(false)
|
|
expect(diskBaselineSignatureLength('not-a-signature')).toBeNull()
|
|
})
|
|
|
|
it.each([
|
|
[
|
|
'an ENOENT code with plain wording',
|
|
Object.assign(new Error('File not found'), { code: 'ENOENT' })
|
|
],
|
|
['an SFTP-style message', new Error('No such file or directory')]
|
|
])('treats %s from an older host as a missing file', (_label, error) => {
|
|
const precondition = { expectedSignature: getDiskBaselineSignature('base') }
|
|
expect(diskConflictFromReadFailure(precondition, error)).toBe('missing')
|
|
})
|
|
|
|
it.each([
|
|
[
|
|
'the desktop IPC wrapping',
|
|
"Error invoking remote method 'fs:readFile': Error: ENOENT: no such file or directory, open '/repo/a.ts'"
|
|
],
|
|
['the runtime wording', "ENOENT: no such file or directory, stat '/repo/a.ts'"]
|
|
])('reads a not-found from %s', (_label, message) => {
|
|
const precondition = { expectedSignature: getDiskBaselineSignature('base') }
|
|
expect(diskConflictFromReadFailure(precondition, new Error(message))).toBe('missing')
|
|
})
|
|
|
|
it.each([
|
|
"EACCES: permission denied, open '/repo/ENOENT.md'",
|
|
"EACCES: permission denied, open '/repo/ENOENT/notes.md'",
|
|
"EACCES: permission denied, open '/repo/No such file or directory/notes.md'",
|
|
"EACCES: permission denied, open '/repo/Error: ENOENT: no such file or directory.txt'",
|
|
"Error invoking remote method 'fs:readFile': Error: EACCES: permission denied, open '/repo/Error: ENOENT: no such file or directory.txt'"
|
|
])('never reads a path that merely names ENOENT as a missing file: %s', (message) => {
|
|
const precondition = {
|
|
expectedSignature: getDiskBaselineSignature('base'),
|
|
allowMissing: true
|
|
}
|
|
expect(() => diskConflictFromReadFailure(precondition, new Error(message))).toThrow('EACCES')
|
|
})
|
|
|
|
it('trusts a conflicting errno code over not-found wording', () => {
|
|
const error = Object.assign(new Error('ENOENT: no such file or directory'), { code: 'EACCES' })
|
|
expect(() =>
|
|
diskConflictFromReadFailure({ expectedSignature: getDiskBaselineSignature('base') }, error)
|
|
).toThrow('ENOENT')
|
|
})
|
|
})
|