fix(skills): verify WSL installs with guest paths

This commit is contained in:
Jinwoo-H
2026-08-11 11:27:38 -04:00
parent a9f10b5dec
commit cdafe43542
2 changed files with 146 additions and 6 deletions
@@ -0,0 +1,115 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SkillInstallResult } from '../../shared/skill-install-contract'
import type { SkillDiscoveryResult } from '../../shared/skills'
const { discoverSkillsInWslMock } = vi.hoisted(() => ({
discoverSkillsInWslMock: vi.fn()
}))
vi.mock('./skill-discovery-wsl', () => ({
discoverSkillsInWsl: discoverSkillsInWslMock
}))
import { verifySkillInstallDiscovery } from './skill-install-discovery-verification'
function installResult(paths: string[]): SkillInstallResult {
return {
operationId: 'operation-wsl',
status: 'installed',
name: 'review',
packageDigest: 'a'.repeat(64),
canonicalPath: paths[0],
placements: paths.map((path, index) => ({
provider: index === 0 ? 'agent-skills' : 'claude',
path,
topology: index === 0 ? 'canonical-copy' : 'provider-alias',
status: 'installed'
}))
}
}
function discovery(rootPaths: string[]): SkillDiscoveryResult {
return {
skills: [
{
id: 'review',
name: 'review',
description: 'Review',
providers: ['agent-skills', 'claude'],
sourceKind: 'home',
sourceLabel: 'Agent skills home',
rootPath: rootPaths[0],
rootPaths,
directoryPath: `${rootPaths[0]}/review`,
skillFilePath: `${rootPaths[0]}/review/SKILL.md`,
installed: true,
fileCount: 1,
updatedAt: null
}
],
sources: [],
scannedAt: 1
}
}
beforeEach(() => {
discoverSkillsInWslMock.mockReset()
})
describe('WSL install discovery verification', () => {
it('matches UNC placements against distro-native discovery paths', async () => {
discoverSkillsInWslMock.mockResolvedValue(
discovery(['/home/alice/.agents/skills', '/home/alice/.claude/skills'])
)
const result = await verifySkillInstallDiscovery({
result: installResult([
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.agents\\skills\\review',
'\\\\wsl$\\ubuntu\\home\\alice\\.claude\\skills\\review'
]),
scope: 'global',
homeDirectory: '\\\\wsl.localhost\\Ubuntu\\home\\alice',
wslDistro: 'Ubuntu'
})
expect(result.status).toBe('installed')
expect(result.placements).not.toContainEqual(expect.objectContaining({ status: 'failed' }))
expect(discoverSkillsInWslMock).toHaveBeenCalledWith({
distro: 'Ubuntu',
homeDir: '/home/alice',
cwd: '/home/alice'
})
})
it('normalizes Windows-backed workspace paths to drvfs paths', async () => {
discoverSkillsInWslMock.mockResolvedValue(discovery(['/mnt/c/work/repo/.agents/skills']))
const result = await verifySkillInstallDiscovery({
result: installResult(['C:\\work\\repo\\.agents\\skills\\review']),
scope: 'workspace',
homeDirectory: 'C:\\Users\\alice',
workspaceDirectory: 'C:\\work\\repo',
wslDistro: 'Ubuntu'
})
expect(result.status).toBe('installed')
expect(discoverSkillsInWslMock).toHaveBeenCalledWith({
distro: 'Ubuntu',
homeDir: '/mnt/c/Users/alice',
cwd: '/mnt/c/work/repo'
})
})
it('fails verification without scanning a mismatched UNC distro', async () => {
const result = await verifySkillInstallDiscovery({
result: installResult(['\\\\wsl.localhost\\Debian\\home\\alice\\.agents\\skills\\review']),
scope: 'global',
homeDirectory: '\\\\wsl.localhost\\Debian\\home\\alice',
wslDistro: 'Ubuntu'
})
expect(result).toMatchObject({
status: 'failed',
errorCategory: 'skill-discovery-verification-failed'
})
expect(discoverSkillsInWslMock).not.toHaveBeenCalled()
})
})
@@ -1,14 +1,39 @@
import { dirname, posix, resolve } from 'node:path'
import type { SkillInstallResult, SkillPlacementResult } from '../../shared/skill-install-contract'
import type { SkillDiscoveryResult } from '../../shared/skills'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { discoverSkills } from './discovery'
import { discoverSkillsInWsl } from './skill-discovery-wsl'
function toWslDiscoveryPath(path: string, distro: string): string {
if (path.includes('\0')) {
throw new Error('skill-install-wsl-path-invalid')
}
const parsed = parseWslUncPath(path)
if (parsed) {
if (parsed.distro.toLocaleLowerCase('en-US') !== distro.toLocaleLowerCase('en-US')) {
throw new Error('skill-install-wsl-distro-mismatch')
}
return posix.normalize(parsed.linuxPath)
}
const slashed = path.replace(/\\/g, '/')
const drive = slashed.match(/^([A-Za-z]):\/(.*)$/)
if (drive) {
return posix.join('/mnt', drive[1].toLocaleLowerCase('en-US'), drive[2])
}
if (!posix.isAbsolute(slashed)) {
throw new Error('skill-install-wsl-path-invalid')
}
return posix.normalize(slashed)
}
function normalizedPath(path: string, wslDistro?: string): string {
const normalized = wslDistro ? path.replace(/\/+$/, '') : resolve(path)
const normalized = wslDistro ? toWslDiscoveryPath(path, wslDistro) : resolve(path)
const withoutTrailingSlash =
wslDistro && normalized !== '/' ? normalized.replace(/\/+$/, '') : normalized
return !wslDistro && process.platform === 'win32'
? normalized.toLocaleLowerCase('en-US')
: normalized
? withoutTrailingSlash.toLocaleLowerCase('en-US')
: withoutTrailingSlash
}
function placementIsDiscovered(
@@ -19,7 +44,7 @@ function placementIsDiscovered(
): boolean {
const placementPath = normalizedPath(placement.path, wslDistro)
const placementRoot = normalizedPath(
wslDistro ? posix.dirname(placement.path) : dirname(placement.path),
wslDistro ? posix.dirname(placementPath) : dirname(placement.path),
wslDistro
)
return discovery.skills.some((skill) => {
@@ -44,8 +69,8 @@ async function discoverInstalledSkill(input: {
if (input.wslDistro) {
return discoverSkillsInWsl({
distro: input.wslDistro,
homeDir: input.homeDirectory,
cwd: input.workspaceDirectory ?? input.homeDirectory
homeDir: toWslDiscoveryPath(input.homeDirectory, input.wslDistro),
cwd: toWslDiscoveryPath(input.workspaceDirectory ?? input.homeDirectory, input.wslDistro)
})
}
return discoverSkills({