diff --git a/src/main/ai-vault/cached-session-list-wsl-probe.test.ts b/src/main/ai-vault/cached-session-list-wsl-probe.test.ts index 4de5330a816..e03ffd615d4 100644 --- a/src/main/ai-vault/cached-session-list-wsl-probe.test.ts +++ b/src/main/ai-vault/cached-session-list-wsl-probe.test.ts @@ -16,10 +16,11 @@ vi.mock('./session-scanner-worker-spawn', () => ({ resetAiVaultScannerWorkerForTests: vi.fn() })) -import { _resetWslCachesForTests, _setWslCachesForTests } from '../wsl' +import { _resetWslCachesForTests, _setWslCachesForTests, listWslDistrosAsync } from '../wsl' import { filterPathsToRunningWslDistrosAsync } from '../wsl-running-path-filter' import { configureAiVaultSessionSources, + getAiVaultWslHomeDirs, listAiVaultSessions, resetAiVaultSessionListCacheForTests } from './cached-session-list' @@ -88,4 +89,22 @@ describe('AI Vault listing wsl.exe probes', () => { `${WSL_HOME}\\.codex` ]) }) + + // Why: a rejected `--list --quiet` yields [] without caching. Treating that as "no distro + // installed" would narrow the allowed roots delete/subagent validation trusts. + it('still discovers WSL homes after the installed-distro probe was rejected', async () => { + execFileMock.mockImplementation((_command, args, _options, callback) => { + if (args.includes('--running')) { + callback(null, 'Ubuntu\n') + } else if (args.includes('--list')) { + callback(new Error('wsl.exe transient failure'), '') + } else { + callback(null, '/home/ada\n') + } + }) + await expect(listWslDistrosAsync()).resolves.toEqual([]) + + await expect(getAiVaultWslHomeDirs()).resolves.toEqual([WSL_HOME]) + expect(wslSpawns()).toContainEqual(['--list', '--running', '--quiet']) + }) }) diff --git a/src/main/ai-vault/cached-session-list.test.ts b/src/main/ai-vault/cached-session-list.test.ts index 9c1fc16ac01..bbdd4be2877 100644 --- a/src/main/ai-vault/cached-session-list.test.ts +++ b/src/main/ai-vault/cached-session-list.test.ts @@ -3,13 +3,15 @@ import type { AiVaultListResult } from '../../shared/ai-vault-types' const { filterPathsToRunningWslDistrosAsync, + getCachedWslDistros, + hasCachedWslDistros, listRunningWslHomeDirsAsync, - listWslDistrosAsync, scanAiVaultSessionsInWorker } = vi.hoisted(() => ({ filterPathsToRunningWslDistrosAsync: vi.fn(async (paths: readonly string[]) => [...paths]), + getCachedWslDistros: vi.fn((): string[] | null => null), + hasCachedWslDistros: vi.fn(() => false), listRunningWslHomeDirsAsync: vi.fn().mockResolvedValue([]), - listWslDistrosAsync: vi.fn().mockResolvedValue(['Ubuntu']), scanAiVaultSessionsInWorker: vi.fn() })) @@ -18,8 +20,9 @@ vi.mock('./session-scanner-worker-spawn', () => ({ resetAiVaultScannerWorkerForTests: vi.fn() })) vi.mock('../wsl', () => ({ - listRunningWslHomeDirsAsync, - listWslDistrosAsync + getCachedWslDistros, + hasCachedWslDistros, + listRunningWslHomeDirsAsync })) vi.mock('../wsl-running-path-filter', () => ({ filterPathsToRunningWslDistrosAsync })) @@ -54,8 +57,9 @@ describe('invalidateAiVaultSessionListCache generation guard', () => { vi.spyOn(process, 'platform', 'get').mockImplementation(() => platform) resetAiVaultSessionListCacheForTests() filterPathsToRunningWslDistrosAsync.mockClear() + getCachedWslDistros.mockReset().mockReturnValue(null) + hasCachedWslDistros.mockReset().mockReturnValue(false) listRunningWslHomeDirsAsync.mockReset().mockResolvedValue([]) - listWslDistrosAsync.mockReset().mockResolvedValue(['Ubuntu']) scanAiVaultSessionsInWorker.mockReset() }) afterEach(() => { @@ -103,13 +107,22 @@ describe('invalidateAiVaultSessionListCache generation guard', () => { expect(listRunningWslHomeDirsAsync).toHaveBeenCalledTimes(1) }) - it('skips running-distro discovery when no WSL distro is installed', async () => { - listWslDistrosAsync.mockResolvedValue([]) + it('skips running-distro discovery once a probe has reported no installed WSL distro', async () => { + hasCachedWslDistros.mockReturnValue(true) + getCachedWslDistros.mockReturnValue([]) await expect(getAiVaultWslHomeDirs()).resolves.toEqual([]) expect(listRunningWslHomeDirsAsync).not.toHaveBeenCalled() }) + it('still discovers running distros before any distro probe has succeeded', async () => { + hasCachedWslDistros.mockReturnValue(false) + listRunningWslHomeDirsAsync.mockResolvedValue(['\\\\wsl.localhost\\Ubuntu\\home\\ada']) + + await expect(getAiVaultWslHomeDirs()).resolves.toEqual(['\\\\wsl.localhost\\Ubuntu\\home\\ada']) + expect(listRunningWslHomeDirsAsync).toHaveBeenCalledTimes(1) + }) + it('skips WSL home discovery off Windows', async () => { platform = 'linux' diff --git a/src/main/ai-vault/cached-session-list.ts b/src/main/ai-vault/cached-session-list.ts index 013e3a49351..c9feb5b9daf 100644 --- a/src/main/ai-vault/cached-session-list.ts +++ b/src/main/ai-vault/cached-session-list.ts @@ -4,7 +4,7 @@ import { resetAiVaultScannerBackgroundForTests, scanAiVaultSessionsInBackground } from './session-scanner-background' -import { listRunningWslHomeDirsAsync, listWslDistrosAsync } from '../wsl' +import { getCachedWslDistros, hasCachedWslDistros, listRunningWslHomeDirsAsync } from '../wsl' import { filterPathsToRunningWslDistrosAsync } from '../wsl-running-path-filter' import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' @@ -138,7 +138,10 @@ export async function getAiVaultWslHomeDirs(): Promise { return [] } // No installed distro can be running: spares WSL-less hosts the running-distro probe. - if ((await listWslDistrosAsync()).length === 0) { + // Cache read only: a rejected wsl.exe probe yields [] without caching, so it must not + // narrow the WSL roots delete/subagent validation trusts; and probing here would let this + // listing be the first to cache [] and flip a configured distro to "missing". + if (hasCachedWslDistros() && getCachedWslDistros()?.length === 0) { return [] } return listRunningWslHomeDirsAsync() diff --git a/src/main/ipc/ai-vault-scan-coalescing.test.ts b/src/main/ipc/ai-vault-scan-coalescing.test.ts index a152a79036b..8a8ad0a3665 100644 --- a/src/main/ipc/ai-vault-scan-coalescing.test.ts +++ b/src/main/ipc/ai-vault-scan-coalescing.test.ts @@ -27,7 +27,7 @@ vi.mock('../ai-vault/remote-session-scanner', () => ({ })) vi.mock('../wsl', () => ({ listRunningWslHomeDirsAsync: vi.fn().mockResolvedValue([]), - listWslDistrosAsync: vi.fn().mockResolvedValue(['Ubuntu']) + hasCachedWslDistros: vi.fn(() => false) })) vi.mock('../wsl-running-path-filter', () => ({ filterPathsToRunningWslDistrosAsync: vi.fn(async (paths: readonly string[]) => [...paths]) diff --git a/src/main/ipc/ai-vault.test.ts b/src/main/ipc/ai-vault.test.ts index 954cbc95a6a..81f9e72f859 100644 --- a/src/main/ipc/ai-vault.test.ts +++ b/src/main/ipc/ai-vault.test.ts @@ -76,7 +76,7 @@ vi.mock('../ai-vault/session-scanner-parse-cache', async (importOriginal) => { vi.mock('../wsl', () => ({ listRunningWslDistrosAsync: vi.fn().mockResolvedValue([]), listRunningWslHomeDirsAsync: vi.fn().mockResolvedValue([]), - listWslDistrosAsync: vi.fn().mockResolvedValue(['Ubuntu']) + hasCachedWslDistros: vi.fn(() => false) })) vi.mock('../providers/ssh-filesystem-dispatch', () => ({