mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-12 04:39:51 +08:00
* ci(windows): exercise MXC Ollama demo with mock API Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> * ci(windows): cover both MXC inference demos Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> * fix(mxc): preserve executable extension resolution Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> * test(mxc): use absolute PowerShell in lifecycle checks Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> * test(mxc): make lifecycle write probes deterministic Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> * test(mxc): assert stable lifecycle completion Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> --------- Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
259 lines
11 KiB
PowerShell
259 lines
11 KiB
PowerShell
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# Runs both shipped MXC inference demos through the real gateway and CLI
|
|
# against the in-process wxc mock and a local HTTP API stub. This proves demo
|
|
# wiring only; it is not evidence of MXC or AppContainer enforcement.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc")]
|
|
[string] $Target,
|
|
|
|
[string] $GatewayPath,
|
|
[string] $CliPath,
|
|
[string] $ArtifactRoot,
|
|
[switch] $KeepArtifacts
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
|
|
if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) {
|
|
throw "windows-mxc-inference-examples-e2e.ps1 requires Windows."
|
|
}
|
|
|
|
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
|
$ExamplesRoot = Join-Path $RepoRoot "crates\openshell-driver-mxc\examples"
|
|
$TargetDir = if ([string]::IsNullOrWhiteSpace($env:CARGO_TARGET_DIR)) {
|
|
Join-Path $RepoRoot "target"
|
|
} else {
|
|
[System.IO.Path]::GetFullPath($env:CARGO_TARGET_DIR)
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($GatewayPath)) {
|
|
$GatewayPath = Join-Path $TargetDir "$Target\release\openshell-gateway.exe"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($CliPath)) {
|
|
$CliPath = Join-Path $TargetDir "$Target\release\openshell.exe"
|
|
}
|
|
$GatewayPath = [System.IO.Path]::GetFullPath($GatewayPath)
|
|
$CliPath = [System.IO.Path]::GetFullPath($CliPath)
|
|
|
|
foreach ($artifact in @($GatewayPath, $CliPath)) {
|
|
if (-not (Test-Path -LiteralPath $artifact -PathType Leaf)) {
|
|
throw "required release artifact is missing: $artifact"
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
|
|
[System.IO.Path]::GetTempPath()
|
|
} else {
|
|
$env:RUNNER_TEMP
|
|
}
|
|
}
|
|
$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot)
|
|
$StageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-inference-examples-e2e-$Target"))
|
|
$expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
|
if (-not $StageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "refusing to use staging path outside artifact root: $StageDir"
|
|
}
|
|
if (Test-Path -LiteralPath $StageDir) {
|
|
Remove-Item -LiteralPath $StageDir -Recurse -Force
|
|
}
|
|
New-Item -ItemType Directory -Path $StageDir | Out-Null
|
|
|
|
foreach ($fixture in @(
|
|
"run-ollama-test.ps1",
|
|
"mxc-ollama.toml",
|
|
"ollama.yaml",
|
|
"run-inference-test.ps1",
|
|
"mxc-inference.toml",
|
|
"inference.yaml"
|
|
)) {
|
|
Copy-Item -LiteralPath (Join-Path $ExamplesRoot $fixture) -Destination $StageDir
|
|
}
|
|
|
|
function Get-AvailablePort {
|
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
|
|
try {
|
|
$listener.Start()
|
|
return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port
|
|
} finally {
|
|
$listener.Stop()
|
|
}
|
|
}
|
|
|
|
function Test-Listener([int] $Port) {
|
|
$client = [System.Net.Sockets.TcpClient]::new()
|
|
try {
|
|
$pending = $client.BeginConnect("127.0.0.1", $Port, $null, $null)
|
|
if (-not $pending.AsyncWaitHandle.WaitOne(250)) { return $false }
|
|
$client.EndConnect($pending)
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
} finally {
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
|
|
$ApiPort = Get-AvailablePort
|
|
$MockToken = "openshell-ci-mock-token"
|
|
$RequestLog = Join-Path $StageDir "mock-inference-requests.log"
|
|
$ServerReady = Join-Path $StageDir "mock-inference.ready"
|
|
$ServerOutLog = Join-Path $StageDir "mock-inference.out.log"
|
|
$ServerErrLog = Join-Path $StageDir "mock-inference.err.log"
|
|
$ServerScript = Join-Path $PSScriptRoot "windows-mxc-inference-stub.ps1"
|
|
$serverProcess = $null
|
|
$passed = $false
|
|
$oldAppData = $env:APPDATA
|
|
$oldLocalAppData = $env:LOCALAPPDATA
|
|
$oldNvApiKey = $env:NV_API_KEY
|
|
|
|
try {
|
|
$serverProcess = Start-Process -FilePath "powershell.exe" -ArgumentList @(
|
|
"-NoProfile",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", "`"$ServerScript`"",
|
|
"-Port", "$ApiPort",
|
|
"-RequestLog", "`"$RequestLog`"",
|
|
"-ReadyPath", "`"$ServerReady`"",
|
|
"-ExpectedBearerToken", $MockToken
|
|
) -PassThru -WindowStyle Hidden -RedirectStandardOutput $ServerOutLog -RedirectStandardError $ServerErrLog
|
|
|
|
$deadline = (Get-Date).AddSeconds(15)
|
|
while ((Get-Date) -lt $deadline -and (-not (Test-Path -LiteralPath $ServerReady) -or -not (Test-Listener $ApiPort))) {
|
|
if ($serverProcess.HasExited) {
|
|
$details = (Get-Content $ServerOutLog, $ServerErrLog -ErrorAction SilentlyContinue) -join [Environment]::NewLine
|
|
throw "mock inference server stopped before listening: $details"
|
|
}
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
if (-not (Test-Listener $ApiPort)) {
|
|
throw "mock inference server did not listen on port $ApiPort within 15 seconds"
|
|
}
|
|
|
|
$env:APPDATA = Join-Path $StageDir "appdata"
|
|
$env:LOCALAPPDATA = Join-Path $StageDir "localappdata"
|
|
New-Item -ItemType Directory -Force -Path $env:APPDATA, $env:LOCALAPPDATA | Out-Null
|
|
|
|
$ollamaRunner = Join-Path $StageDir "run-ollama-test.ps1"
|
|
$ollamaShare = Join-Path $StageDir "ollama-share"
|
|
$ollamaArgs = @(
|
|
"-NoProfile",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", $ollamaRunner,
|
|
"-Mock",
|
|
"-GatewayPath", $GatewayPath,
|
|
"-CliPath", $CliPath,
|
|
"-ShareDir", $ollamaShare,
|
|
"-OllamaPort", "$ApiPort",
|
|
"-Model", "openshell-ci-mock",
|
|
"-Prompt", "Return the CI mock response.",
|
|
"-KeepArtifacts"
|
|
)
|
|
$output = & powershell.exe @ollamaArgs 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
$output | ForEach-Object { Write-Host $_ }
|
|
if ($exitCode -ne 0) {
|
|
throw "shipped Ollama demo failed in mock mode (exit $exitCode)"
|
|
}
|
|
|
|
$ollamaResultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-ollama-*" |
|
|
Sort-Object LastWriteTimeUtc -Descending |
|
|
Select-Object -First 1
|
|
if ($null -eq $ollamaResultDir) { throw "Ollama demo did not produce a results directory" }
|
|
|
|
$ollamaSummary = Get-Content -LiteralPath (Join-Path $ollamaResultDir.FullName "summary.txt") -Raw
|
|
if ($ollamaSummary -notmatch '(?m)^verdict=PASS\s*$' -or $ollamaSummary -notmatch '(?m)^mode=mock-wiring\s*$') {
|
|
throw "Ollama demo summary did not report a mock-wiring PASS: $ollamaSummary"
|
|
}
|
|
$ollamaResponse = Get-Content -LiteralPath (Join-Path $ollamaResultDir.FullName "ollama-response.json") -Raw | ConvertFrom-Json
|
|
if ($ollamaResponse.response -ne "Hello from the CI mock.") {
|
|
throw "Ollama demo did not retain the expected mock completion"
|
|
}
|
|
|
|
$env:NV_API_KEY = $MockToken
|
|
$cloudRunner = Join-Path $StageDir "run-inference-test.ps1"
|
|
$cloudShare = Join-Path $StageDir "cloud-share"
|
|
$cloudApiUrl = "http://127.0.0.1:$ApiPort/v1/chat/completions"
|
|
$cloudArgs = @(
|
|
"-NoProfile",
|
|
"-ExecutionPolicy", "Bypass",
|
|
"-File", $cloudRunner,
|
|
"-Mock",
|
|
"-GatewayPath", $GatewayPath,
|
|
"-CliPath", $CliPath,
|
|
"-ShareDir", $cloudShare,
|
|
"-ApiUrl", $cloudApiUrl,
|
|
"-Model", "openshell-ci-mock",
|
|
"-Prompt", "Return the CI mock response.",
|
|
"-KeepArtifacts"
|
|
)
|
|
$output = & powershell.exe @cloudArgs 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
$output | ForEach-Object { Write-Host $_ }
|
|
if ($exitCode -ne 0) {
|
|
throw "shipped cloud-inference demo failed in mock mode (exit $exitCode)"
|
|
}
|
|
|
|
$cloudResultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-inference-*" |
|
|
Sort-Object LastWriteTimeUtc -Descending |
|
|
Select-Object -First 1
|
|
if ($null -eq $cloudResultDir) { throw "cloud-inference demo did not produce a results directory" }
|
|
|
|
$cloudSummary = Get-Content -LiteralPath (Join-Path $cloudResultDir.FullName "summary.txt") -Raw
|
|
if ($cloudSummary -notmatch '(?m)^verdict=PASS\s*$' -or $cloudSummary -notmatch '(?m)^mode=mock-wiring\s*$') {
|
|
throw "cloud-inference demo summary did not report a mock-wiring PASS: $cloudSummary"
|
|
}
|
|
$cloudResponse = Get-Content -LiteralPath (Join-Path $cloudResultDir.FullName "inference-response.json") -Raw | ConvertFrom-Json
|
|
if ($cloudResponse.choices[0].message.content -ne "Hello from the CI mock.") {
|
|
throw "cloud-inference demo did not retain the expected mock completion"
|
|
}
|
|
|
|
$requestLines = @([System.IO.File]::ReadAllLines($RequestLog))
|
|
$expectedRequests = @(
|
|
@{ Pattern = '^GET /api/tags HTTP/\S+ authorization=absent$'; Count = 2; Description = 'host prerequisite and sandbox Ollama tag requests' },
|
|
@{ Pattern = '^POST /api/generate HTTP/\S+ authorization=absent$'; Count = 1; Description = 'sandbox Ollama generation request' },
|
|
@{ Pattern = '^POST /v1/chat/completions HTTP/\S+ authorization=synthetic$'; Count = 1; Description = 'sandbox cloud-inference request with the synthetic CI credential' }
|
|
)
|
|
foreach ($expected in $expectedRequests) {
|
|
$actualCount = @($requestLines | Where-Object { $_ -match $expected.Pattern }).Count
|
|
if ($actualCount -ne $expected.Count) {
|
|
throw "mock inference server observed $actualCount $($expected.Description); expected $($expected.Count): $($requestLines -join '; ')"
|
|
}
|
|
}
|
|
|
|
$passed = $true
|
|
Write-Host "MXC inference examples mock E2E passed for $Target"
|
|
} catch {
|
|
Write-Host "MXC inference examples mock E2E failed: $($_.Exception.Message)" -ForegroundColor Red
|
|
Get-ChildItem -LiteralPath $StageDir -File -Recurse -Include "*.log", "summary.txt" -ErrorAction SilentlyContinue |
|
|
ForEach-Object {
|
|
Write-Host "--- $($_.FullName) ---"
|
|
Get-Content -LiteralPath $_.FullName -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ }
|
|
}
|
|
throw
|
|
} finally {
|
|
$env:APPDATA = $oldAppData
|
|
$env:LOCALAPPDATA = $oldLocalAppData
|
|
if ([string]::IsNullOrWhiteSpace($oldNvApiKey)) {
|
|
Remove-Item Env:NV_API_KEY -ErrorAction SilentlyContinue
|
|
} else {
|
|
$env:NV_API_KEY = $oldNvApiKey
|
|
}
|
|
if ($serverProcess -and -not $serverProcess.HasExited) {
|
|
Stop-Process -Id $serverProcess.Id -Force -ErrorAction SilentlyContinue
|
|
try { [void] $serverProcess.WaitForExit(5000) } catch {}
|
|
}
|
|
if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $StageDir)) {
|
|
Remove-Item -LiteralPath $StageDir -Recurse -Force
|
|
} else {
|
|
Write-Host "MXC inference example artifacts: $StageDir"
|
|
}
|
|
}
|