From b5f97ca533208ea5b699fdb2bbca40f2fccb8d0d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:21:42 -0700 Subject: [PATCH] Restored-tab disk re-check can't loop or raise a false conflict; drop test-only editor-state guard (#31250) * refactor(editor): drop isWholeEditorState and the diff-signature re-export; bound superseded restore re-reads - Remove isWholeEditorState; its callers take the real AppState. Tests that mocked partial stores now build full states from createTestStore. - Import getDiskBaselineSignature from shared/disk-baseline-signature directly and delete the re-export from diff-content-signature.ts. - Restored-tab conflict scan: count superseded re-reads toward MAX_VERIFY_ATTEMPTS, so a read slower than the re-verify interval gives up with the conflict banner instead of looping forever. Adds a regression test. Co-Authored-By: Claude * fix(editor): keep the last read at the superseded re-read cap instead of giving up A matching read at the cap cleared nothing and showed a false changed-on-disk banner; it now resolves like any read. Only read failures give up. Co-Authored-By: Claude * fix(editor): back off on superseded restore re-reads instead of capping them A superseded read may predate the newer check, so it never resolves the verification, even at the attempt cap. Overlapping checks now delay the next fresh read (bounded, one timer per tab) and keep verification pending; only failed reads count toward giving up. Co-Authored-By: Claude * test(editor): address the restored tab by its owned id in the overlapping-check tests After #31196 a server's tab is no longer keyed by its path, so these tests flagged a tab that does not exist and no read was ever issued. Co-Authored-By: Claude --------- Co-authored-by: Claude --- .../editor/ExternalFileChangeBanner.test.tsx | 9 +-- .../editor/ExternalFileChangeBanner.tsx | 2 +- .../editor/diff-content-signature.ts | 3 - .../editor/editor-changed-on-disk-mark.ts | 17 ++--- .../editor/editor-clean-tab-disk-baseline.ts | 2 +- ...restored-tab-conflict-scan.recheck.test.ts | 31 ++++++++- .../editor-restored-tab-conflict-scan.test.ts | 42 +++++------- .../editor-restored-tab-conflict-scan.ts | 21 ++++-- .../editor/editor-save-disk-precondition.ts | 11 ---- .../components/editor/editor-save-queue.ts | 2 +- .../editor/editor-stale-mount-guard.ts | 5 +- ...editor-untitled-dont-save-dialogs.test.tsx | 2 +- .../editor-untitled-save-lifecycle.test.ts | 2 +- ...-monaco-editor-mount.cursor-owner.test.tsx | 6 +- .../useEditorPanelContentState.test.tsx | 7 +- .../useEditorPanelExternalContentEvents.ts | 11 +--- ...EditorPanelVisibilityContentState.test.tsx | 2 + ...editor-external-watch-disk-verification.ts | 2 +- .../useEditorExternalWatch-self-move.test.ts | 27 +++++--- ...seEditorExternalWatch-switch-back.test.tsx | 2 +- .../src/hooks/useEditorExternalWatch.test.ts | 66 +++++++++++++------ .../lib/execute-open-editor-path-move.test.ts | 2 +- .../editor/tabs/untitled-file-cleanup.ts | 2 +- 23 files changed, 165 insertions(+), 111 deletions(-) diff --git a/src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx b/src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx index d48cf186e6d..9021244b904 100644 --- a/src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx +++ b/src/renderer/src/components/editor/ExternalFileChangeBanner.test.tsx @@ -30,8 +30,9 @@ import { keepTabEditsOverExternalChange, reloadTabContentFromDisk } from './ExternalFileChangeBanner' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { useAppStore } from '@/store' +import { createTestStore } from '@/store/slices/store-test-helpers' const file = { id: 'file-1', @@ -52,15 +53,15 @@ describe('ExternalFileChangeBanner', () => { function mockStoreState(editorDrafts: Record, openFiles: OpenFile[] = [file]) { vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), clearEditorDraft, markFileDirty, setExternalMutation, setEditorDraft, setLastKnownDiskSignature, editorDrafts, - openFiles, - settings: {} - } as never) + openFiles + }) } beforeEach(() => { diff --git a/src/renderer/src/components/editor/ExternalFileChangeBanner.tsx b/src/renderer/src/components/editor/ExternalFileChangeBanner.tsx index bf91d5e1513..cbc840ba641 100644 --- a/src/renderer/src/components/editor/ExternalFileChangeBanner.tsx +++ b/src/renderer/src/components/editor/ExternalFileChangeBanner.tsx @@ -10,7 +10,7 @@ import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' import type { OpenFile } from '@/store/slices/editor' import { ExternalFileChangeCompareDialog } from './ExternalFileChangeCompareDialog' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { trackExternalChangeConflictAction } from './editor-external-change-telemetry' import { attemptEditorFileSave } from './editor-file-save-attempt' import { diff --git a/src/renderer/src/components/editor/diff-content-signature.ts b/src/renderer/src/components/editor/diff-content-signature.ts index 9c74a207818..927d9711d89 100644 --- a/src/renderer/src/components/editor/diff-content-signature.ts +++ b/src/renderer/src/components/editor/diff-content-signature.ts @@ -9,6 +9,3 @@ export function getDiffContentSignature(content: string): string { } return (hash >>> 0).toString(16) } - -// Why shared: the writer that refuses a stale save hashes disk with this same function. -export { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' diff --git a/src/renderer/src/components/editor/editor-changed-on-disk-mark.ts b/src/renderer/src/components/editor/editor-changed-on-disk-mark.ts index f8c49331b1d..bbb54cdac58 100644 --- a/src/renderer/src/components/editor/editor-changed-on-disk-mark.ts +++ b/src/renderer/src/components/editor/editor-changed-on-disk-mark.ts @@ -1,12 +1,9 @@ +import type { AppState } from '@/store' import type { OpenFile } from '@/store/slices/editor' import { canAutoSaveOpenFile } from './editor-autosave' -import { editorModelSiblingTabs, isWholeEditorState } from './editor-save-disk-precondition' +import { editorModelSiblingTabs } from './editor-save-disk-precondition' import { trackExternalChangeConflictShown } from './editor-external-change-telemetry' -type ChangedOnDiskMarkState = { - setExternalMutation: (fileId: string, mutation: 'deleted' | 'renamed' | 'changed' | null) => void -} - // Why: the changed-on-disk mark has one rule — dirty, banner-capable tab, // telemetry only on the first surfacing — but three writers (the watch hook // at fs-event time, the autosave controller's notification backstop, and the @@ -14,15 +11,13 @@ type ChangedOnDiskMarkState = { // each writer keeps its own echo/eligibility guard at the call site. A code // model is dirty when any of its tabs is, and the mark reaches all of them. export function markFileChangedOnDisk( - state: ChangedOnDiskMarkState, + state: AppState, file: OpenFile, options: { connectionId: string | undefined; origin: 'live' | 'restore' } ): void { - // Why: some writers pass only the setter; the model's other tabs are known only from the whole store. - const tabs = - isWholeEditorState(state) && state.openFiles.some((tab) => tab.id === file.id) - ? editorModelSiblingTabs(state, file) - : [file] + const tabs = state.openFiles.some((tab) => tab.id === file.id) + ? editorModelSiblingTabs(state, file) + : [file] if (!tabs.some((tab) => tab.isDirty) || !canAutoSaveOpenFile(file)) { return } diff --git a/src/renderer/src/components/editor/editor-clean-tab-disk-baseline.ts b/src/renderer/src/components/editor/editor-clean-tab-disk-baseline.ts index b9dde358223..05a32b3499b 100644 --- a/src/renderer/src/components/editor/editor-clean-tab-disk-baseline.ts +++ b/src/renderer/src/components/editor/editor-clean-tab-disk-baseline.ts @@ -1,7 +1,7 @@ import { useAppStore, type AppState } from '@/store' import type { OpenFile } from '@/store/slices/editor' import { isEditorContentUnchanged } from './editor-content-dirty-state' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import type { FileContent } from './editor-panel-content-types' import { editorModelSiblingTabs, diff --git a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.recheck.test.ts b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.recheck.test.ts index f852f793b1c..d4e3abb235a 100644 --- a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.recheck.test.ts +++ b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.recheck.test.ts @@ -27,7 +27,7 @@ import { MAX_VERIFY_ATTEMPTS, requestRestoredTabReverification } from './editor-restored-tab-conflict-scan' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' const FILE = '/repo/a.ts' let store: ReturnType @@ -84,6 +84,35 @@ it('reads once more when a new check is requested while a read is in flight', as expect(tab().pendingDiskBaselineVerification).toBeFalsy() }) +it.each([ + { disk: 'v1', banner: undefined }, + { disk: 'v2 external write', banner: 'changed' } +])('waits out overlapping checks, then judges a fresh read ($disk)', async ({ disk, banner }) => { + const replies: ((content: string) => void)[] = [] + mocks.readRuntimeFileContent.mockImplementation( + () => new Promise((resolve) => replies.push((content) => resolve({ content, isBinary: false }))) + ) + store.getState().setPendingDiskBaselineVerification(fileId, true) + detach = attachRestoredTabConflictScan(store) + await vi.advanceTimersByTimeAsync(0) + expect(replies).toHaveLength(1) + + // Every read is slower than the re-verify interval, so a newer check always lands first. + for (let i = 0; i < MAX_VERIFY_ATTEMPTS + 5; i += 1) { + requestRestoredTabReverification(fileId) + replies[i]('v1') + await vi.advanceTimersByTimeAsync(60_000) + expect(replies).toHaveLength(i + 2) + } + expect(tab().pendingDiskBaselineVerification).toBe(true) + expect(tab().externalMutation).toBeUndefined() + + replies.at(-1)!(disk) + await vi.advanceTimersByTimeAsync(0) + expect(tab().pendingDiskBaselineVerification).toBeFalsy() + expect(tab().externalMutation).toBe(banner) +}) + it('marks a dirty runtime-owned tab deleted when its host says the file is gone', async () => { mocks.readRuntimeFileContent.mockRejectedValue(new Error('read failed')) mocks.runtimePathsExist.mockResolvedValue([{ exists: false }]) diff --git a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts index 73a8172b70c..922d1dd2777 100644 --- a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts +++ b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createStore, type StoreApi } from 'zustand/vanilla' -import { createEditorSlice } from '@/store/slices/editor' +import type { StoreApi } from 'zustand/vanilla' +import { createTestStore } from '@/store/slices/store-test-helpers' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' import type { AppState } from '@/store' import { attachRestoredTabConflictScan } from './editor-restored-tab-conflict-scan' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' const mocks = vi.hoisted(() => ({ readRuntimeFileContent: vi.fn(), @@ -20,14 +20,6 @@ vi.mock('@/lib/connection-context', () => ({ getConnectionIdForFile: mocks.getConnectionIdForFile })) -function createEditorStore(): StoreApi { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return createStore()((...args: any[]) => ({ - settings: {}, - ...createEditorSlice(...(args as Parameters)) - })) as unknown as StoreApi -} - function openRestoredDirtyTab( store: StoreApi, filePath: string, @@ -71,7 +63,7 @@ describe('attachRestoredTabConflictScan', () => { content: 'agent rewrote this offline', isBinary: false }) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/file.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -88,7 +80,7 @@ describe('attachRestoredTabConflictScan', () => { content: 'original baseline', isBinary: false }) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/file.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -102,7 +94,7 @@ describe('attachRestoredTabConflictScan', () => { }) it('does not read files for clean tabs or tabs without a baseline', async () => { - const store = createEditorStore() + const store = createTestStore() store.getState().openFile({ filePath: '/repo/clean.ts', relativePath: 'clean.ts', @@ -134,7 +126,7 @@ describe('attachRestoredTabConflictScan', () => { mocks.readRuntimeFileContent .mockRejectedValueOnce(new Error('connection not ready')) .mockResolvedValue({ content: 'agent rewrote this offline', isBinary: false }) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/file.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -153,7 +145,7 @@ describe('attachRestoredTabConflictScan', () => { content: 'original baseline', isBinary: false }) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/match.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -178,7 +170,7 @@ describe('attachRestoredTabConflictScan', () => { }) it('does not re-verify live dirty tabs that were never flagged at hydration', async () => { - const store = createEditorStore() + const store = createTestStore() store.getState().openFile({ filePath: '/repo/live.ts', relativePath: 'live.ts', @@ -207,7 +199,7 @@ describe('attachRestoredTabConflictScan', () => { // suspended for the whole session. mocks.readRuntimeFileContent.mockRejectedValue(new Error('ENOENT')) mocks.pathExists.mockResolvedValue(false) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/deleted.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -230,7 +222,7 @@ describe('attachRestoredTabConflictScan', () => { // transport recovers. mocks.readRuntimeFileContent.mockRejectedValue(new Error('connection not ready')) mocks.pathExists.mockRejectedValue(new Error('connection not ready')) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/unreachable.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -254,7 +246,7 @@ describe('attachRestoredTabConflictScan', () => { }) mocks.pathExists.mockResolvedValue(true) mocks.getConnectionIdForFile.mockReturnValue(null) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/Users/me/notes.txt', 'original baseline', { relativePath: 'notes.txt', worktreeId: FLOATING_TERMINAL_WORKTREE_ID @@ -282,7 +274,7 @@ describe('attachRestoredTabConflictScan', () => { isBinary: false }) mocks.getConnectionIdForFile.mockReturnValue(null) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/file.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -297,7 +289,7 @@ describe('attachRestoredTabConflictScan', () => { }) it('does not verify an external SSH file through a replacement target', async () => { - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/tmp/external.ts', 'original baseline') store.setState({ openFiles: store @@ -328,7 +320,7 @@ describe('attachRestoredTabConflictScan', () => { pendingReads.push(resolve) }) ) - const store = createEditorStore() + const store = createTestStore() for (let i = 0; i < 6; i++) { openRestoredDirtyTab(store, `/repo/file-${i}.ts`, 'original baseline') } @@ -361,7 +353,7 @@ describe('attachRestoredTabConflictScan', () => { resolveRead = resolve }) ) - const store = createEditorStore() + const store = createTestStore() openRestoredDirtyTab(store, '/repo/file.ts', 'original baseline') const detach = attachRestoredTabConflictScan(store) @@ -390,7 +382,7 @@ describe('attachRestoredTabConflictScan', () => { pending.push({ filePath, resolve }) }) ) - const store = createEditorStore() + const store = createTestStore() // Three reads fill the concurrency cap; the fourth waits in the queue. for (let i = 0; i < 4; i++) { openRestoredDirtyTab(store, `/repo/file-${i}.ts`, 'stale baseline') diff --git a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts index ea889325a80..8b5943f03b6 100644 --- a/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts +++ b/src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts @@ -10,7 +10,7 @@ import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target' import { canAutoSaveOpenFile } from './editor-autosave' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { markFileChangedOnDisk } from './editor-changed-on-disk-mark' import { runtimePathsExist } from '@/runtime/runtime-path-existence-batch' @@ -60,6 +60,7 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void { // Dedupes queued + in-flight verifications; the store's pending flag is the durable "needs verification" signal. const inFlightFileIds = new Set() const attemptsByFileId = new Map() + const supersededReadsByFileId = new Map() const retryTimers = new Set>() // Queue ids, not OpenFile snapshots: a snapshot can go stale (close/reopen or same-path save) while it waits for a slot. const verifyQueue: string[] = [] @@ -129,10 +130,18 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void { if (verifyRequestGeneration(fileId) === requestGeneration) { return false } - queueMicrotask(() => { - inFlightFileIds.delete(fileId) - scan() - }) + // Why back off, not cap: a superseded answer may predate the newer check, so it never resolves; churn only delays the fresh read. + const superseded = (supersededReadsByFileId.get(fileId) ?? 0) + 1 + supersededReadsByFileId.set(fileId, superseded) + const timer = setTimeout( + () => { + retryTimers.delete(timer) + inFlightFileIds.delete(fileId) + scan() + }, + Math.min((superseded - 1) * VERIFY_RETRY_MS, VERIFY_SLOW_RETRY_MS) + ) + retryTimers.add(timer) return true } @@ -163,6 +172,7 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void { return } attemptsByFileId.delete(file.id) + supersededReadsByFileId.delete(file.id) // Verification resolved: lift autosave suspension regardless of outcome; wasPending flags a save that already re-baselined. const wasPending = liveFile.pendingDiskBaselineVerification === true store.getState().clearPendingDiskBaselineVerification(file.id) @@ -233,6 +243,7 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void { // Why: still unverifiable after every retry; resume nothing silently — the banner makes the user choose. const giveUp = (file: OpenFile): void => { attemptsByFileId.delete(file.id) + supersededReadsByFileId.delete(file.id) const liveFile = store.getState().openFiles.find((f) => f.id === file.id) if (!liveFile?.pendingDiskBaselineVerification) { return diff --git a/src/renderer/src/components/editor/editor-save-disk-precondition.ts b/src/renderer/src/components/editor/editor-save-disk-precondition.ts index b6adeba877b..8112f02a690 100644 --- a/src/renderer/src/components/editor/editor-save-disk-precondition.ts +++ b/src/renderer/src/components/editor/editor-save-disk-precondition.ts @@ -85,17 +85,6 @@ export function editorModelDraftHolder(state: AppState, file: OpenFile): OpenFil ) } -/** Whether a store snapshot is whole enough to resolve a tab's model siblings. */ -export function isWholeEditorState(state: unknown): state is AppState { - return ( - typeof state === 'object' && - state !== null && - 'editorDrafts' in state && - 'worktreesByRepo' in state && - 'setLastKnownDiskSignature' in state - ) -} - /** * The one disk version of a code-view model: every tab sharing the model holds the same value, set * only here. Other views keep their own. Returns the sibling tabs whose value moved. diff --git a/src/renderer/src/components/editor/editor-save-queue.ts b/src/renderer/src/components/editor/editor-save-queue.ts index 7bb5cde7f82..de0aa062673 100644 --- a/src/renderer/src/components/editor/editor-save-queue.ts +++ b/src/renderer/src/components/editor/editor-save-queue.ts @@ -16,7 +16,7 @@ import { recordSelfWrite, SELF_WRITE_REMOTE_TTL_MS } from './editor-self-write-registry' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { trackExternalChangeConflictAction } from './editor-external-change-telemetry' import { editorTabFileAccess } from '@/lib/local-file-access' import { isEditorLargePasteInFlight } from './editor-large-paste-in-flight' diff --git a/src/renderer/src/components/editor/editor-stale-mount-guard.ts b/src/renderer/src/components/editor/editor-stale-mount-guard.ts index afd3241de59..b8b5f2a4e05 100644 --- a/src/renderer/src/components/editor/editor-stale-mount-guard.ts +++ b/src/renderer/src/components/editor/editor-stale-mount-guard.ts @@ -1,7 +1,7 @@ import { useAppStore } from '@/store' import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { notifyEditorFileCacheStale } from './editor-autosave' -import { isEditorModelDirty, isWholeEditorState } from './editor-save-disk-precondition' +import { isEditorModelDirty } from './editor-save-disk-precondition' /** * A clean tab's text (cache, or a leftover draft) that no longer matches its disk version, such as @@ -49,9 +49,6 @@ export function settleStaleCacheReload(fileId: string): void { */ export function mayPushTabTextIntoModel(fileId: string): boolean { const state = useAppStore.getState() - if (!isWholeEditorState(state)) { - return true - } const file = state.openFiles.find((openFile) => openFile.id === fileId) if (!file || file.isDirty) { // Why: a dirty tab's text is its own edits. diff --git a/src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx b/src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx index ece6409c8eb..25d563448a4 100644 --- a/src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx +++ b/src/renderer/src/components/editor/editor-untitled-dont-save-dialogs.test.tsx @@ -8,7 +8,7 @@ import { createUntitledNoteStore, type FakeEditorDisk } from './editor-autosave-controller-test-fixture' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { useTerminalSaveDialog } from '../terminal/useTerminalSaveDialog' import { useTerminalEditorCloseDialogActions, diff --git a/src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts b/src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts index 38709333368..7afa7d76f00 100644 --- a/src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts +++ b/src/renderer/src/components/editor/editor-untitled-save-lifecycle.test.ts @@ -12,7 +12,7 @@ import { } from './editor-autosave-controller-test-fixture' import { discardEditorFileChangesAndClose } from './discard-editor-file-changes' import { __clearSelfWriteRegistryForTests } from './editor-self-write-registry' -import { getDiskBaselineSignature } from './diff-content-signature' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' const storeHolder = vi.hoisted((): { store: StoreApi | null } => ({ store: null })) diff --git a/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx b/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx index 667e3be7530..f430bc32562 100644 --- a/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx +++ b/src/renderer/src/components/editor/use-monaco-editor-mount.cursor-owner.test.tsx @@ -4,13 +4,16 @@ import { Suspense } from 'react' import { afterEach, expect, it, vi } from 'vitest' import type { editor } from 'monaco-editor' import type { OnMount } from '@monaco-editor/react' +import type { StoreApi } from 'zustand' +import type { AppState } from '@/store' import { createEditorTabsStore } from '@/store/slices/editor-slice-test-harness' import { captureEditorFileOperationProvenance } from '@/lib/editor-file-operation-owner' import type { MonacoEditorMountParams } from './monaco-editor-mount-params' import { useMonacoEditorMount } from './use-monaco-editor-mount' +const storeRef = vi.hoisted((): { current?: StoreApi } => ({})) vi.mock('@/store', () => ({ - useAppStore: { getState: () => ({ pendingEditorReveal: null, pendingEditorFocusRequest: null }) } + useAppStore: { getState: () => storeRef.current?.getState() } })) vi.mock('./monaco-markdown-doc-link-decorations', () => ({ createMarkdownDocLinkDecorationController: vi.fn() @@ -27,6 +30,7 @@ afterEach(cleanup) it('keeps a retained cursor listener on the current file ID after same-path owner migration', () => { const store = createEditorTabsStore() + storeRef.current = store const filePath = '/repo/file.ts' const oldId = 'restored-file' store.setState({ diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx index d7c84e81ee5..1615e34056b 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelContentState.test.tsx @@ -68,7 +68,8 @@ vi.mock('@/store', () => ({ })) import { useEditorPanelContentState } from './useEditorPanelContentState' -import { getDiskBaselineSignature } from './diff-content-signature' +import { createTestStore } from '@/store/slices/store-test-helpers' +import { getDiskBaselineSignature } from '../../../../shared/disk-baseline-signature' import { ORCA_EDITOR_EXTERNAL_FILE_CHANGE_EVENT } from './editor-autosave' type Deferred = { @@ -166,6 +167,7 @@ describe('useEditorPanelContentState', () => { mocks.isWorktreeConnectionResolved.mockReturnValue(true) mocks.getState.mockReset() mocks.getState.mockReturnValue({ + ...createTestStore().getState(), settings: null, openFiles: [], setLastKnownDiskSignature: vi.fn() @@ -315,6 +317,7 @@ describe('useEditorPanelContentState', () => { }) mocks.getConnectionIdForFile.mockReturnValue(undefined) mocks.getState.mockReturnValue({ + ...createTestStore().getState(), settings: { activeRuntimeEnvironmentId: 'runtime-1' }, openFiles: [], setLastKnownDiskSignature: vi.fn() @@ -883,6 +886,7 @@ describe('useEditorPanelContentState', () => { const activeFile = createOpenFile() const setLastKnownDiskSignature = vi.fn() mocks.getState.mockReturnValue({ + ...createTestStore().getState(), settings: null, openFiles: [activeFile], setLastKnownDiskSignature @@ -911,6 +915,7 @@ describe('useEditorPanelContentState', () => { const activeFile = createOpenFile({ isDirty: true }) const setLastKnownDiskSignature = vi.fn() mocks.getState.mockReturnValue({ + ...createTestStore().getState(), settings: null, openFiles: [activeFile], setLastKnownDiskSignature diff --git a/src/renderer/src/components/editor/useEditorPanelExternalContentEvents.ts b/src/renderer/src/components/editor/useEditorPanelExternalContentEvents.ts index a7205c10ca0..6a4f7c75135 100644 --- a/src/renderer/src/components/editor/useEditorPanelExternalContentEvents.ts +++ b/src/renderer/src/components/editor/useEditorPanelExternalContentEvents.ts @@ -15,11 +15,7 @@ import { isReloadableSingleFileDiffTab } from './editor-panel-diff-reload' import { createEditorWatcherReloadCoalescer } from './editor-watcher-reload-coalescer' import { markFileChangedOnDisk } from './editor-changed-on-disk-mark' import { hasRecentSelfWrite } from './editor-self-write-registry' -import { - editorModelSiblingTabs, - isEditorModelDirty, - isWholeEditorState -} from './editor-save-disk-precondition' +import { editorModelSiblingTabs, isEditorModelDirty } from './editor-save-disk-precondition' type EditorViewModeByFile = ReturnType['editorViewMode'] @@ -93,7 +89,7 @@ export function useEditorPanelExternalContentEvents({ return 'stale' } // Why: a clean tab of a model another tab is editing shows that buffer; never reload it alone. - if (isWholeEditorState(state) && isEditorModelDirty(state, file)) { + if (isEditorModelDirty(state, file)) { return null } return isVisibleRef.current && file.id === activeContentFileIdRef.current ? file : 'stale' @@ -142,8 +138,7 @@ export function useEditorPanelExternalContentEvents({ // clean tab of a model another tab is editing shows that buffer too. if ( file.isDirty || - (isWholeEditorState(storeState) && - storeState.openFiles.some((tab) => tab.id === file.id) && + (storeState.openFiles.some((tab) => tab.id === file.id) && isEditorModelDirty(storeState, file)) ) { continue diff --git a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx index 62c129a7203..7f297c093a1 100644 --- a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx @@ -42,6 +42,7 @@ vi.mock('@/store', () => ({ useAppStore: { getState: mocks.getState } })) import { ORCA_EDITOR_EXTERNAL_FILE_CHANGE_EVENT } from './editor-autosave' import { useEditorPanelContentState } from './useEditorPanelContentState' +import { createTestStore } from '@/store/slices/store-test-helpers' type Deferred = { promise: Promise @@ -161,6 +162,7 @@ describe('useEditorPanelContentState visibility', () => { mocks.readRuntimeFileContent.mockReset() mocks.getState.mockReset() mocks.getState.mockReturnValue({ + ...createTestStore().getState(), settings: null, openFiles: [], setLastKnownDiskSignature: vi.fn() diff --git a/src/renderer/src/hooks/editor-external-watch-disk-verification.ts b/src/renderer/src/hooks/editor-external-watch-disk-verification.ts index 69fb7933628..ca402b5b88e 100644 --- a/src/renderer/src/hooks/editor-external-watch-disk-verification.ts +++ b/src/renderer/src/hooks/editor-external-watch-disk-verification.ts @@ -6,7 +6,7 @@ import { notifyEditorExternalFileChange } from '@/components/editor/editor-autosave' import { markFileChangedOnDisk } from '@/components/editor/editor-changed-on-disk-mark' -import { getDiskBaselineSignature } from '@/components/editor/diff-content-signature' +import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signature' import { clearSelfWrite, getRecentSelfWrite, diff --git a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts b/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts index 48832cfad5b..4de08af22b8 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch-self-move.test.ts @@ -20,6 +20,8 @@ import { } from './editor-external-watch-event-reconciliation' import { verifyLatchedMoveDestinations } from './useEditorExternalWatch' import { useAppStore } from '@/store' +import type { OpenFile } from '@/store/slices/editor' +import { createTestStore } from '@/store/slices/store-test-helpers' import { getOpenFilesForExternalFileChange } from '@/components/editor/editor-autosave' import { __clearSelfWriteRegistryForTests } from '@/components/editor/editor-self-write-registry' import { @@ -27,7 +29,7 @@ import { beginEditorPathMove, settleEditorPathMove } from '@/components/editor/editor-path-move-inflight' -import { getDiskBaselineSignature } from '@/components/editor/diff-content-signature' +import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signature' const findTarget = (worktreePath: string, runtimeEnvironmentId: string | null = null) => worktreePath === '/repo' @@ -46,6 +48,7 @@ describe('self-move source suppression (in-flight)', () => { worktreePath: '/repo', filePath: '/repo/notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'edit' as const, isDirty: false } @@ -54,9 +57,10 @@ describe('self-move source suppression (in-flight)', () => { vi.clearAllMocks() vi.useFakeTimers() vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [fileNotes], setExternalMutation - } as never) + }) }) afterEach(() => { @@ -97,13 +101,15 @@ describe('self-move source suppression (in-flight)', () => { worktreePath: '/repo', filePath: '/repo/src/late.md', relativePath: 'src/late.md', + language: 'markdown', mode: 'edit' as const, isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [lateTab], setExternalMutation - } as never) + }) beginEditorPathMove({ operationId: 'op-dir', worktreeId: 'wt-1', @@ -148,13 +154,13 @@ describe('self-move echo verification (content identity)', () => { const baselineSignature = getDiskBaselineSignature(BASELINE_CONTENT) const TARGET_PATH = '/repo/subdir/notes.md' - function movedDirtyTab(overrides: Record = {}): Record { + function movedDirtyTab(overrides: Partial = {}): OpenFile { return { id: 'file-notes', worktreeId: 'wt-1', - worktreePath: '/repo', filePath: TARGET_PATH, relativePath: 'subdir/notes.md', + language: 'markdown', mode: 'edit' as const, isDirty: true, lastKnownDiskSignature: baselineSignature, @@ -164,14 +170,15 @@ describe('self-move echo verification (content identity)', () => { } } - function mockState(file: Record): void { + function mockState(file: OpenFile): void { vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [file], setExternalMutation, setPendingLiveDiskVerification, clearSelfMoveEcho - } as never) - vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file] as never) + }) + vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file]) } function payloadFor(): FsChangedPayload { @@ -293,6 +300,7 @@ describe('verifyLatchedMoveDestinations (proactive, cross-worktree path)', () => filePath: FLOATING_FILE_PATH, relativePath: '../notes/readme.md', runtimeEnvironmentId: null, + language: 'markdown', mode: 'edit' as const, isDirty: true, lastKnownDiskSignature: baseline, @@ -306,11 +314,12 @@ describe('verifyLatchedMoveDestinations (proactive, cross-worktree path)', () => readFile = vi.fn().mockResolvedValue({ isBinary: false, content: CONTENT }) vi.stubGlobal('window', { api: { fs: { readFile } } }) vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [floatingTab], setPendingLiveDiskVerification, clearSelfMoveEcho, setExternalMutation - } as never) + }) }) afterEach(() => { diff --git a/src/renderer/src/hooks/useEditorExternalWatch-switch-back.test.tsx b/src/renderer/src/hooks/useEditorExternalWatch-switch-back.test.tsx index abbe04c3fa4..95045b22b1d 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch-switch-back.test.tsx +++ b/src/renderer/src/hooks/useEditorExternalWatch-switch-back.test.tsx @@ -48,7 +48,7 @@ vi.mock('@/lib/connection-context', () => ({ getConnectionIdForFile: () => undef import { createTestStore } from '@/store/slices/store-test-helpers' import { useEditorExternalWatch } from './useEditorExternalWatch' import { attachRestoredTabConflictScan } from '@/components/editor/editor-restored-tab-conflict-scan' -import { getDiskBaselineSignature } from '@/components/editor/diff-content-signature' +import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signature' import { isAutosaveSuspendedForFile, ORCA_EDITOR_EXTERNAL_FILE_CHANGE_EVENT diff --git a/src/renderer/src/hooks/useEditorExternalWatch.test.ts b/src/renderer/src/hooks/useEditorExternalWatch.test.ts index f249f322eef..8ff9b882796 100644 --- a/src/renderer/src/hooks/useEditorExternalWatch.test.ts +++ b/src/renderer/src/hooks/useEditorExternalWatch.test.ts @@ -25,6 +25,7 @@ import { } from './editor-external-watch-event-reconciliation' import { getEditorExternalWatchTargetKey as getWatchedTargetKey } from './editor-external-watch-targets' import { useAppStore } from '@/store' +import { createTestStore } from '@/store/slices/store-test-helpers' import { getOpenFilesForExternalFileChange, notifyEditorExternalFileChange @@ -101,6 +102,7 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo/notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'edit' as const, isDirty: false } @@ -109,9 +111,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { vi.clearAllMocks() vi.useFakeTimers() vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [fileNotes], setExternalMutation - } as never) + }) }) afterEach(() => { @@ -178,14 +181,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: 'C:\\Repo', filePath: 'C:\\Repo\\notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'edit' as const, isDirty: false, runtimeEnvironmentId: 'env-1' } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [file], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(() => ({ worktreeId: 'wt-win', @@ -216,14 +221,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '//Server/Share/Repo', filePath: '//Server/Share/Repo/notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'edit' as const, isDirty: false, runtimeEnvironmentId: 'env-1' } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [file], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([file] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(() => ({ worktreeId: 'wt-unc', @@ -249,9 +256,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { it('does not drop external edits that arrive inside the self-write TTL', async () => { vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [fileNotes], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([fileNotes] as never) const readFile = vi.fn().mockResolvedValue({ content: 'agent edit', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -272,9 +280,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { it('still suppresses the watcher echo from Orca self-writes', async () => { vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [fileNotes], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([fileNotes] as never) const readFile = vi.fn().mockResolvedValue({ content: 'orca save', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -294,9 +303,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { runtimeEnvironmentId: 'env-1' } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [runtimeFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([runtimeFile] as never) const readFile = vi.fn().mockResolvedValue({ content: 'local save', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -323,9 +333,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { runtimeEnvironmentId: 'env-1' } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [localFile, runtimeFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockImplementation((_files, notification) => notification.runtimeEnvironmentId === 'env-1' ? ([runtimeFile] as never) @@ -359,14 +370,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo', relativePath: 'Changes', + language: 'markdown', mode: 'diff' as const, diffSource: 'combined-uncommitted' as const, isDirty: false } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [combinedDiffTab], setExternalMutation - } as never) + }) // Why: combined tabs match no single path, so the per-path lookup stays empty. vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([]) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -394,14 +407,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo', relativePath: 'Changes', + language: 'markdown', mode: 'diff' as const, diffSource: 'combined-uncommitted' as const, isDirty: false } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile, combinedDiffTab], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -423,9 +438,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -448,14 +464,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo/notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'diff' as const, diffSource: 'unstaged' as const, isDirty: false } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile, cleanDiffTab], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile, cleanDiffTab] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -478,9 +496,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) const readFile = vi.fn().mockResolvedValue({ content: 'orca save', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -500,9 +519,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) const readFile = vi.fn().mockResolvedValue({ content: 'agent content', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -522,9 +542,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) const readFile = vi.fn().mockResolvedValue({ content: 'agent content', isBinary: false }) vi.stubGlobal('window', { api: { fs: { readFile } } }) @@ -550,9 +571,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyFile] as never) let resolveEchoRead: (value: { content: string; isBinary: boolean }) => void = () => {} const readFile = vi @@ -587,14 +609,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo/notes.md', relativePath: 'notes.md', + language: 'markdown', mode: 'diff' as const, diffSource: 'unstaged' as const, isDirty: true } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyDiffTab], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyDiffTab] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -613,9 +637,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { externalMutation: 'changed' as const } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [dirtyChangedFile], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([dirtyChangedFile] as never) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -633,14 +658,16 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { worktreePath: '/repo', filePath: '/repo', relativePath: 'Branch Changes', + language: 'markdown', mode: 'diff' as const, diffSource: 'combined-branch' as const, isDirty: false } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [branchDiffTab], setExternalMutation - } as never) + }) vi.mocked(getOpenFilesForExternalFileChange).mockReturnValue([]) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) @@ -659,9 +686,10 @@ describe('createExternalWatchEventHandler tombstone coalescing', () => { runtimeEnvironmentId: 'env-1' } vi.mocked(useAppStore.getState).mockReturnValue({ + ...createTestStore().getState(), openFiles: [localFile, runtimeFile], setExternalMutation - } as never) + }) const { handleFsChanged, dispose } = createExternalWatchEventHandler(findTarget) handleFsChanged(payload([{ kind: 'delete', absolutePath: '/repo/notes.md' }]), 'env-1') diff --git a/src/renderer/src/lib/execute-open-editor-path-move.test.ts b/src/renderer/src/lib/execute-open-editor-path-move.test.ts index cf3c1c433db..b85e1e44868 100644 --- a/src/renderer/src/lib/execute-open-editor-path-move.test.ts +++ b/src/renderer/src/lib/execute-open-editor-path-move.test.ts @@ -15,7 +15,7 @@ vi.mock('@/components/editor/editor-autosave', async (importOriginal) => { import { useAppStore } from '@/store' import { executeOpenEditorPathMove } from './execute-open-editor-path-move' import { __activeEditorPathMoveCountForTests } from '@/components/editor/editor-path-move-inflight' -import { getDiskBaselineSignature } from '@/components/editor/diff-content-signature' +import { getDiskBaselineSignature } from '../../../shared/disk-baseline-signature' import { buildEditorExternalWatchEventHandler as createExternalWatchEventHandler } from '@/hooks/editor-external-watch-event-reconciliation' const CONTEXT = { diff --git a/src/renderer/src/store/slices/editor/tabs/untitled-file-cleanup.ts b/src/renderer/src/store/slices/editor/tabs/untitled-file-cleanup.ts index 4ac81fab497..abf08ba2cb7 100644 --- a/src/renderer/src/store/slices/editor/tabs/untitled-file-cleanup.ts +++ b/src/renderer/src/store/slices/editor/tabs/untitled-file-cleanup.ts @@ -3,7 +3,7 @@ import type { OpenFile } from '../types/open-file' import { mayShareEditorBackingFile } from '../file-ids/editor-file-ids' import { findWorktreeById } from '../../worktree-helpers' import { getEditorFileOperationContext } from '@/lib/editor-file-operation-owner' -import { getDiskBaselineSignature } from '@/components/editor/diff-content-signature' +import { getDiskBaselineSignature } from '../../../../../../shared/disk-baseline-signature' import { deleteRuntimePath, deleteRuntimeRelativePath