diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 47d44fe10fa..236f0156560 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -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, diff --git a/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx b/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx index 8bdb92d1dcd..55730b0cf12 100644 --- a/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx +++ b/src/renderer/src/components/editor/EditorConflictReviewSurface.live-record.test.tsx @@ -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() - }) }) diff --git a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx index 2e34f63a6c9..a1bb049ee00 100644 --- a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx @@ -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( - - ) - const firstKey = lifecycle.viewStateKeys.at(-1) - expect(firstKey).toBeTypeOf('string') - - view.rerender() - - 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() - - 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() - - // 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( - - ) - - 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( - - ) - - expect(lifecycle.richMarkdownProps).toHaveLength(1) - expect(lifecycle.richMarkdownProps[0]).toMatchObject({ - externalSshTargetId: 'ssh-target', - runtimeEnvironmentId: 'runtime-environment', - worktreeId: markdown.worktreeId - }) - }) }) diff --git a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx index e69a31afc46..7fb27c02826 100644 --- a/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx +++ b/src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx @@ -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') - }) }) diff --git a/src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts b/src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts index 95295d7f9e7..980cd353256 100644 --- a/src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts +++ b/src/renderer/src/components/editor/autosave/editor-autosave-conflict-flow.test.ts @@ -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() - } - }) }) diff --git a/src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts b/src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts index e09ffd03b50..6623b544536 100644 --- a/src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts +++ b/src/renderer/src/components/editor/autosave/editor-autosave-controller.test.ts @@ -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) => { diff --git a/src/renderer/src/components/editor/autosave/editor-autosave.test.ts b/src/renderer/src/components/editor/autosave/editor-autosave.test.ts deleted file mode 100644 index 96149eff6be..00000000000 --- a/src/renderer/src/components/editor/autosave/editor-autosave.test.ts +++ /dev/null @@ -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 { - 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([]) - }) -}) diff --git a/src/renderer/src/components/editor/check-details/panel/CheckRunDetailsPanel.log-outcomes.test.tsx b/src/renderer/src/components/editor/check-details/panel/CheckRunDetailsPanel.log-outcomes.test.tsx index bdb0c1b2002..3af6fe98a34 100644 --- a/src/renderer/src/components/editor/check-details/panel/CheckRunDetailsPanel.log-outcomes.test.tsx +++ b/src/renderer/src/components/editor/check-details/panel/CheckRunDetailsPanel.log-outcomes.test.tsx @@ -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( - - - - ) - 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( - - - - ) - 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() - 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( - - - - ) - 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) => { diff --git a/src/renderer/src/components/editor/check-details/reading/check-details-reading-viewport.test.ts b/src/renderer/src/components/editor/check-details/reading/check-details-reading-viewport.test.ts index 2bb32805872..40013f2e4b2 100644 --- a/src/renderer/src/components/editor/check-details/reading/check-details-reading-viewport.test.ts +++ b/src/renderer/src/components/editor/check-details/reading/check-details-reading-viewport.test.ts @@ -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')) -} diff --git a/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-metadata-retention.test.tsx b/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-metadata-retention.test.tsx index 74d9190426f..a0124391810 100644 --- a/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-metadata-retention.test.tsx +++ b/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-metadata-retention.test.tsx @@ -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') -}) diff --git a/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx b/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx index 64ae5c6cbfb..b28ac0e12c6 100644 --- a/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx +++ b/src/renderer/src/components/editor/combined-diff/load-sections/combined-diff-stale-analysis-work-budget.test.tsx @@ -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) - }) })