fix(wsl-mover): retry a failed listing, never a record another window was refused

Review of #32343: not-main-window can't clear by waiting, while a listing failure is transient.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-23 04:05:24 -07:00
co-authored by Claude
parent fb90078675
commit 332bbb1419
2 changed files with 33 additions and 9 deletions
@@ -106,12 +106,34 @@ describe('WSL move renderer store rewrites', () => {
expect(client.record).not.toHaveBeenCalled()
})
it('never throws when the move list is unavailable', async () => {
client.list.mockRejectedValue(new Error('no bridge'))
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect((await loadRunner()).runWslMoveRendererStoreRewrites()).resolves.toBeUndefined()
expect(warn).toHaveBeenCalled()
warn.mockRestore()
it('never throws when the move list is unavailable, and asks again later', async () => {
vi.useFakeTimers()
try {
client.list.mockRejectedValueOnce(new Error('main not ready')).mockResolvedValue([])
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect((await loadRunner()).runWslMoveRendererStoreRewrites()).resolves.toBeUndefined()
expect(warn).toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(2_000)
expect(client.list).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(120_000)
expect(client.list).toHaveBeenCalledTimes(2)
warn.mockRestore()
} finally {
vi.useRealTimers()
}
})
it('does not retry a record another window was refused', async () => {
vi.useFakeTimers()
try {
client.list.mockResolvedValue([move({ storeIds: ['quick-open-history'] })])
client.record.mockResolvedValue({ recorded: false, reason: 'not-main-window' })
await (await loadRunner()).runWslMoveRendererStoreRewrites()
await vi.advanceTimersByTimeAsync(120_000)
expect(client.record).toHaveBeenCalledTimes(1)
} finally {
vi.useRealTimers()
}
})
it('adds a host copy beside each moved entry, then records every store', async () => {
@@ -82,8 +82,9 @@ function writeAppliedMarker(marker: string): void {
}
}
// Main's refusals that a later call can clear; the rest mean the store is no longer pending.
const RETRYABLE_RECORD_REASONS: ReadonlySet<string> = new Set(['gated-off', 'failed', 'not-main-window'])
// Main's refusals a later call can clear. Another window (`not-main-window`) never becomes the main
// one, and the rest mean the store is no longer pending.
const RETRYABLE_RECORD_REASONS: ReadonlySet<string> = new Set(['gated-off', 'failed'])
/** True when a store's ledger entry still has to land: the move stays unfinished until it does. */
async function rewriteMove(move: WslMovePendingRendererRewrite): Promise<boolean> {
@@ -144,8 +145,9 @@ async function rewritePendingMoves(): Promise<boolean> {
unrecorded = (await rewriteMove(move)) || unrecorded
}
} catch (error) {
// No bridge to ask (a web client): nothing here could ever record, so no retry.
// Transient (main not ready yet); the backoff bounds a bridge that never answers.
console.warn('[wsl-move] Could not list renderer rewrites:', error)
unrecorded = true
}
return unrecorded
}