Files
kompas3d-mcp/tools/tests/AgentAssets.Tests.ps1
T
mikhail e3228abb26 feat(tools): режим -Remove у sync-agent-assets — снятие junction'ов перед checkout
При переключении на ветку или коммит без 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.
2026-07-31 08:24:26 +03:00

150 lines
8.3 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 }
}
}