fix(ai-vault): gate WSL home discovery on the cached distro list, not a probe

`listWslDistrosAsync()` resolves `[]` when the `wsl.exe` probe is rejected, so a
transient failure made `getAiVaultWslHomeDirs()` conclude "no WSL distros" and
skip discovery. That narrowed the allowed-roots set `ai-vault-delete` and
`ai-vault-subagent-list` validate against, wrongly rejecting WSL-hosted paths.

Gate on `hasCachedWslDistros()` / `getCachedWslDistros()` instead: a pure cache
read that only skips discovery once a successful probe has reported zero user
distros. It also never probes, so the AI Vault listing cannot be the first to
cache `[]` and flip a configured distro to "missing" in runtime resolution.
This commit is contained in:
Neil
2026-09-12 20:44:29 -07:00
parent 100a329465
commit ea3ce031b8
5 changed files with 47 additions and 12 deletions
@@ -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'])
})
})
+20 -7
View File
@@ -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'
+5 -2
View File
@@ -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<string[]> {
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()
@@ -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])
+1 -1
View File
@@ -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', () => ({