fix(tools): UTF-8 BOM для PowerShell 5.1, безопасный AllowCopy, доп. тесты

- AgentAssets.psm1 и sync-agent-assets.ps1 пересохранены в UTF-8 с BOM:
  powershell.exe читает файлы без BOM в системной ANSI-кодировке и ломает
  парсинг на кириллических строковых литералах (pwsh 7 работал за счёт
  автодетекта и маскировал баг).
- Copy-Item в ветке -AllowCopy получил -ErrorAction Stop: без него ошибка
  копирования писалась как non-terminating и функция возвращала 'Copied'
  при частично скопированном каталоге, если вызывающий не выставил
  $ErrorActionPreference='Stop' сам.
- Test-AgentSkillLink сравнивает пути через
  [string]::Equals(...,OrdinalIgnoreCase) явно, а не через регистронезависимый -eq.
- Добавлены тесты: отсутствующий SourcePath, перелинковка на другой
  источник (старый target остаётся нетронутым) и AllowCopy-регрессия
  (мок Copy-Item с [CmdletBinding()], чтобы корректно эмулировать
  ErrorAction реального cmdlet).
This commit is contained in:
2026-07-31 01:58:06 +03:00
parent 97e95d5f44
commit 62fbc3742b
3 changed files with 52 additions and 4 deletions
+4 -3
View File
@@ -1,4 +1,4 @@
Set-StrictMode -Version Latest
Set-StrictMode -Version Latest
<#
.SYNOPSIS
@@ -15,7 +15,8 @@ function Test-AgentSkillLink {
if ($null -eq $item -or $item.LinkType -ne 'Junction') { return $false }
$actual = [System.IO.Path]::GetFullPath(($item.Target | Select-Object -First 1))
return $actual.TrimEnd('\') -eq ([System.IO.Path]::GetFullPath($SourcePath)).TrimEnd('\')
$expected = [System.IO.Path]::GetFullPath($SourcePath)
return [string]::Equals($actual.TrimEnd('\'), $expected.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
}
<#
@@ -60,7 +61,7 @@ function Sync-AgentSkillLink {
if (-not $AllowCopy) {
throw "не удалось создать junction $LinkPath -> $SourcePath ($($_.Exception.Message)). Повторите с -AllowCopy, если копия допустима."
}
Copy-Item -LiteralPath $SourcePath -Destination $LinkPath -Recurse -Force
Copy-Item -LiteralPath $SourcePath -Destination $LinkPath -Recurse -Force -ErrorAction Stop
return 'Copied'
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
#requires -Version 5.1
#requires -Version 5.1
[CmdletBinding()]
param(
[switch] $Check,
+47
View File
@@ -47,4 +47,51 @@ Describe 'Sync-AgentSkillLink' {
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 }
}
}