fix(plugin): жёсткий разбор пути к серверу и обёртка запуска в лаунчере
- Test-Path -PathType Leaf: путь-каталог в KOMPAS_MCP_EXE больше не проходит проверку молча — раньше падение случалось на голом & $exe сырым CommandNotFoundException в обход диагностической обёртки. - запуск сервера (& $exe @args) обёрнут в try/catch: сбой самого запуска (битый бинарь, недостающая зависимость, отказ в доступе) теперь тоже даёт чистое сообщение 'kompas-mcp launcher: ...' и код 1, а не сырой трейс; ненулевой код возврата уже запущенного сервера по-прежнему пробрасывается как есть. - тесты: три новых сценария (каталог вместо файла, ноль байт в stdout на пути отказа, повреждённый exe) + первый тест переписан на побайтовую проверку stdout через Start-Process без фильтрации пустых строк.
This commit is contained in:
@@ -17,7 +17,9 @@ try {
|
||||
$exe = Resolve-KompasMcpExecutable -Lock $lock
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $exe)) {
|
||||
# -PathType Leaf: без него путь к существующему каталогу молча проходит проверку,
|
||||
# а падает уже голый вызов "& $exe" сырым CommandNotFoundException в обход catch ниже.
|
||||
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
|
||||
throw "не найден сервер: $exe"
|
||||
}
|
||||
}
|
||||
@@ -26,6 +28,17 @@ catch {
|
||||
exit 1
|
||||
}
|
||||
|
||||
& $exe @args
|
||||
$serverExit = $LASTEXITCODE
|
||||
try {
|
||||
# Сплаттинг @args во внешний процесс: пустые строки-аргументы теряются (особенность
|
||||
# PowerShell) — для MCP-клиента безвредно, он серверу аргументов не передаёт.
|
||||
& $exe @args
|
||||
$serverExit = $LASTEXITCODE
|
||||
}
|
||||
catch {
|
||||
# Сбой самого запуска (битый бинарь, недостающая зависимость, отказ в доступе) —
|
||||
# не путать с ненулевым кодом возврата уже запущенного сервера, это не exception.
|
||||
[Console]::Error.WriteLine("kompas-mcp launcher: не удалось запустить сервер: $($_.Exception.Message)")
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit $serverExit
|
||||
@@ -1,13 +1,42 @@
|
||||
BeforeAll {
|
||||
$script:Launcher = Join-Path $PSScriptRoot '..' '..' 'plugin' 'scripts' 'launch-kompas-mcp.ps1'
|
||||
|
||||
# Запускает лаунчер через Start-Process с раздельным перехватом stdout/stderr во
|
||||
# временные файлы — в отличие от захвата пайплайном (`& powershell ...`), это даёт
|
||||
# точный побайтовый результат каждого потока, а не строки, склеенные PowerShell.
|
||||
function Invoke-Launcher {
|
||||
param([string[]] $LauncherArgs = @())
|
||||
|
||||
$stdOut = [System.IO.Path]::GetTempFileName()
|
||||
$stdErr = [System.IO.Path]::GetTempFileName()
|
||||
$psArgs = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $script:Launcher) + $LauncherArgs
|
||||
|
||||
$proc = Start-Process -FilePath 'powershell' -ArgumentList $psArgs -NoNewWindow -Wait -PassThru `
|
||||
-RedirectStandardOutput $stdOut -RedirectStandardError $stdErr
|
||||
|
||||
return [pscustomobject]@{
|
||||
ExitCode = $proc.ExitCode
|
||||
StdOutPath = $stdOut
|
||||
StdErrPath = $stdErr
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-LauncherResult {
|
||||
param($Result)
|
||||
Remove-Item -LiteralPath $Result.StdOutPath, $Result.StdErrPath -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'launch-kompas-mcp.ps1' {
|
||||
It 'не сорит в stdout: наружу идёт только вывод сервера' {
|
||||
$env:KOMPAS_MCP_EXE = $env:ComSpec
|
||||
try {
|
||||
$out = & powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $script:Launcher /c echo PONG
|
||||
($out | Where-Object { $_.Trim().Length -gt 0 }) -join "`n" | Should -Be 'PONG'
|
||||
$result = Invoke-Launcher -LauncherArgs @('/c', 'echo', 'PONG')
|
||||
try {
|
||||
# Побайтовое (посимвольное) сравнение — без фильтрации пустых строк,
|
||||
# чтобы лишний символ/строка в stdout не проскочила незамеченной.
|
||||
(Get-Content -LiteralPath $result.StdOutPath -Raw) | Should -Be "PONG`r`n"
|
||||
} finally { Remove-LauncherResult $result }
|
||||
} finally { Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
@@ -32,4 +61,56 @@ Describe 'launch-kompas-mcp.ps1' {
|
||||
($stderr | Out-String) | Should -Match 'kompas-mcp launcher'
|
||||
} finally { Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue }
|
||||
}
|
||||
|
||||
It 'падает с кодом 1 и диагностическим сообщением (не сырым трейсом), когда KOMPAS_MCP_EXE указывает на каталог' {
|
||||
$dir = Join-Path ([System.IO.Path]::GetTempPath()) ('kompasmcp_dir_' + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
$env:KOMPAS_MCP_EXE = $dir
|
||||
try {
|
||||
$result = Invoke-Launcher
|
||||
try {
|
||||
$result.ExitCode | Should -Be 1
|
||||
$stderrText = Get-Content -LiteralPath $result.StdErrPath -Raw
|
||||
$stderrText | Should -Match 'kompas-mcp launcher'
|
||||
$stderrText | Should -Not -Match 'CommandNotFoundException'
|
||||
} finally { Remove-LauncherResult $result }
|
||||
} finally {
|
||||
Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It 'на пути отказа не пишет в stdout ни одного байта' {
|
||||
$dir = Join-Path ([System.IO.Path]::GetTempPath()) ('kompasmcp_dir_' + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
$env:KOMPAS_MCP_EXE = $dir
|
||||
try {
|
||||
$result = Invoke-Launcher
|
||||
try {
|
||||
$result.ExitCode | Should -Be 1
|
||||
# Длина файла в байтах, а не строковый вывод — один лишний байт в stdout
|
||||
# ломает JSON-RPC на всю MCP-сессию, это самый критичный инвариант лаунчера.
|
||||
(Get-Item -LiteralPath $result.StdOutPath).Length | Should -Be 0
|
||||
} finally { Remove-LauncherResult $result }
|
||||
} finally {
|
||||
Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
It 'падает с кодом 1 и диагностическим сообщением, когда KOMPAS_MCP_EXE указывает на повреждённый exe' {
|
||||
$badExe = Join-Path ([System.IO.Path]::GetTempPath()) ('kompasmcp_bad_' + [guid]::NewGuid().ToString('N') + '.exe')
|
||||
Set-Content -LiteralPath $badExe -Value 'это не исполняемый файл' -Encoding ascii
|
||||
$env:KOMPAS_MCP_EXE = $badExe
|
||||
try {
|
||||
$result = Invoke-Launcher
|
||||
try {
|
||||
$result.ExitCode | Should -Be 1
|
||||
(Get-Content -LiteralPath $result.StdErrPath -Raw) | Should -Match 'kompas-mcp launcher'
|
||||
} finally { Remove-LauncherResult $result }
|
||||
} finally {
|
||||
Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $badExe -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user