e3228abb26
При переключении на ветку или коммит без plugin/skills junction'ы навыков повисают битыми, и git checkout падает на середине: часть рабочего дерева уже удалена, а HEAD остаётся на прежней ветке. Remove-AgentSkillLink удаляет сам reparse point через Directory.Delete (без -Recurse), поэтому в target не заходит и содержимое plugin/skills не трогает. Junction опознаётся по атрибуту ReparsePoint, а не по резолву target — иначе режим не работал бы ровно в том случае, ради которого сделан. Настоящий каталог не удаляется, а возвращает Skipped. Режимы скрипта разведены parameter set'ами: -Remove -Check вместе теперь дают ошибку разбора параметров вместо тихого выигрыша одного из ключей. Покрыто пятью кейсами в tools/tests/AgentAssets.Tests.ps1 (включая junction с исчезнувшим источником); порядок действий и восстановление после падения чекаута описаны в CLAUDE.md.
150 lines
8.3 KiB
PowerShell
150 lines
8.3 KiB
PowerShell
BeforeAll {
|
||
Import-Module (Join-Path $PSScriptRoot '..' 'AgentAssets.psm1') -Force
|
||
|
||
function New-TempTree {
|
||
$root = Join-Path ([System.IO.Path]::GetTempPath()) ("agentassets_" + [guid]::NewGuid().ToString('N'))
|
||
$source = Join-Path $root 'plugin/skills/demo'
|
||
New-Item -ItemType Directory -Path $source -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $source 'SKILL.md') -Value 'demo' -Encoding utf8
|
||
return [pscustomobject]@{ Root = $root; Source = $source; Link = (Join-Path $root '.claude/skills/demo') }
|
||
}
|
||
}
|
||
|
||
Describe 'Sync-AgentSkillLink' {
|
||
It 'создаёт junction на исходный навык' {
|
||
$t = New-TempTree
|
||
try {
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Should -Be 'Created'
|
||
(Get-Item -LiteralPath $t.Link -Force).LinkType | Should -Be 'Junction'
|
||
Get-Content -LiteralPath (Join-Path $t.Link 'SKILL.md') | Should -Be 'demo'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'идемпотентен: повторный запуск ничего не ломает' {
|
||
$t = New-TempTree
|
||
try {
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Out-Null
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Should -Be 'AlreadyLinked'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'не трогает настоящий каталог, а падает с понятной ошибкой' {
|
||
$t = New-TempTree
|
||
try {
|
||
New-Item -ItemType Directory -Path $t.Link -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $t.Link 'mine.md') -Value 'ручное' -Encoding utf8
|
||
|
||
{ Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link } | Should -Throw '*не является junction*'
|
||
Get-Content -LiteralPath (Join-Path $t.Link 'mine.md') | Should -Be 'ручное'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'Test-AgentSkillLink различает связанное и несвязанное состояние' {
|
||
$t = New-TempTree
|
||
try {
|
||
Test-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Should -BeFalse
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Out-Null
|
||
Test-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Should -BeTrue
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'бросает понятную ошибку, если SourcePath не существует' {
|
||
$t = New-TempTree
|
||
try {
|
||
$missing = Join-Path $t.Root 'plugin/skills/nope'
|
||
{ Sync-AgentSkillLink -SourcePath $missing -LinkPath $t.Link } | Should -Throw '*нет исходного навыка*'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'перелинковка: ссылка на другой источник не трогает файлы старого источника' {
|
||
$t = New-TempTree
|
||
try {
|
||
$source2 = Join-Path $t.Root 'plugin/skills/demo2'
|
||
New-Item -ItemType Directory -Path $source2 -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $source2 'SKILL.md') -Value 'demo2' -Encoding utf8
|
||
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Should -Be 'Created'
|
||
Sync-AgentSkillLink -SourcePath $source2 -LinkPath $t.Link | Should -Be 'Created'
|
||
|
||
Get-Content -LiteralPath (Join-Path $t.Link 'SKILL.md') | Should -Be 'demo2'
|
||
Get-Content -LiteralPath (Join-Path $t.Source 'SKILL.md') | Should -Be 'demo'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'AllowCopy: ошибка Copy-Item не должна проглатываться независимо от ErrorActionPreference вызывающего' {
|
||
# Явно 'Continue' — воспроизводим вызывающего БЕЗ $ErrorActionPreference='Stop'
|
||
# (run-ps-tests.ps1 сам выставляет 'Stop' на уровне скрипта, и это маскировало бы баг).
|
||
$ErrorActionPreference = 'Continue'
|
||
$t = New-TempTree
|
||
# Родительский каталог создаём заранее — иначе Mock New-Item с -ParameterFilter
|
||
# по Junction перехватит и вызов New-Item -ItemType Directory (родитель LinkPath)
|
||
# и провалится с "No mock matched", маскируя настоящий баг Copy-Item.
|
||
New-Item -ItemType Directory -Path (Split-Path -Parent $t.Link) -Force | Out-Null
|
||
try {
|
||
Mock -ModuleName AgentAssets New-Item -ParameterFilter { $ItemType -eq 'Junction' } { throw 'junction недоступен (симуляция)' }
|
||
# [CmdletBinding()] обязателен: только у advanced-функции переданный вызывающим
|
||
# -ErrorAction реально локализует $ErrorActionPreference внутри тела мока —
|
||
# иначе мок не воспроизводит поведение настоящего Copy-Item.
|
||
Mock -ModuleName AgentAssets Copy-Item {
|
||
[CmdletBinding()]
|
||
param($LiteralPath, $Destination, [switch]$Recurse, [switch]$Force)
|
||
Write-Error 'copy failed (симуляция)'
|
||
}
|
||
|
||
{ Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link -AllowCopy } | Should -Throw
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
}
|
||
|
||
Describe 'Remove-AgentSkillLink' {
|
||
It 'снимает junction, оставляя исходный навык нетронутым' {
|
||
$t = New-TempTree
|
||
try {
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Out-Null
|
||
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Removed'
|
||
Test-Path -LiteralPath $t.Link | Should -BeFalse
|
||
Get-Content -LiteralPath (Join-Path $t.Source 'SKILL.md') | Should -Be 'demo'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'снимает junction с исчезнувшим источником (ветка без plugin/skills)' {
|
||
$t = New-TempTree
|
||
try {
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Out-Null
|
||
Remove-Item -LiteralPath $t.Source -Recurse -Force
|
||
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Removed'
|
||
Test-Path -LiteralPath $t.Link | Should -BeFalse
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'не трогает настоящий каталог' {
|
||
$t = New-TempTree
|
||
try {
|
||
New-Item -ItemType Directory -Path $t.Link -Force | Out-Null
|
||
Set-Content -LiteralPath (Join-Path $t.Link 'mine.md') -Value 'ручное' -Encoding utf8
|
||
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Skipped'
|
||
Get-Content -LiteralPath (Join-Path $t.Link 'mine.md') | Should -Be 'ручное'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'на отсутствующем пути сообщает Missing и не падает' {
|
||
$t = New-TempTree
|
||
try {
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Missing'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
|
||
It 'идемпотентен: повторное снятие даёт Missing' {
|
||
$t = New-TempTree
|
||
try {
|
||
Sync-AgentSkillLink -SourcePath $t.Source -LinkPath $t.Link | Out-Null
|
||
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Removed'
|
||
Remove-AgentSkillLink -LinkPath $t.Link | Should -Be 'Missing'
|
||
} finally { Remove-Item -LiteralPath $t.Root -Recurse -Force -ErrorAction SilentlyContinue }
|
||
}
|
||
}
|