0ed211fe16
Три предметных замечания ревью качества: - Resolve-KompasMcpExecutable: WaitOne() перенесён в try/finally, отдельно ловится AbandonedMutexException (владелец умер посреди установки — владение переходит к нам, это не ошибка); ReleaseMutex вызывается только если mutex реально захвачен, иначе он бросает SynchronizationLockException и маскирует первопричину. Добавлен -Downloader passthrough для тестируемости. - Read-KompasMcpLock: разбор JSON обёрнут в try/catch с сообщением, называющим файл; наличие полей version/url/sha256 проверяется через PSObject.Properties.Name (обращение к отсутствующему свойству под Set-StrictMode -Version Latest бросает PropertyNotFoundException); sha256 валидируется на 64 hex-символа. - Добавлены тесты на конкурентную логику: Resolve-KompasMcpExecutable (повторный вызов не скачивает), гонка "сосед выиграл" (целевой каталог уже содержит годную/негодную установку) в Install-KompasMcpServer, счастливый путь и все новые ветки валидации в Read-KompasMcpLock. Мелочи: & $Downloader ... | Out-Null (внедряемый загрузчик не должен писать в success stream); уточнён комментарий Test-KompasMcpInstall — оно намеренно трактует любую ошибку чтения маркера (не только битый JSON, но и блокировку антивирусом) как "установка негодная". AbandonedMutexException-ветка вручную воспроизведена и проверена: процесс- владелец mutex убит, пока другой процесс уже блокирован в WaitOne() — тот получает AbandonedMutexException и корректно продолжает работу.
218 lines
11 KiB
PowerShell
218 lines
11 KiB
PowerShell
BeforeAll {
|
|
Import-Module (Join-Path $PSScriptRoot '..' '..' 'plugin' 'scripts' 'KompasMcpBootstrap.psm1') -Force
|
|
|
|
function New-TempRoot {
|
|
$root = Join-Path ([System.IO.Path]::GetTempPath()) ("kompasmcp_" + [guid]::NewGuid().ToString('N'))
|
|
New-Item -ItemType Directory -Path $root -Force | Out-Null
|
|
return $root
|
|
}
|
|
|
|
# Готовит zip с «сервером» и возвращает лок с настоящим SHA256.
|
|
function New-FakeRelease([string] $Root, [string] $Version) {
|
|
$payload = Join-Path $Root 'payload'
|
|
New-Item -ItemType Directory -Path $payload -Force | Out-Null
|
|
Set-Content -LiteralPath (Join-Path $payload 'kompas-mcp.exe') -Value 'not-a-real-exe' -Encoding ascii
|
|
|
|
$zip = Join-Path $Root 'release.zip'
|
|
Compress-Archive -Path (Join-Path $payload '*') -DestinationPath $zip -Force
|
|
Remove-Item -LiteralPath $payload -Recurse -Force
|
|
|
|
$sha = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
return [pscustomobject]@{
|
|
Zip = $zip
|
|
Lock = [pscustomobject]@{ version = $Version; sourceSha = 'deadbeef'; url = "file:///$zip"; sha256 = $sha }
|
|
}
|
|
}
|
|
}
|
|
|
|
Describe 'Install-KompasMcpServer' {
|
|
It 'ставит сервер, пишет маркер и возвращает путь к каталогу версии' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
|
|
$dir = Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader {
|
|
param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force
|
|
}
|
|
|
|
$dir | Should -Be (Join-Path $installRoot '1.0.0')
|
|
Test-Path -LiteralPath (Join-Path $dir 'kompas-mcp.exe') | Should -BeTrue
|
|
(Get-Content -LiteralPath (Join-Path $dir 'install.json') -Raw | ConvertFrom-Json).sha256 |
|
|
Should -Be $rel.Lock.sha256
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'отказывается ставить при несовпадении SHA256 и не оставляет каталог версии' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$rel.Lock.sha256 = '0' * 64
|
|
$installRoot = Join-Path $root 'install'
|
|
|
|
{ Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader {
|
|
param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force
|
|
} } | Should -Throw '*SHA256*'
|
|
|
|
Test-Path -LiteralPath (Join-Path $installRoot '1.0.0') | Should -BeFalse
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'не скачивает повторно, если установка уже годная' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
$script:calls = 0
|
|
$downloader = { param($Url, $Destination) $script:calls++; Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force }
|
|
|
|
Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader | Out-Null
|
|
Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader | Out-Null
|
|
|
|
$script:calls | Should -Be 1
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'считает установку негодной, если маркер не совпадает с локом' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
$dir = Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader {
|
|
param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force
|
|
}
|
|
|
|
Set-Content -LiteralPath (Join-Path $dir 'install.json') -Value '{"version":"1.0.0","sha256":"нечто"}' -Encoding utf8
|
|
Test-KompasMcpInstall -Directory $dir -Lock $rel.Lock | Should -BeFalse
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'гонка: если целевой каталог версии уже содержит годную установку, не скачивает и возвращает его путь' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
$target = Join-Path $installRoot '1.0.0'
|
|
|
|
# Имитируем "соседа", который уже успешно установил версию раньше нас.
|
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
|
Set-Content -LiteralPath (Join-Path $target 'kompas-mcp.exe') -Value 'not-a-real-exe' -Encoding ascii
|
|
$neighborMarker = [ordered]@{ version = $rel.Lock.version; sha256 = $rel.Lock.sha256 }
|
|
Set-Content -LiteralPath (Join-Path $target 'install.json') -Value ($neighborMarker | ConvertTo-Json) -Encoding utf8
|
|
|
|
$script:calls = 0
|
|
$downloader = { param($Url, $Destination) $script:calls++; Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force }
|
|
|
|
$dir = Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader
|
|
|
|
$dir | Should -Be $target
|
|
$script:calls | Should -Be 0
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'гонка: если целевой каталог версии существует, но установка в нём негодная, бросает внятную ошибку' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
$target = Join-Path $installRoot '1.0.0'
|
|
|
|
# Каталог версии существует (например, остался от оборванной установки),
|
|
# но без валидного маркера/exe — установка в нём негодная.
|
|
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
|
|
|
{ Install-KompasMcpServer -Lock $rel.Lock -InstallRoot $installRoot -Downloader {
|
|
param($Url, $Destination) Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force
|
|
} } | Should -Throw '*негодная*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
}
|
|
|
|
Describe 'Resolve-KompasMcpExecutable' {
|
|
It 'возвращает путь к exe и не скачивает повторно при втором вызове' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$rel = New-FakeRelease -Root $root -Version '1.0.0'
|
|
$installRoot = Join-Path $root 'install'
|
|
$script:calls = 0
|
|
$downloader = { param($Url, $Destination) $script:calls++; Copy-Item -LiteralPath $rel.Zip -Destination $Destination -Force }
|
|
|
|
$exe1 = Resolve-KompasMcpExecutable -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader
|
|
$exe2 = Resolve-KompasMcpExecutable -Lock $rel.Lock -InstallRoot $installRoot -Downloader $downloader
|
|
|
|
$exe1 | Should -Be (Join-Path $installRoot '1.0.0' 'kompas-mcp.exe')
|
|
$exe2 | Should -Be $exe1
|
|
Test-Path -LiteralPath $exe1 | Should -BeTrue
|
|
$script:calls | Should -Be 1
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
}
|
|
|
|
Describe 'Read-KompasMcpLock' {
|
|
It 'падает внятно, когда релиза ещё нет' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
Set-Content -LiteralPath $path -Value '{"version":"0.0.0","sourceSha":"","url":"","sha256":""}' -Encoding utf8
|
|
|
|
{ Read-KompasMcpLock -Path $path } | Should -Throw '*релиз*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'счастливый путь: валидный опубликованный лок разбирается и возвращает поля' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
$sha = '1' * 64
|
|
Set-Content -LiteralPath $path -Value "{`"version`":`"1.2.3`",`"sourceSha`":`"deadbeef`",`"url`":`"https://example.invalid/release.zip`",`"sha256`":`"$sha`"}" -Encoding utf8
|
|
|
|
$lock = Read-KompasMcpLock -Path $path
|
|
|
|
$lock.version | Should -Be '1.2.3'
|
|
$lock.url | Should -Be 'https://example.invalid/release.zip'
|
|
$lock.sha256 | Should -Be $sha
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'падает внятно на битом JSON' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
Set-Content -LiteralPath $path -Value '{ не json вообще' -Encoding utf8
|
|
|
|
{ Read-KompasMcpLock -Path $path } | Should -Throw '*server.lock.json*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'падает внятно, если в валидном JSON нет поля version' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
Set-Content -LiteralPath $path -Value '{"sourceSha":"deadbeef","url":"https://example.invalid/release.zip","sha256":"1111111111111111111111111111111111111111111111111111111111111111"}' -Encoding utf8
|
|
|
|
{ Read-KompasMcpLock -Path $path } | Should -Throw '*version*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'падает внятно, если у опубликованной версии нет поля url' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
$sha = '1' * 64
|
|
Set-Content -LiteralPath $path -Value "{`"version`":`"1.2.3`",`"sourceSha`":`"deadbeef`",`"sha256`":`"$sha`"}" -Encoding utf8
|
|
|
|
{ Read-KompasMcpLock -Path $path } | Should -Throw '*url*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
It 'падает внятно, если у опубликованной версии sha256 пустой или некорректной длины' {
|
|
$root = New-TempRoot
|
|
try {
|
|
$path = Join-Path $root 'server.lock.json'
|
|
Set-Content -LiteralPath $path -Value '{"version":"1.2.3","sourceSha":"deadbeef","url":"https://example.invalid/release.zip","sha256":"beef"}' -Encoding utf8
|
|
|
|
{ Read-KompasMcpLock -Path $path } | Should -Throw '*sha256*'
|
|
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
}
|