From 05af0e92ef74c975fd61afe1ecaeff2b395412a7 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 24 May 2026 13:03:41 -0700 Subject: [PATCH] Fix WSL preflight context snapshot stability (#2749) --- .../src/lib/local-preflight-context.test.ts | 89 +++++++++++++++++++ .../src/lib/local-preflight-context.ts | 17 +++- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/lib/local-preflight-context.test.ts diff --git a/src/renderer/src/lib/local-preflight-context.test.ts b/src/renderer/src/lib/local-preflight-context.test.ts new file mode 100644 index 00000000000..1c036f8d530 --- /dev/null +++ b/src/renderer/src/lib/local-preflight-context.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from 'vitest' +import type { AppState } from '@/store/types' +import { + getLocalPreflightContext, + getWslDistroFromPath, + localPreflightContextKey +} from './local-preflight-context' + +function makeState(args: { repoPath?: string | null; worktreePath?: string | null }): AppState { + const repoId = 'repo-1' + const worktreeId = `${repoId}::worktree-1` + return { + activeRepoId: repoId, + activeWorktreeId: args.worktreePath === undefined ? null : worktreeId, + repos: + args.repoPath === undefined + ? [] + : [ + { + id: repoId, + path: args.repoPath + } + ], + worktreesByRepo: + args.worktreePath === undefined + ? {} + : { + [repoId]: [ + { + id: worktreeId, + repoId, + path: args.worktreePath + } + ] + } + } as AppState +} + +describe('local preflight context', () => { + it('extracts WSL distro names from supported UNC forms', () => { + expect(getWslDistroFromPath(String.raw`\\wsl.localhost\Ubuntu\home\alice\repo`)).toBe('Ubuntu') + expect(getWslDistroFromPath(String.raw`\\wsl$\Debian\home\alice\repo`)).toBe('Debian') + expect(getWslDistroFromPath('/Users/alice/repo')).toBeNull() + }) + + it('returns a stable snapshot for repeated WSL selector reads', () => { + const state = makeState({ + worktreePath: String.raw`\\wsl.localhost\Ubuntu\home\alice\repo` + }) + + const first = getLocalPreflightContext(state) + const second = getLocalPreflightContext(state) + + expect(first).toBe(second) + expect(first).toEqual({ wslDistro: 'Ubuntu' }) + expect(localPreflightContextKey(first)).toBe('wsl:Ubuntu') + }) + + it('reuses the same WSL snapshot across equivalent active repo and worktree paths', () => { + const fromRepo = getLocalPreflightContext( + makeState({ + repoPath: String.raw`\\wsl.localhost\Ubuntu\home\alice\repo` + }) + ) + const fromWorktree = getLocalPreflightContext( + makeState({ + repoPath: '/Users/alice/repo', + worktreePath: String.raw`\\wsl.localhost\Ubuntu\home\alice\repo` + }) + ) + const fromOtherDistro = getLocalPreflightContext( + makeState({ + worktreePath: String.raw`\\wsl.localhost\Debian\home\alice\repo` + }) + ) + + expect(fromRepo).toBe(fromWorktree) + expect(fromOtherDistro).not.toBe(fromRepo) + expect(fromOtherDistro).toEqual({ wslDistro: 'Debian' }) + }) + + it('uses the stable host context for non-WSL paths', () => { + const state = makeState({ repoPath: '/Users/alice/repo' }) + + expect(getLocalPreflightContext(state)).toBeUndefined() + expect(getLocalPreflightContext(state)).toBeUndefined() + expect(localPreflightContextKey(getLocalPreflightContext(state))).toBe('host') + }) +}) diff --git a/src/renderer/src/lib/local-preflight-context.ts b/src/renderer/src/lib/local-preflight-context.ts index 1c0cf887e23..7f17191e680 100644 --- a/src/renderer/src/lib/local-preflight-context.ts +++ b/src/renderer/src/lib/local-preflight-context.ts @@ -3,10 +3,25 @@ import { parseWslUncPath } from '../../../shared/wsl-paths' export type LocalPreflightContext = { wslDistro?: string | null } | undefined +const wslPreflightContextsByDistro = new Map>() + export function getWslDistroFromPath(path?: string | null): string | null { return path ? (parseWslUncPath(path)?.distro ?? null) : null } +function getWslPreflightContext(wslDistro: string): NonNullable { + const cached = wslPreflightContextsByDistro.get(wslDistro) + if (cached) { + return cached + } + + // Why: React/Zustand selectors must return a cached snapshot. A fresh object + // here triggers a useSyncExternalStore loop when Settings observes WSL repos. + const context = Object.freeze({ wslDistro }) + wslPreflightContextsByDistro.set(wslDistro, context) + return context +} + export function getLocalPreflightContext(state: AppState): LocalPreflightContext { const activeWorktree = state.activeWorktreeId ? Object.values(state.worktreesByRepo ?? {}) @@ -16,7 +31,7 @@ export function getLocalPreflightContext(state: AppState): LocalPreflightContext const activePath = activeWorktree?.path ?? (state.repos ?? []).find((repo) => repo.id === state.activeRepoId)?.path const wslDistro = getWslDistroFromPath(activePath) - return wslDistro ? { wslDistro } : undefined + return wslDistro ? getWslPreflightContext(wslDistro) : undefined } export function localPreflightContextKey(context: LocalPreflightContext): string {