test: remove low-value editor prop and helper tests (#38366)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-04 20:50:27 -08:00
committed by GitHub
co-authored by OrcaWin
parent 20b28609f4
commit 46be7e06bb
11 changed files with 4 additions and 1440 deletions
-1
View File
@@ -7023,7 +7023,6 @@
"src/renderer/src/components/editor/rich-markdown/schema/drag-selection-guard-lifetime.test.ts": 626,
"src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts": 1931,
"src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts": 2324,
"src/renderer/src/components/editor/autosave/editor-autosave.test.ts": 529,
"src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-adapter.test.ts": 2812,
"src/renderer/src/components/editor/model-lifetime/buffer-text/editor-buffer-sharing.test.ts": 5929,
"src/renderer/src/components/editor/save/bytes/editor-bytes-documents.test.ts": 690,
@@ -9,7 +9,7 @@ import type { GitStatusEntry } from '../../../../shared/git/status/git-status-ty
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
import { createEditorStore } from './autosave/editor-autosave-controller-test-fixture'
import { createEditorPanelDraftSelector } from './editor-panel-draft-selector'
import { editorTabModelUri } from './save/queue/editor-save-disk-precondition'
import { EditorConflictReviewSurface } from './EditorConflictReviewSurface'
type MonacoProps = {
@@ -163,33 +163,6 @@ afterEach(() => {
})
describe('conflict-review overview as a live read-only view of the record', () => {
it("shows the record's unsaved text from another workspace's view, on that record's model", () => {
const { store, view, review } = setup()
store.getState().setEditorDraft(view.id, 'unsaved')
store.getState().markFileDirty(view.id, true)
const overview = render(store, review, 'disk')
expect(overview.content).toBe('unsaved')
expect(overview.readOnly).toBe(true)
expect(overview.fileId).toBe(view.id)
const state = store.getState()
const overviewView = state.openFiles.find((file) => file.id === overview.fileId)!
expect(editorTabModelUri(state, { ...overviewView, filePath: overview.filePath })).toBe(
editorTabModelUri(state, view)
)
})
it('follows typing in another view of the file', () => {
const { store, view, review } = setup()
expect(render(store, review, 'disk').content).toBe('disk')
store.getState().setEditorDraft(view.id, 'a')
store.getState().markFileDirty(view.id, true)
expect(render(store, review, 'disk').content).toBe('a')
store.getState().setEditorDraft(view.id, 'ab')
expect(render(store, review, 'disk').content).toBe('ab')
})
it("can't edit or save the record", () => {
const { store, view, review } = setup()
store.getState().setEditorDraft(view.id, 'unsaved')
@@ -222,71 +195,4 @@ describe('conflict-review overview as a live read-only view of the record', () =
expect(state.openFiles[0].isDirty).toBe(true)
expect(state.editorBufferKept).toEqual({})
})
it('shows disk text when no view holds the file', () => {
const { store, view, review } = setup()
store.getState().closeFile(view.id)
const overview = render(store, review, 'disk')
expect(overview.fileId).toBe(PATH)
expect(overview.content).toBe('disk')
})
it("retries a failed read through the borrowed view's own workspace", () => {
const { store, view, review } = setup()
const reloadContent = vi.fn()
render(store, review, 'disk', { loadError: 'denied', reloadContent })
retries.current[0]()
// Retry is a user act that may start a stopped WSL distro.
expect(reloadContent).toHaveBeenCalledWith(view, { wakeWslDistro: true })
})
it('applies a reveal explicitly owned by this overview and rejects another pane', () => {
const { store, view, review } = setup()
const reveal = { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
const ownOverview = render(store, review, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1:overview:file.ts' }
})
expect(ownOverview.revealLine).toBe(9)
const anotherPane = render(store, review, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
})
expect(anotherPane.revealLine).toBeUndefined()
})
it('keeps a selected review editor scoped to its review pane', () => {
const { store, review } = setup()
store.getState().openConflictReviewFile(review.id, 'wt-2', '/repo', ENTRY, 'typescript')
const selectedReview = store.getState().openFiles.find((file) => file.id === review.id)
if (!selectedReview?.conflictReview?.selectedFileId) {
throw new Error('Expected selected conflict')
}
const reveal = {
fileId: selectedReview.conflictReview.selectedFileId,
filePath: PATH,
line: 9,
column: 1,
matchLength: 0
}
expect(
render(store, selectedReview, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
}).revealLine
).toBeUndefined()
expect(
render(store, selectedReview, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1' }
}).revealLine
).toBe(9)
})
it("mounts as a passive mirror: the view's workspace, no cursor tracking, ignores untargeted reveals", () => {
const { store, view, review } = setup()
const overview = render(store, review, 'disk', {
pendingEditorReveal: { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
})
expect(overview.worktreeId).toBe('wt-1')
expect(overview.passiveMirror).toBe(true)
expect(overview.revealLine).toBeUndefined()
})
})
@@ -2,7 +2,6 @@
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids'
const lifecycle = vi.hoisted(() => ({
events: [] as string[],
@@ -230,28 +229,6 @@ afterEach(() => {
})
describe('EditorContent Monaco lifecycle boundary', () => {
it('keeps default-pane view state separate for the same path on different runtime hosts', () => {
const filePath = '/repo/main.ts'
const worktreeId = 'repo::/repo'
const first = file(filePath, {
id: buildOwnedEditorFileId(filePath, worktreeId, 'host-a'),
runtimeEnvironmentId: 'host-a'
})
const second = file(filePath, {
id: buildOwnedEditorFileId(filePath, worktreeId, 'host-b'),
runtimeEnvironmentId: 'host-b'
})
const view = render(
<EditorContent {...props(first, 'first host')} viewStateScopeId={first.id} />
)
const firstKey = lifecycle.viewStateKeys.at(-1)
expect(firstKey).toBeTypeOf('string')
view.rerender(<EditorContent {...props(second, 'second host')} viewStateScopeId={second.id} />)
expect(lifecycle.viewStateKeys.at(-1)).not.toBe(firstKey)
})
it('unmounts the prior path before reconciling a stale retained target model', () => {
const first = file('/repo/first.ts')
const second = file('/repo/second.ts')
@@ -302,70 +279,4 @@ describe('EditorContent Monaco lifecycle boundary', () => {
'mount-diff:/repo/notes.ts'
])
})
it('passes live-tail ownership only for a read-only live log', () => {
const liveLog = file('/repo/session.jsonl', { readOnly: true, liveTail: true })
render(<EditorContent {...props(liveLog, 'session content')} />)
expect(lifecycle.mountedProps).toEqual([
{ filePath: liveLog.filePath, readOnly: true, liveTail: true, viewStateId: 'same-pane' }
])
})
it('tells Monaco which pane it is so it can retire an explicit focus handoff', () => {
const source = file('/repo/main.ts')
render(<EditorContent {...props(source, 'source content')} />)
// Missing pane IDs leave reveals armed, allowing later rich-mode remounts to steal focus.
expect(lifecycle.mountedProps.at(0)?.viewStateId).toBe('same-pane')
})
it('preserves notebook save ownership and worktree routing across the extracted surface', () => {
const notebook = file('/repo/analysis.ipynb', { language: 'notebook' })
const notebookProps = props(notebook, '{"cells": []}')
render(
<EditorContent {...notebookProps} resolvedLanguage="notebook" isNotebook mdViewMode="rich" />
)
expect(lifecycle.notebookProps).toHaveLength(1)
expect(lifecycle.notebookProps[0]).toMatchObject({
fileId: notebook.id,
filePath: notebook.filePath,
worktreeId: notebook.worktreeId,
scrollCacheKey: `${notebook.filePath}::same-pane:notebook`,
onContentChange: notebookProps.handleContentChange,
onSave: notebookProps.handleSave
})
})
it('forwards SSH and runtime ownership to rich markdown after extraction', () => {
const markdown = file('/repo/notes.md', {
language: 'markdown',
externalSshTargetId: 'ssh-target',
runtimeEnvironmentId: 'runtime-environment'
})
render(
<EditorContent
{...props(markdown, '# Notes')}
resolvedLanguage="markdown"
isMarkdown
mdViewMode="rich"
inlineMarkdownRenderState={{
renderMode: 'rich-editor',
richModeUnsupportedMessage: null
}}
/>
)
expect(lifecycle.richMarkdownProps).toHaveLength(1)
expect(lifecycle.richMarkdownProps[0]).toMatchObject({
externalSshTargetId: 'ssh-target',
runtimeEnvironmentId: 'runtime-environment',
worktreeId: markdown.worktreeId
})
})
})
@@ -99,72 +99,6 @@ describe('EditorPanelHeaderPath inline rename', () => {
vi.stubGlobal('cancelAnimationFrame', vi.fn())
})
it('opens a field holding the whole name', () => {
renderPath(baseFile())
openRenameInput()
const input = getRenameInput('Rename file notes.md')
expect(input.value).toBe('notes.md')
})
it('lets the field claim the full header width', () => {
renderPath(baseFile())
openRenameInput()
const input = getRenameInput('Rename file notes.md')
expect(input.className).toContain('w-full')
expect(input.className).toContain('max-w-full')
})
it('selects the basename so typing replaces just the name', () => {
renderPath(baseFile())
openRenameInput()
const input = getRenameInput('Rename file notes.md')
expect(document.activeElement).toBe(input)
expect(input.selectionStart).toBe(0)
expect(input.selectionEnd).toBe('notes'.length)
})
it('renames to the typed name verbatim', () => {
renderPath(baseFile())
openRenameInput()
const input = getRenameInput('Rename file notes.md')
fireEvent.change(input, { target: { value: 'renamed.mdx' } })
fireEvent.keyDown(input, { key: 'Enter' })
expect(renameFileOnDiskMock).toHaveBeenCalledWith({
oldPath: '/repo/notes.md',
newName: 'renamed.mdx',
worktreeId: 'wt-1',
worktreePath: '/repo',
documentScoped: false
})
})
it('commits on blur like the tab bar and file explorer', () => {
renderPath(baseFile())
openRenameInput()
const input = getRenameInput('Rename file notes.md')
fireEvent.change(input, { target: { value: 'renamed.md' } })
fireEvent.blur(input)
expect(renameFileOnDiskMock).toHaveBeenCalledWith(
expect.objectContaining({ newName: 'renamed.md' })
)
})
it('does not request a rename when the name was not edited', () => {
renderPath(baseFile())
openRenameInput()
fireEvent.keyDown(getRenameInput('Rename file notes.md'), { key: 'Enter' })
expect(renameFileOnDiskMock).not.toHaveBeenCalled()
})
it('ignores IME confirmation and its redispatch before accepting deliberate Enter', () => {
renderPath(baseFile())
openRenameInput()
@@ -236,56 +170,4 @@ describe('EditorPanelHeaderPath inline rename', () => {
expect(renameFileOnDiskMock).not.toHaveBeenCalled()
})
it('selects the whole name when there is no extension', () => {
const file = baseFile({
id: '/repo/Makefile',
filePath: '/repo/Makefile',
relativePath: 'Makefile'
})
renderPath(file)
openRenameInput()
const input = getRenameInput('Rename file Makefile')
expect(input.selectionStart).toBe(0)
expect(input.selectionEnd).toBe('Makefile'.length)
})
})
describe('EditorPanelHeaderPath reveal in file manager', () => {
const openInFileManager = vi.fn()
function openPathMenu(): HTMLElement {
const pathRow = document.querySelector('.editor-header-path-row')
if (!pathRow) {
throw new Error('Missing editor header path row')
}
fireEvent.contextMenu(pathRow)
return screen.getByRole('menuitem', { name: /Open Containing Folder/ })
}
beforeEach(() => {
openInFileManager.mockReset().mockResolvedValue({ ok: true })
Object.assign(window, { api: { shell: { openInFileManager } } })
})
it('reveals the open file through the shared reveal action', () => {
renderPath(baseFile())
fireEvent.click(openPathMenu())
expect(openInFileManager).toHaveBeenCalledWith('/repo/notes.md', 'local')
})
it.each([
['a remote runtime owns', { runtimeEnvironmentId: 'env-1' }],
['opened from an SSH host outside the workspace', { externalSshTargetId: 'ssh-1' }]
])('disables reveal as local-only for a file %s', (_owner, overrides) => {
renderPath(baseFile(overrides))
const reveal = openPathMenu()
expect(reveal.getAttribute('aria-disabled')).toBe('true')
expect(reveal.textContent).toContain('Local only')
})
})
@@ -56,48 +56,6 @@ describe('editor autosave changed-on-disk conflict flow', () => {
__clearSelfWriteRegistryForTests()
})
it('preserves dirty drafts and marks the tab changed-on-disk on external file change', () => {
stubEditorWindow()
const store = createEditorStore()
openDirtyFile(store)
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
dispatchExternalChange()
const file = store.getState().openFiles[0]
expect(file?.isDirty).toBe(true)
expect(file?.externalMutation).toBe('changed')
expect(store.getState().editorDrafts['/repo/file.ts']).toBe('unsaved edit')
} finally {
cleanup()
}
})
it('clears drafts and a stale changed-on-disk mark for clean tabs on external file change', () => {
stubEditorWindow()
const store = createEditorStore()
store.getState().openFile({
filePath: '/repo/file.ts',
relativePath: 'file.ts',
worktreeId: 'wt-1',
language: 'typescript',
mode: 'edit'
})
store.getState().setExternalMutation('/repo/file.ts', 'changed')
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
dispatchExternalChange()
const file = store.getState().openFiles[0]
expect(file?.isDirty).toBe(false)
expect(file?.externalMutation).toBeUndefined()
} finally {
cleanup()
}
})
it('does not backstop-mark a dirty tab for the echo of its own save', () => {
stubEditorWindow()
const store = createEditorStore()
@@ -154,21 +112,6 @@ describe('editor autosave changed-on-disk conflict flow', () => {
return { fakeDisk, store, cleanup: attachEditorAutosaveController(store) }
}
it.each([
['disk still holds the baseline', 'base'],
['disk already holds the draft', 'restored draft']
])('lands and goes clean without a banner when %s', async (_label, disk) => {
const { fakeDisk, store, cleanup } = restore(disk)
try {
await vi.advanceTimersByTimeAsync(1500)
expect(fakeDisk.files.get('/repo/file.ts')).toBe('restored draft')
expect(store.getState().openFiles[0]).toMatchObject({ isDirty: false })
expect(store.getState().openFiles[0]?.externalMutation).toBeUndefined()
} finally {
cleanup()
}
})
it('is refused with the banner, keeps the draft and stops autosaving when disk changed', async () => {
const { fakeDisk, store, cleanup } = restore('agent write')
try {
@@ -212,28 +155,4 @@ describe('editor autosave changed-on-disk conflict flow', () => {
cleanup()
}
})
it("clears the changed-on-disk mark after the banner's Overwrite", async () => {
const writeFile = stubEditorWindow()
const store = createEditorStore()
openDirtyFile(store, 'user version')
store.getState().setExternalMutation('/repo/file.ts', 'changed')
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
await requestEditorFileSave({ fileId: '/repo/file.ts', overwrite: true })
expect(writeFile).toHaveBeenCalledWith({
filePath: '/repo/file.ts',
content: 'user version',
connectionId: undefined,
expectedExecutionHostId: 'local'
})
const file = store.getState().openFiles[0]
expect(file?.isDirty).toBe(false)
expect(file?.externalMutation).toBeUndefined()
} finally {
cleanup()
}
})
})
@@ -21,13 +21,9 @@ import {
import { clearRuntimeCompatibilityCacheForTests } from '@/runtime/runtime-rpc-client'
import { getDefaultSettings } from '../../../../../shared/constants'
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
import { folderWorkspaceKey } from '../../../../../shared/workspace-scope'
import { emptyTabLayoutState } from '../../../store/slices/editor-slice-test-harness'
import { withLoadedBaselines } from './editor-autosave-controller-test-fixture'
import {
makeFolderWorkspace,
makeProjectGroup
} from '@/components/sidebar/workspace-creation/folder/folder-workspace-composer-submit-fixtures'
const mocks = vi.hoisted(() => ({
getConnectionIdForFile: vi.fn()
@@ -166,220 +162,6 @@ describe('attachEditorAutosaveController', () => {
__clearSelfWriteRegistryForTests()
})
it('saves dirty files even when the visible EditorPanel is not mounted', async () => {
const writeFile = vi.fn().mockResolvedValue(undefined)
const eventTarget = new EventTarget()
vi.stubGlobal('window', {
addEventListener: eventTarget.addEventListener.bind(eventTarget),
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
api: {
fs: {
writeFile
}
}
} satisfies WindowStub)
const store = createEditorStore()
store.getState().openFile({
filePath: '/repo/file.ts',
relativePath: 'file.ts',
worktreeId: 'wt-1',
language: 'typescript',
mode: 'edit'
})
store.getState().setEditorDraft('/repo/file.ts', 'edited')
store.getState().markFileDirty('/repo/file.ts', true)
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
await requestDirtyFileSave()
expect(writeFile).toHaveBeenCalledWith(
expect.objectContaining({
filePath: '/repo/file.ts',
content: 'edited',
connectionId: undefined,
expectedExecutionHostId: 'local'
})
)
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
expect(store.getState().editorDrafts).toEqual({})
} finally {
cleanup()
}
})
it('saves folder workspace files through the path-specific SSH connection', async () => {
const writeFile = vi.fn().mockResolvedValue(undefined)
const eventTarget = new EventTarget()
vi.stubGlobal('window', {
addEventListener: eventTarget.addEventListener.bind(eventTarget),
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
api: {
fs: {
writeFile
}
}
} satisfies WindowStub)
const store = createEditorStore()
const workspaceKey = folderWorkspaceKey('folder-workspace-1')
store.setState({
folderWorkspaces: [
makeFolderWorkspace({
folderPath: '/home/neil/platform',
executionHostId: 'ssh:ssh-1',
connectionId: 'ssh-1'
})
],
projectGroups: [{ ...makeProjectGroup(), executionHostId: 'ssh:ssh-1' }],
worktreesByRepo: {
'folder-workspace-1': [
makeWorktree({
id: workspaceKey,
repoId: 'folder-workspace-1',
path: '/home/neil/platform',
hostId: 'ssh:ssh-1'
})
]
},
sshConnectionStates: new Map([
[
'ssh-1',
{
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0,
connectionGeneration: 4
}
]
])
})
store.getState().openFile({
filePath: '/home/neil/platform/api/src/file.ts',
relativePath: 'api/src/file.ts',
worktreeId: workspaceKey,
language: 'typescript',
mode: 'edit'
})
store.getState().setEditorDraft('/home/neil/platform/api/src/file.ts', 'edited')
store.getState().markFileDirty('/home/neil/platform/api/src/file.ts', true)
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
await requestDirtyFileSave()
expect(writeFile).toHaveBeenCalledWith(
expect.objectContaining({
filePath: '/home/neil/platform/api/src/file.ts',
content: 'edited',
connectionId: 'ssh-1',
expectedExecutionHostId: 'ssh:ssh-1',
expectedSshTargetId: 'ssh-1',
expectedSshConnectionGeneration: 4
})
)
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
} finally {
cleanup()
}
})
it('saves runtime-owned folder workspace files through the folder root', async () => {
clearRuntimeCompatibilityCacheForTests()
const writeFile = vi.fn().mockResolvedValue(undefined)
const runtimeCall = vi.fn().mockResolvedValue({
ok: true,
result: {},
_meta: { runtimeId: 'runtime-env-1' }
})
const runtimeTransportCall = vi.fn((args: RuntimeEnvironmentCallRequest) => {
return (
createCompatibleRuntimeStatusResponseIfNeeded(args, 'runtime-env-1') ?? runtimeCall(args)
)
})
const eventTarget = new EventTarget()
vi.stubGlobal('window', {
addEventListener: eventTarget.addEventListener.bind(eventTarget),
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
api: {
fs: { writeFile },
runtimeEnvironments: { call: runtimeTransportCall }
}
} satisfies WindowStub)
const store = createEditorStore()
const workspaceId = 'folder-workspace-runtime'
const workspaceKey = folderWorkspaceKey(workspaceId)
store.setState({
settings: {
editorAutoSave: true,
editorAutoSaveDelayMs: 1000,
activeRuntimeEnvironmentId: 'env-2'
} as never,
worktreesByRepo: {},
folderWorkspaces: [
{
id: workspaceId,
projectGroupId: 'group-runtime',
folderPath: '/runtime/folder',
connectionId: null
} as never
],
projectGroups: [
{
id: 'group-runtime',
connectionId: null,
executionHostId: 'runtime:env-1'
} as never
]
})
const fileId = store.getState().openFile({
filePath: '/runtime/folder/src/file.ts',
relativePath: 'src/file.ts',
worktreeId: workspaceKey,
language: 'typescript',
mode: 'edit'
})
store.getState().setEditorDraft(fileId, 'edited')
store.getState().markFileDirty(fileId, true)
const cleanup = attachEditorAutosaveController(withLoadedBaselines(store))
try {
await requestDirtyFileSave()
expect(runtimeCall).toHaveBeenCalledWith(
expect.objectContaining({
selector: 'env-1',
method: 'files.writeIfUnchanged',
expectedEnvironmentPairingRevision: undefined,
params: {
worktree: `id:${workspaceKey}`,
relativePath: 'src/file.ts',
content: 'edited',
expectedExecutionHostId: 'local',
diskPrecondition: expect.objectContaining({ allowMissing: true })
},
timeoutMs: 15_000
})
)
expect(writeFile).not.toHaveBeenCalled()
expect(store.getState().openFiles[0]?.isDirty).toBe(false)
} finally {
cleanup()
}
})
it.each(['unique', 'conflict-first', 'conflict-last'] as const)(
'keeps legacy runtime saves on a unique published directory (%s)',
async (variant) => {
@@ -1,327 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor/types/open-file'
import {
canAutoSaveOpenFile,
getOpenFilesForExternalFileChange,
isExternalReloadableEditorTab,
normalizeAutoSaveDelayMs,
ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT,
ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT,
requestEditorFileClose,
requestEditorFileSave,
requestEditorSaveQuiesce
} from './editor-autosave'
type WindowEventStub = Pick<
Window,
'addEventListener' | 'removeEventListener' | 'dispatchEvent' | 'setTimeout' | 'clearTimeout'
>
function makeOpenFile(overrides: Partial<OpenFile> = {}): OpenFile {
return {
id: '/repo/file.ts',
filePath: '/repo/file.ts',
relativePath: 'file.ts',
worktreeId: 'wt-1',
language: 'typescript',
isDirty: false,
mode: 'edit',
...overrides
}
}
beforeEach(() => {
const eventTarget = new EventTarget()
vi.stubGlobal('window', {
addEventListener: eventTarget.addEventListener.bind(eventTarget),
removeEventListener: eventTarget.removeEventListener.bind(eventTarget),
dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis)
} satisfies WindowEventStub)
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('canAutoSaveOpenFile', () => {
it('allows normal edit tabs and unstaged single-file diffs', () => {
expect(canAutoSaveOpenFile(makeOpenFile())).toBe(true)
expect(
canAutoSaveOpenFile(
makeOpenFile({
id: 'wt-1::diff::unstaged::file.ts',
mode: 'diff',
diffSource: 'unstaged'
})
)
).toBe(true)
})
it('rejects staged, combined, and conflict-review tabs', () => {
expect(
canAutoSaveOpenFile(
makeOpenFile({
id: 'wt-1::diff::staged::file.ts',
mode: 'diff',
diffSource: 'staged'
})
)
).toBe(false)
expect(
canAutoSaveOpenFile(
makeOpenFile({
id: 'wt-1::all-diffs::uncommitted',
filePath: '/repo',
relativePath: 'All Changes',
mode: 'diff',
diffSource: 'combined-uncommitted'
})
)
).toBe(false)
expect(
canAutoSaveOpenFile(
makeOpenFile({
id: 'wt-1::conflicts',
filePath: '/repo',
relativePath: 'Conflicts',
mode: 'conflict-review'
})
)
).toBe(false)
})
it('rejects read-only edit tabs (AI Vault View Log)', () => {
expect(canAutoSaveOpenFile(makeOpenFile({ readOnly: true }))).toBe(false)
})
})
describe('normalizeAutoSaveDelayMs', () => {
it('defaults and clamps invalid values', () => {
expect(normalizeAutoSaveDelayMs(undefined)).toBe(1000)
expect(normalizeAutoSaveDelayMs(Number.NaN)).toBe(1000)
expect(normalizeAutoSaveDelayMs('750')).toBe(750)
expect(normalizeAutoSaveDelayMs('oops')).toBe(1000)
expect(normalizeAutoSaveDelayMs(10)).toBe(250)
expect(normalizeAutoSaveDelayMs(25_000)).toBe(10_000)
})
})
describe('requestEditorSaveQuiesce', () => {
it('resolves immediately when no editor listener claims the request', async () => {
const release = await requestEditorSaveQuiesce({ fileId: 'file-1' })
expect(release).toBeTypeOf('function')
expect(release()).toBeUndefined()
})
it('waits for a claiming listener to finish quiescing', async () => {
let resolved = false
const handler = (event: Event): void => {
const detail = (event as CustomEvent).detail as {
claim: () => void
resolve: () => void
}
detail.claim()
window.setTimeout(() => detail.resolve(), 0)
}
window.addEventListener(ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT, handler as EventListener)
try {
const promise = requestEditorSaveQuiesce({ fileId: 'file-1' }).then(() => {
resolved = true
})
expect(resolved).toBe(false)
await promise
expect(resolved).toBe(true)
} finally {
window.removeEventListener(ORCA_EDITOR_QUIESCE_FILE_SAVES_EVENT, handler as EventListener)
}
})
})
describe('requestEditorFileSave', () => {
it('rejects when no save controller claims the request', async () => {
await expect(requestEditorFileSave({ fileId: 'file-1' })).rejects.toThrow(
'Editor save controller is unavailable.'
)
})
})
describe('requestEditorFileClose', () => {
it('dispatches a close request event with the file id', () => {
const listener = vi.fn()
window.addEventListener(ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT, listener as EventListener)
try {
requestEditorFileClose('file-1')
expect(listener).toHaveBeenCalledTimes(1)
const event = listener.mock.calls[0][0] as CustomEvent<{ fileId: string }>
expect(event.detail).toEqual({ fileId: 'file-1' })
} finally {
window.removeEventListener(ORCA_EDITOR_REQUEST_FILE_CLOSE_EVENT, listener as EventListener)
}
})
})
describe('isExternalReloadableEditorTab', () => {
it('includes edit, preview, and single-file staged/unstaged diff tabs', () => {
expect(isExternalReloadableEditorTab(makeOpenFile())).toBe(true)
expect(
isExternalReloadableEditorTab(
makeOpenFile({ mode: 'markdown-preview', language: 'markdown' })
)
).toBe(true)
expect(
isExternalReloadableEditorTab(
makeOpenFile({ mode: 'diff', diffSource: 'unstaged', id: 'diff-unstaged' })
)
).toBe(true)
expect(
isExternalReloadableEditorTab(
makeOpenFile({ mode: 'diff', diffSource: 'staged', id: 'diff-staged' })
)
).toBe(true)
expect(
isExternalReloadableEditorTab(
makeOpenFile({ mode: 'diff', diffSource: 'combined-uncommitted', id: 'combined' })
)
).toBe(false)
})
})
describe('getOpenFilesForExternalFileChange', () => {
it('matches edit tabs and unstaged diff tabs for the same worktree file', () => {
const matchingEdit = makeOpenFile()
const matchingPreview = makeOpenFile({
id: 'markdown-preview::/repo/file.ts',
mode: 'markdown-preview',
language: 'markdown',
markdownPreviewSourceFileId: '/repo/file.ts'
})
const matchingUnstagedDiff = makeOpenFile({
id: 'wt-1::diff::unstaged::file.ts',
mode: 'diff',
diffSource: 'unstaged'
})
const stagedDiff = makeOpenFile({
id: 'wt-1::diff::staged::file.ts',
mode: 'diff',
diffSource: 'staged'
})
const otherWorktree = makeOpenFile({
id: '/other/file.ts',
filePath: '/other/file.ts',
worktreeId: 'wt-2'
})
expect(
getOpenFilesForExternalFileChange(
[matchingEdit, matchingPreview, matchingUnstagedDiff, stagedDiff, otherWorktree],
{
worktreeId: 'wt-1',
worktreePath: '/repo',
relativePath: 'file.ts'
}
).map((file) => file.id)
).toEqual([
'/repo/file.ts',
'markdown-preview::/repo/file.ts',
'wt-1::diff::unstaged::file.ts',
'wt-1::diff::staged::file.ts'
])
})
it('filters same-path matches by runtime owner when the watcher supplies one', () => {
const localEdit = makeOpenFile({ id: 'local-edit', runtimeEnvironmentId: null })
const runtimeEdit = makeOpenFile({ id: 'runtime-edit', runtimeEnvironmentId: 'env-1' })
const runtimeDiff = makeOpenFile({
id: 'runtime-diff',
mode: 'diff',
diffSource: 'unstaged',
runtimeEnvironmentId: 'env-1'
})
expect(
getOpenFilesForExternalFileChange([localEdit, runtimeEdit, runtimeDiff], {
worktreeId: 'wt-1',
worktreePath: '/repo',
relativePath: 'file.ts',
runtimeEnvironmentId: null
}).map((file) => file.id)
).toEqual(['local-edit'])
expect(
getOpenFilesForExternalFileChange([localEdit, runtimeEdit, runtimeDiff], {
worktreeId: 'wt-1',
worktreePath: '/repo',
relativePath: 'file.ts',
runtimeEnvironmentId: 'env-1'
}).map((file) => file.id)
).toEqual(['runtime-edit', 'runtime-diff'])
})
it('matches Windows edit tabs across separator and drive spellings, but not tail case', () => {
const tab = makeOpenFile({ id: 'tab', filePath: 'c:\\Repo\\File.ts' })
const otherCase = makeOpenFile({ id: 'other-case', filePath: 'c:\\Repo\\file.ts' })
expect(
getOpenFilesForExternalFileChange([tab, otherCase], {
worktreeId: 'wt-1',
worktreePath: 'C:/Repo',
relativePath: 'File.ts'
})
).toEqual([tab])
})
it('matches restored WSL aliases only for a proven local Windows watcher', () => {
vi.stubGlobal('navigator', { userAgent: 'Windows' })
const terminalLinkTab = makeOpenFile({
id: '//wsl.localhost/Ubuntu/workspace/repo/file.ts',
filePath: '//wsl.localhost/Ubuntu/workspace/repo/file.ts',
worktreeId: 'wt-wsl'
})
expect(
getOpenFilesForExternalFileChange([terminalLinkTab], {
worktreeId: 'wt-wsl',
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
relativePath: 'file.ts',
runtimeEnvironmentId: null,
allowLocalWindowsWslAliases: true
}).map((file) => file.id)
).toEqual(['//wsl.localhost/Ubuntu/workspace/repo/file.ts'])
expect(
getOpenFilesForExternalFileChange([terminalLinkTab], {
worktreeId: 'wt-wsl',
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
relativePath: 'file.ts',
runtimeEnvironmentId: null
})
).toEqual([])
vi.stubGlobal('navigator', { userAgent: 'Linux' })
expect(
getOpenFilesForExternalFileChange([terminalLinkTab], {
worktreeId: 'wt-wsl',
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
relativePath: 'file.ts',
runtimeEnvironmentId: null,
allowLocalWindowsWslAliases: true
})
).toEqual([])
vi.stubGlobal('navigator', { userAgent: 'Windows' })
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
expect(
getOpenFilesForExternalFileChange([terminalLinkTab], {
worktreeId: 'wt-wsl',
worktreePath: '\\\\wsl.localhost\\Ubuntu\\workspace\\repo',
relativePath: 'file.ts',
runtimeEnvironmentId: null,
allowLocalWindowsWslAliases: true
})
).toEqual([])
})
})
@@ -126,106 +126,6 @@ afterEach(() => {
})
describe('synthetic outcome payloads through real sidebar and header actions', () => {
it('keeps the provider action and truthful manual output without inventing failed steps', () => {
const manualCheck = { ...check, conclusion: 'neutral' as const }
const details = {
...readingDetails(),
conclusion: 'neutral',
summary: 'Waiting for manual approval.',
jobs: []
}
render(
<TooltipProvider>
<CheckRunDetailsPanel
check={manualCheck}
details={details}
loading={false}
error={null}
openUrl={check.url}
worktreeId={null}
/>
</TooltipProvider>
)
expect(screen.getByText('Waiting for manual approval.')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Open details' })).toBeTruthy()
expect(screen.queryByText('Failed jobs')).toBeNull()
expect(screen.queryByText('Log excerpt')).toBeNull()
expect(document.body.textContent).not.toMatch(/expired|Couldn't load logs|steps failed/)
})
it.each(['available', 'empty', 'not_requested', undefined] as const)(
'does not invent first-text navigation from null outcome %s',
async (outcome) => {
const first = readingDetails(null, outcome)
const pending = gate()
useAppStore.setState({ fetchPRCheckDetails: vi.fn(() => pending.promise) })
open(first)
const marker = vi.spyOn(diagnosticPosition, 'findInitialCheckLogDiagnostic')
render(
<TooltipProvider>
<StoredPanel />
</TooltipProvider>
)
expect(scrollTopCache.size).toBe(1)
expect([...scrollTopCache.keys()].some((key) => key.includes('::intent:'))).toBe(false)
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
await act(async () => pending.resolve(readingDetails(undefined, 'available')))
expect(screen.getByTestId('outcome-pane').querySelector('pre')?.scrollTop).toBe(0)
expect(marker).not.toHaveBeenCalled()
}
)
it('ends zero-layout first-text setup and cannot revive it after close or late frames', async () => {
const frames = new Map<number, FrameRequestCallback>()
let frameId = 0
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
frames.set(++frameId, callback)
return frameId
})
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation((key) => {
frames.delete(key)
})
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(0)
const first = payload()
const pending = gate()
useAppStore.setState({ fetchPRCheckDetails: vi.fn(() => pending.promise) })
open(first)
const marker = vi.spyOn(diagnosticPosition, 'findInitialCheckLogDiagnostic')
const { unmount } = render(
<TooltipProvider>
<StoredPanel />
</TooltipProvider>
)
expect(marker).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
await act(async () =>
pending.resolve(payload('Error: synthetic delayed first text', 'available'))
)
const queued = [...frames.values()]
act(() => {
frames.clear()
for (const callback of queued) {
callback(0)
}
})
expect(marker).not.toHaveBeenCalled()
expect(frames.size).toBe(0)
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(200)
fireEvent.resize(window)
expect(marker).not.toHaveBeenCalled()
act(() => {
useAppStore.setState({ openFiles: [] })
})
scrollTopCache.clear()
act(() => {
for (const callback of queued) {
callback(0)
}
})
unmount()
expect(scrollTopCache.size).toBe(0)
expect(frames.size).toBe(0)
})
it.each([false, true])(
'retries one combined request with unchanged identities, already open=%s',
async (alreadyOpen) => {
@@ -2,20 +2,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { scrollTopCache, setWithLRU } from '@/lib/scroll-cache'
import {
checkDetailsReadingKey,
checkLogDigest,
checkReadingIntentKey,
checkReadingLogKey,
checkReadingOuterKey,
checkReadingStepsKey,
reconcileCheckDetailsReadingCache
checkReadingOuterKey
} from './check-details-reading-cache'
import { createCheckDetailsReadingOwner } from './check-details-reading-owner'
import { attachCheckReadingViewport } from './check-details-reading-viewport'
import {
measuredReadingNode,
readingDetails,
readingFile,
readingFixture
} from './check-details-reading-fixture'
import * as digest from '../../../../../../shared/sha256'
@@ -52,49 +47,6 @@ function flushFrames(): void {
}
describe('owned reading positions', () => {
it.each([false, true])(
'allows first payload navigation only while deliberate intent survives, cancelled=%s',
(cancelled) => {
const { owner, store, file, identity, text, logKey } = readingFixture()
const pending = { ...file.checkRunDetails!, details: null }
store.setState({ openFiles: [{ ...file, checkRunDetails: pending }] })
reconcileCheckDetailsReadingCache(file.id, undefined, pending, () => true)
const detachOwner = owner.attach()
const outer = measuredReadingNode()
const detachOuter = attachCheckReadingViewport({
node: outer,
owner,
cacheKey: checkReadingOuterKey(owner.key)
})
if (cancelled) {
fireReading(outer)
}
const next = file.checkRunDetails!
store.setState({ openFiles: [file] })
reconcileCheckDetailsReadingCache(file.id, pending, next, () => true)
const node = measuredReadingNode(text)
const initial = vi.fn(() => 600)
const detach = attachCheckReadingViewport({
node,
owner,
cacheKey: logKey,
job: identity,
text,
initial,
consumeIntent: () => {
const eligible = owner.claim(identity)
scrollTopCache.delete(checkReadingIntentKey(owner.key, identity))
return eligible
}
})
expect(initial).toHaveBeenCalledTimes(cancelled ? 0 : 1)
expect(node.scrollTop).toBe(cancelled ? 0 : 600)
detach()
detachOuter()
detachOwner()
}
)
it('retirement cancels a pending frame and rejects a captured callback after close and reopen', () => {
const { owner, store, file, logKey, text, identity } = readingFixture()
const node = measuredReadingNode(text, 0)
@@ -119,40 +71,6 @@ describe('owned reading positions', () => {
detachOwner()
expect(scrollTopCache.has(logKey)).toBe(false)
})
it('settled outer scrolling scans no cache keys and invokes no setup work', () => {
const { owner, store, identity, text } = readingFixture()
const node = measuredReadingNode()
const detachOwner = owner.attach()
const cancel = vi.fn()
const unregister = owner.register({
job: identity,
text,
start: () => {},
retire: () => undefined,
cancel
})
const initial = vi.fn(() => 400)
const detach = attachCheckReadingViewport({
node,
owner,
cacheKey: checkReadingOuterKey(owner.key),
initial
})
fireReading(node)
const keys = vi.spyOn(scrollTopCache, 'keys')
for (let index = 0; index < 100; index++) {
node.scrollTop = index
node.dispatchEvent(new Event('scroll'))
store.setState({ unrelated: index })
}
expect(keys).not.toHaveBeenCalled()
expect(initial).not.toHaveBeenCalled()
expect(cancel).toHaveBeenCalledTimes(1)
detach()
unregister()
detachOwner()
})
it.each(['outer', 'inner'])(
'preserves retained %s target through zero layout and a real remount',
(kind) => {
@@ -181,77 +99,6 @@ describe('owned reading positions', () => {
}
)
it('saves a valid zero in a measurable short document', () => {
const { owner, logKey, text, identity } = readingFixture()
const node = measuredReadingNode(text)
Object.defineProperty(node, 'scrollHeight', { value: 100 })
setWithLRU(scrollTopCache, logKey, 180)
const detachOwner = owner.attach()
const detach = attachCheckReadingViewport({
node,
owner,
cacheKey: logKey,
job: identity,
text
})
expect(scrollTopCache.get(logKey)).toBe(0)
detach()
detachOwner()
})
it('consumes an unmeasurable initial attempt once and cancels its sole frame on teardown', () => {
const { owner, logKey, text, identity } = readingFixture()
const node = measuredReadingNode(text, 0)
const initial = vi.fn(() => 700)
const detachOwner = owner.attach()
const detach = attachCheckReadingViewport({
node,
owner,
cacheKey: logKey,
job: identity,
text,
initial,
consumeIntent: () => true
})
expect(frames.size).toBe(1)
flushFrames()
expect(scrollTopCache.get(logKey)).toBe(0)
Object.defineProperty(node, 'clientHeight', { configurable: true, value: 200 })
node.dispatchEvent(new Event('resize'))
node.dispatchEvent(new Event('scroll'))
expect(initial).not.toHaveBeenCalled()
detach()
detachOwner()
expect(frames.size).toBe(0)
})
it('advances the watermark on another file dirty/save update and restores the later scroll', () => {
const { owner, store, logKey, text, identity } = readingFixture()
const detachOwner = owner.attach()
const node = measuredReadingNode(text)
const options = { node, owner, cacheKey: logKey, job: identity, text }
const detach = attachCheckReadingViewport(options)
store.setState({
openFiles: [
...store.getState().openFiles,
{ ...readingFile(), id: 'other', mode: 'edit', isDirty: true }
]
})
store.setState({
openFiles: store
.getState()
.openFiles.map((file) => (file.id === 'other' ? { ...file, isDirty: false } : file))
})
node.scrollTop = 450
node.dispatchEvent(new Event('scroll'))
detach()
node.scrollTop = 0
const remount = attachCheckReadingViewport(options)
expect(node.scrollTop).toBe(450)
remount()
detachOwner()
})
it('fences callbacks published before the observer and never rewinds on a reentrant notification', () => {
const { owner, store, file, logKey, text, identity } = readingFixture()
const node = measuredReadingNode(text)
@@ -315,30 +162,6 @@ describe('owned reading positions', () => {
detachOwner()
})
it('cancels first-payload and job intent with reading input, but ignores bubbled control clicks', () => {
const { owner, file, text, identity } = readingFixture()
reconcileCheckDetailsReadingCache(file.id, undefined, file.checkRunDetails!, () => true)
const unclaimed = checkReadingIntentKey(
checkDetailsReadingKey(file.id, null, 'context'),
identity
)
const node = measuredReadingNode(text)
const button = document.createElement('button')
node.append(button)
const detachOwner = owner.attach()
const detach = attachCheckReadingViewport({
node,
owner,
cacheKey: checkReadingOuterKey(owner.key)
})
button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
expect(scrollTopCache.get(unclaimed)).toBe(-1)
node.dispatchEvent(new WheelEvent('wheel'))
expect(scrollTopCache.has(unclaimed)).toBe(false)
detach()
detachOwner()
})
it('does no hashing, marker work, Range work or frame scheduling during 100 scroll/unrelated notifications', () => {
const { owner, store, logKey, text, identity } = readingFixture()
const node = measuredReadingNode(text)
@@ -367,46 +190,4 @@ describe('owned reading positions', () => {
detachOwner()
expect(remove.mock.calls.map(([name]) => name)).toEqual(add.mock.calls.map(([name]) => name))
})
it('shares the real 20-entry LRU across two five-job panes and ordinary editor keys', () => {
const { store, file, identity, text } = readingFixture()
const first = createCheckDetailsReadingOwner(store, file.id, 'left', 'context')
const second = createCheckDetailsReadingOwner(store, file.id, 'right', 'context')
for (let index = 0; index < 8; index++) {
setWithLRU(scrollTopCache, `ordinary-${index}`, index)
}
for (const owner of [first, second]) {
setWithLRU(scrollTopCache, checkReadingOuterKey(owner.key), 320)
for (let index = 0; index < 5; index++) {
setWithLRU(
scrollTopCache,
checkReadingLogKey(owner.key, `${identity}-${index}`, checkLogDigest(text)),
180
)
setWithLRU(scrollTopCache, checkReadingStepsKey(owner.key, `${identity}-${index}`, true), 0)
expect(scrollTopCache.size).toBeLessThanOrEqual(20)
}
}
expect(scrollTopCache.size).toBe(20)
expect(scrollTopCache.has(checkReadingOuterKey(first.key))).toBe(false)
expect([...scrollTopCache.values()].every((value) => typeof value === 'number')).toBe(true)
expect([...scrollTopCache.keys()].some((key) => key.includes(text))).toBe(false)
const detachOwner = first.attach()
const node = measuredReadingNode()
const initial = vi.fn(() => 600)
const detach = attachCheckReadingViewport({
node,
owner: first,
cacheKey: checkReadingOuterKey(first.key),
initial
})
expect(node.scrollTop).toBe(0)
expect(initial).not.toHaveBeenCalled()
detach()
detachOwner()
})
})
function fireReading(node: HTMLElement): void {
node.dispatchEvent(new WheelEvent('wheel'))
}
@@ -262,53 +262,3 @@ it('discards an in-flight read when the other area of its path changes', async (
expect(hook.result.current.sections[0]?.modifiedContent).toBe('current')
expect(hook.result.current.generation).toBe(generation)
})
it.each([
{
name: 'order',
change: (review: Review): Review => ({ ...review, status: review.status.toReversed() })
},
{
name: 'worktree owner',
change: (review: Review): Review => ({
...review,
file: { ...review.file, worktreeId: 'folder:other' }
})
}
])('uses the full reset when $name changes', async ({ name, change }) => {
mocks.fetch.mockResolvedValue(textResult())
const review: Review = { status: entries(3), file, viewStateKey: `fallback-${name}` }
const hook = renderReview(review)
await flush()
const before = hook.result.current
mocks.fetch.mockClear()
const next = change(review)
hook.rerender(next)
await flush()
expect(hook.result.current.generation).toBe(before.generation + 1)
expect(hook.result.current.sections[0]).not.toBe(before.sections[0])
expect(mocks.fetch.mock.calls[0]?.[0].file.worktreeId).toBe(next.file.worktreeId)
})
it('reloads a row whose submodule admission changes under an unchanged cache signature', async () => {
mocks.fetch.mockResolvedValue(textResult())
const review: Review = {
status: [{ ...entries(1)[0]!, added: undefined }],
file,
viewStateKey: 'submodule-policy'
}
const hook = renderReview(review)
await flush()
hook.rerender({
...review,
status: [
{
...review.status[0]!,
submodule: { commitChanged: true, trackedChanges: false, untrackedChanges: false }
}
]
})
await flush()
expect(hook.result.current.sections[0]?.loadOnDemand).toBe(false)
expect(hook.result.current.sections[0]?.modifiedContent).toBe('after\n')
})
@@ -285,22 +285,6 @@ describe('combined diff discarded-result analysis budget', () => {
}
)
it('keeps reloading a visible section while external writes never stop', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
mocks.fetch.mockResolvedValue(textResult('base', 'edited'))
const hook = await startViewer()
hook.result.current.registry.renderedIndicesRef.current.add(0)
// Watch batches every 100 ms: a restarting 300 ms coalesce would never fire.
for (let elapsed = 0; elapsed < 3_000; elapsed += 100) {
hook.result.current.retry.requestSectionReload(0)
await act(async () => {
await vi.advanceTimersByTimeAsync(100)
})
await flushLoads()
}
expect(mocks.fetch.mock.calls.length).toBeGreaterThanOrEqual(4)
})
it('commits slow refetches while external writes never stop', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
let version = 0
@@ -393,100 +377,12 @@ describe('combined diff discarded-result analysis budget', () => {
expect(mocks.fetch).toHaveBeenCalledTimes(2)
expect(reads.count()).toBe(0)
})
it.each(['offscreen', 'collapsed'])(
'does not force a %s stale section to reload',
async (visibility) => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const body = 'obsolete hidden diff\n'.padEnd(4096, 'x')
const pending = deferredResult()
mocks.fetch.mockReturnValueOnce(pending.promise)
const hook = await startViewer()
if (visibility === 'collapsed') {
act(() => {
hook.result.current.setSections((sections) =>
sections.map((section) => ({ ...section, collapsed: true }))
)
})
hook.result.current.registry.renderedIndicesRef.current.add(0)
}
const sections = hook.result.current.sections
hook.result.current.retry.requestSectionReload(0)
const reads = countBodyReads(body)
await settle(pending, textResult(body))
reads.restore()
expect(hook.result.current.sections).toBe(sections)
expect(hook.result.current.registry.loadedIndicesRef.current.size).toBe(0)
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
expect(hook.result.current.registry.sectionLoadTokensRef.current.get(0)).toBe(2)
expect(hook.result.current.registry.reloadTimersRef.current.size).toBe(0)
await act(async () => {
await vi.advanceTimersByTimeAsync(300)
})
expect(mocks.fetch).toHaveBeenCalledOnce()
expect(reads.count()).toBe(0)
}
)
})
describe('combined diff live result compatibility', () => {
const body = 'original body\n'.padEnd(512, 'a')
const modified = 'modified body\n'.padEnd(128, 'b')
const legacy = textResult(body, modified)
const metadata: GitDiffResult = {
...legacy,
largeDiffRenderLimit: getLargeDiffRenderLimit(legacy)
}
const hostLimitMetadata: GitDiffTextResult = {
...textResult(''),
largeDiffRenderLimit: getLargeDiffRenderLimit(textResult('\n'.repeat(120_001)))
}
const responseCases: [string, GitDiffResult, boolean][] = [
['legacy', legacy, true],
['native wire shape', structuredClone(legacy), true],
['relay wire shape', structuredClone(legacy), true],
['supplied metadata', metadata, false],
[
'binary',
{
kind: 'binary',
originalContent: body,
modifiedContent: modified,
originalIsBinary: true,
modifiedIsBinary: false,
isImage: true,
mimeType: 'application/pdf',
modifiedDeleted: true
},
false
],
['empty', textResult(''), true],
['line-limited legacy', textResult('\n'.repeat(120_001)), true],
['character-limited legacy', textResult('x'.repeat(6_000_001)), false],
['native limit metadata', structuredClone(hostLimitMetadata), false],
['relay limit metadata', structuredClone(hostLimitMetadata), false]
]
it.each(responseCases)('preserves the complete %s result', async (_name, response, scansBody) => {
const pending = deferredResult()
mocks.fetch.mockReturnValueOnce(pending.promise)
const hook = await startViewer()
const initial = hook.result.current.sections[0]!
const reads = countBodyReads(body)
await settle(pending, response)
reads.restore()
expect(hook.result.current.sections).toEqual([expectedSection(initial, response)])
if (hook.result.current.sections[0]?.largeDiffRenderLimit?.limited) {
expect(hook.result.current.sections[0]?.diffResult).not.toBe(response)
} else {
expect(hook.result.current.sections[0]?.diffResult).toBe(response)
}
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
expect(hook.result.current.sectionHeights).toEqual({})
expect(mocks.fetch).toHaveBeenCalledOnce()
expect(reads.count()).toBe(scansBody && response.originalContent === body ? body.length : 0)
})
it('preserves section identity on unchanged reload and remeasures changed content once', async () => {
const first = deferredResult()
@@ -523,27 +419,6 @@ describe('combined diff live result compatibility', () => {
expect(mocks.fetch).toHaveBeenCalledTimes(3)
})
it.each([new Error(' original remote failure '), new Error(' '), 'rejected value'])(
'keeps the existing rejection message for %s',
async (error) => {
const pending = deferredResult()
mocks.fetch.mockReturnValueOnce(pending.promise)
const hook = await startViewer()
const initial = hook.result.current.sections[0]!
await act(async () => {
pending.reject(error)
await Promise.resolve()
})
const message =
error instanceof Error && error.message.trim() ? error.message : 'Unable to load diff.'
expect(hook.result.current.sections).toEqual([
expectedSection(initial, textResult(''), message)
])
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
}
)
it('preserves the actual 30 second deadline and clears its timer after failure', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
const pending = deferredResult()
@@ -608,18 +483,4 @@ describe('combined diff live result compatibility', () => {
expect(hook.result.current.sections).toEqual([expectedSection(sections[0]!, legacy)])
expect(mocks.fetch).toHaveBeenCalledTimes(2)
})
it('preserves a synchronous fetch failure', async () => {
const failure = new Error('original synchronous failure')
mocks.fetch.mockImplementationOnce(() => {
throw failure
})
const hook = await startViewer()
expect(hook.result.current.sections[0]?.error).toBe(failure.message)
expect(hook.result.current.sections[0]?.diffResult).toEqual(textResult(''))
expect(hook.result.current.sections[0]?.largeDiffRenderLimit).toBeNull()
expect(hook.result.current.sections[0]?.loading).toBe(false)
expect(hook.result.current.registry.loadedIndicesRef.current.has(0)).toBe(true)
expect(hook.result.current.registry.loadingIndicesRef.current.size).toBe(0)
})
})