diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdcb605ad..2d5950308 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,28 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
## [Unreleased]
+## [v0.6.12] - 2026-04-01
+
+### Fixed
+
+- Reworked Windows preflight and Docker runtime-manager process capture so
+ large `docker manifest inspect` payloads no longer deadlock the bounded
+ timeout path, preserving truthful Windows NI preflight and runtime
+ determinism outcomes instead of hanging the release-proof surface.
+
+### Added
+
+- Regression coverage in `tests/Invoke-DockerRuntimeManager.Tests.ps1` and
+ `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1` proving large manifest
+ output still completes cleanly through the shared timeout helper.
+
+### Changed
+
+- Built the `v0.6.12` release line on top of the published `v0.6.11` stable
+ baseline, preserving the Windows NI proof authority and VI-history
+ native-path repair while adding the preflight deadlock fix required for a
+ clean release rail.
+
## [v0.6.11] - 2026-04-01
### Fixed
diff --git a/Directory.Build.props b/Directory.Build.props
index eb321ef70..936fc2e2e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,9 +7,9 @@
false
true
true
- 0.6.11
- 0.6.11.0
- 0.6.11.0
+ 0.6.12
+ 0.6.12.0
+ 0.6.12.0
$(Version)+local
package-first
project
diff --git a/Invoke-PesterTests.ps1 b/Invoke-PesterTests.ps1
index e4a7ee1c4..4ae31a763 100644
--- a/Invoke-PesterTests.ps1
+++ b/Invoke-PesterTests.ps1
@@ -567,6 +567,9 @@ $ErrorActionPreference = 'Stop'
if (-not (Get-Variable -Name includeIntegrationBool -Scope Script -ErrorAction SilentlyContinue)) {
$script:includeIntegrationBool = $false
}
+$script:stuckGuardEnabled = $false
+$script:hbPath = $null
+$script:partialLogPath = $null
$script:executionPackResolved = 'full'
$script:executionPackReason = 'default'
$script:executionPackBaseIncludePatterns = @()
@@ -1204,11 +1207,6 @@ Write-Host " Script Root: $root"
Write-Host " Tests Directory: $testsDir"; if ($limitToSingle) { Write-Host " Single Test File: $singleTestFile" }
Write-Host " Results Directory: $resultsDir"
Write-Host ""
-$script:dispatcherEventsPath = Join-Path $resultsDir 'dispatcher-events.ndjson'
-Write-DispatcherEventLine -Level info -Phase 'lifecycle' -Message 'Dispatcher session initialized.' -Data @{
- testsDir = $testsDir
- resultsDir = $resultsDir
-}
# Validate tests directory exists
if (-not (Test-Path -LiteralPath $testsDir -PathType Container)) {
@@ -1306,6 +1304,27 @@ function _Build-Snapshot {
return $index
}
+# Artifact tracking pre-snapshot (optional). Capture before any dispatcher-owned
+# result files are written so isolated runs report those files as created rather
+# than modified.
+$script:artifactTrail = $null
+$script:executionFinalizeContextPath = $null
+$preIndex = $null
+$artifactRoots = @()
+if ($TrackArtifacts) {
+ if (-not $ArtifactGlobs -or $ArtifactGlobs.Count -eq 0) {
+ $ArtifactGlobs = @('tests/results','results','tmp-agg/results','scratch-schema-test/results')
+ }
+ $artifactRoots = _Resolve-ArtifactRoots -Roots $ArtifactGlobs -Base $root
+ try { $preIndex = _Build-Snapshot -Roots $artifactRoots -Base $root } catch { $preIndex = @{} }
+}
+
+$script:dispatcherEventsPath = Join-Path $resultsDir 'dispatcher-events.ndjson'
+Write-DispatcherEventLine -Level info -Phase 'lifecycle' -Message 'Dispatcher session initialized.' -Data @{
+ testsDir = $testsDir
+ resultsDir = $resultsDir
+}
+
# Hard gate: never start tests while LabVIEW.exe is running
$labviewOpen = @(_Find-ProcsByPattern -Patterns @('LabVIEW') )
if ($labviewOpen.Count -gt 0) {
@@ -1765,19 +1784,6 @@ if ($CleanLabVIEW) {
_Report-Procs -Names @('LabVIEW')
}
-# Artifact tracking pre-snapshot (optional)
-$script:artifactTrail = $null
-$script:executionFinalizeContextPath = $null
-$preIndex = $null
-$artifactRoots = @()
-if ($TrackArtifacts) {
- if (-not $ArtifactGlobs -or $ArtifactGlobs.Count -eq 0) {
- $ArtifactGlobs = @('tests/results','results','tmp-agg/results','scratch-schema-test/results')
- }
- $artifactRoots = _Resolve-ArtifactRoots -Roots $ArtifactGlobs -Base $root
- try { $preIndex = _Build-Snapshot -Roots $artifactRoots -Base $root } catch { $preIndex = @{} }
-}
-
# Count test files (respect single file mode)
if ($limitToSingle) {
$testFiles = @([IO.FileInfo]::new($singleTestFile))
diff --git a/docs/archive/releases/RELEASE_NOTES_v0.6.12.md b/docs/archive/releases/RELEASE_NOTES_v0.6.12.md
new file mode 100644
index 000000000..29a575938
--- /dev/null
+++ b/docs/archive/releases/RELEASE_NOTES_v0.6.12.md
@@ -0,0 +1,38 @@
+# Release Notes v0.6.12
+
+`v0.6.12` is a maintenance release that carries the published `v0.6.11` stable
+line forward and fixes the Windows preflight process-capture deadlock exposed
+when Docker returns large manifest output.
+
+## Highlights
+
+- The released backend no longer deadlocks while collecting large Docker
+ manifest payloads during Windows preflight and runtime-manager checks.
+- A shared timeout helper now owns bounded process execution and concurrent
+ stdout/stderr draining for the Windows preflight seam.
+- The stable release keeps the `v0.6.11` Windows NI proof authority and
+ VI-history native-path repair instead of regressing to an older integration
+ surface.
+
+## Included maintenance slice
+
+- `#2091` fix: Windows preflight process capture deadlock
+- carry forward the published `v0.6.11` release-line policy and proof surfaces
+
+## Validation highlights
+
+- Focused Pester coverage proves large-manifest replay no longer blocks either
+ `tools/Invoke-DockerRuntimeManager.ps1` or
+ `tools/Test-WindowsNI2026q1HostPreflight.ps1`.
+- The release branch is prepared from the published `v0.6.11` line and merged
+ with the post-`#2091` `develop` tip, preserving the already-published stable
+ fixes while adding the Windows preflight deadlock repair.
+- Release helper docs, changelog, and version surfaces align on `0.6.12`.
+
+## Consumer impact
+
+- Stable consumers should move from `@v0.6.11` to `@v0.6.12` to pick up the
+ Windows preflight/runtime-manager deadlock repair.
+- `comparevi-history` should repin `comparevi-backend-ref.txt` to `v0.6.12`
+ before rerunning the clone-backed `ni/labview-icon-editor` proof on the
+ released backend.
diff --git a/docs/release/PR_NOTES.md b/docs/release/PR_NOTES.md
index 27026a95f..dc0f0b8f4 100644
--- a/docs/release/PR_NOTES.md
+++ b/docs/release/PR_NOTES.md
@@ -1,37 +1,41 @@
-# Release v0.6.11 - PR Notes Helper
+# Release v0.6.12 - PR Notes Helper
-Reference sheet for the `v0.6.11` maintenance release. This cut carries the
-`v0.6.10` stable line forward and adds the VI-history native-path repair proven
-on the Windows NI Docker-backed proof surface.
+Reference sheet for the `v0.6.12` maintenance release. This cut carries the
+published `v0.6.11` stable line forward and adds the Windows preflight
+process-capture deadlock fix proven on the Windows NI Docker-backed proof
+surface.
## 1. Summary
-Release `v0.6.11` focuses on three themes:
+Release `v0.6.12` focuses on three themes:
-- **VI-history native-path correctness**: the released backend now resolves
- Windows NI proof-surface file paths without losing the repository-relative VI
- target, so hosted and local replay lanes can reach the real history target
- instead of a synthetic temp-root mismatch.
+- **Deadlock-free Windows preflight capture**: the released backend now drains
+ large Docker manifest output without stalling the bounded timeout path, so
+ Windows NI preflight and runtime-manager checks can report real readiness
+ instead of hanging.
- **Windows NI proof continuity**: the LabVIEW Docker-backed Windows proof path
- remains the authoritative hosted execution surface, and `v0.6.11` is cut
- directly from the `v0.6.10` stable line so the released proof authority is
- preserved rather than reintroduced from stale `develop`.
+ remains the authoritative hosted execution surface, and `v0.6.12` is cut
+ directly from the published `v0.6.11` stable line so the released proof
+ authority is preserved rather than reintroduced from stale `develop`.
- **Consumer-ready repin path**: the release packet is aligned for the next
`comparevi-history` pin bump and the clone-backed `ni/labview-icon-editor`
history proof rerun on the newly published backend.
## 2. Maintenance Highlights
-- `tools/Compare-VIHistory.ps1`, `tools/Compare-RefsToTemp.ps1`, and
- `tools/Render-VIHistoryReport.ps1` now preserve the intended VI-history
- target path across the Windows NI proof surface instead of collapsing to a
- host-native path that the replay layer cannot certify.
-- `tests/TestFileExistsAtRef.Tests.ps1` and
- `tests/CompareVI.GitRefs.VI2.Tests.ps1` now cover the backend-side path and
- git-ref seams that caused the Windows proof regression.
-- Stable release surfaces now pin `0.6.11`, while the helper docs still point
- consumers at `v0.6.10` until publication completes.
+- `tools/ProcessTimeoutHelper.ps1` now centralizes bounded process execution
+ and concurrent stdout/stderr capture for the Docker-backed Windows proof
+ seam.
+- `tools/Invoke-DockerRuntimeManager.ps1`,
+ `tools/Assert-DockerRuntimeDeterminism.ps1`, and
+ `tools/Test-WindowsNI2026q1HostPreflight.ps1` now share that helper so large
+ manifest responses cannot deadlock the release-proof path.
+- `tests/Invoke-DockerRuntimeManager.Tests.ps1` and
+ `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1` now prove large manifest
+ output stays non-blocking on the authoritative Windows surface.
+- Stable release surfaces now pin `0.6.12`, while the helper docs still point
+ consumers at `v0.6.11` until publication completes.
## 3. Validation Snapshot
@@ -44,12 +48,12 @@ Release `v0.6.11` focuses on three themes:
- [ ] Hosted Windows NI Docker proof is green on the release branch.
- [ ] Local-proof autonomy selector still emits the truthful next proof
surface instead of looping or hanging.
-- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.11` completes from
+- [ ] `node tools/npm/run-script.mjs release:finalize -- 0.6.12` completes from
a clean helper lane and writes fresh finalize metadata under
`tests/results/_agent/release/`
-- [ ] Published release `v0.6.11` includes the signed distribution assets,
+- [ ] Published release `v0.6.12` includes the signed distribution assets,
`SHA256SUMS.txt`, `sbom.spdx.json`, and `provenance.json`
-- [ ] `comparevi-history` repins `comparevi-backend-ref.txt` to `v0.6.11`
+- [ ] `comparevi-history` repins `comparevi-backend-ref.txt` to `v0.6.12`
before the clone-backed `ni/labview-icon-editor` proof is rerun
## 4. Reviewer Focus
@@ -58,23 +62,25 @@ Release `v0.6.11` focuses on three themes:
- `package.json`
- `Directory.Build.props`
- `tools/CompareVI.Tools/CompareVI.Tools.psd1`
-- Review the released Windows NI Docker proof and VI-history backend surfaces for correctness:
- - `.github/workflows/windows-ni-proof-reusable.yml`
- - `tools/Compare-VIHistory.ps1`
- - `tools/Compare-RefsToTemp.ps1`
- - `tools/Render-VIHistoryReport.ps1`
+- Review the released Windows preflight/runtime-manager surfaces for
+ correctness:
+ - `tools/ProcessTimeoutHelper.ps1`
+ - `tools/Invoke-DockerRuntimeManager.ps1`
+ - `tools/Test-WindowsNI2026q1HostPreflight.ps1`
+ - `tests/Invoke-DockerRuntimeManager.Tests.ps1`
+ - `tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1`
- Review the release helper packet for consistency:
- `CHANGELOG.md`
- `docs/release/TAG_PREP_CHECKLIST.md`
- - `docs/archive/releases/RELEASE_NOTES_v0.6.11.md`
+ - `docs/archive/releases/RELEASE_NOTES_v0.6.12.md`
## 5. Follow-Up After Stable
-1. Re-pin `comparevi-history` from `v0.6.10` to `v0.6.11` and rerun the
+1. Re-pin `comparevi-history` from `v0.6.11` to `v0.6.12` and rerun the
clone-backed `ni/labview-icon-editor` proof on the released backend.
2. Confirm the Windows NI proof artifacts and the published benchmark packet
still agree on the certified backend version after the repin.
3. Re-evaluate the current emitted history surface against the real developer
question before treating any mode as decision-ready.
---- Updated: 2026-04-01 (prepared for the `v0.6.11` maintenance cut).
+--- Updated: 2026-04-01 (prepared for the `v0.6.12` maintenance cut).
diff --git a/docs/release/README.md b/docs/release/README.md
index ab9ed6058..0aa8ed8a7 100644
--- a/docs/release/README.md
+++ b/docs/release/README.md
@@ -7,7 +7,9 @@ work.
## Contents
- `PR_NOTES.md` - release PR summary helper
-- `RELEASE_EVIDENCE_v0.6.6.md` - latest immutable release and certified-consumer evidence packet retained in-repo while the `v0.6.11` cut supersedes the published `v0.6.10` baseline
+- `RELEASE_EVIDENCE_v0.6.6.md` - latest immutable release and
+ certified-consumer evidence packet retained in-repo while the `v0.6.12` cut
+ supersedes the published `v0.6.11` baseline
- `TAG_PREP_CHECKLIST.md` - tag preparation checklist
- `POST_RELEASE_FOLLOWUPS.md` - post-release backlog tracker
- `ROLLBACK_PLAN.md` - rollback procedure reference
diff --git a/docs/release/TAG_PREP_CHECKLIST.md b/docs/release/TAG_PREP_CHECKLIST.md
index a99859896..2e473222b 100644
--- a/docs/release/TAG_PREP_CHECKLIST.md
+++ b/docs/release/TAG_PREP_CHECKLIST.md
@@ -1,14 +1,14 @@
-# v0.6.11 Tag Preparation Checklist
+# v0.6.12 Tag Preparation Checklist
-Helper reference for cutting or replaying the `v0.6.11` maintenance release.
+Helper reference for cutting or replaying the `v0.6.12` maintenance release.
Aligns with the archived release notes
-(`../archive/releases/RELEASE_NOTES_v0.6.11.md`) and the checked-in stable
+(`../archive/releases/RELEASE_NOTES_v0.6.12.md`) and the checked-in stable
release surfaces.
## 1. Pre-flight Verification
-- [ ] Work from `release/v0.6.11` and ensure it contains the final maintenance
+- [ ] Work from `release/v0.6.12` and ensure it contains the final maintenance
changes.
- [ ] CI is green on the release branch (`lint`, `pester / normalize`,
`smoke-gate`, `Policy Guard (Upstream) / policy-guard`,
@@ -22,26 +22,30 @@ release surfaces.
## 2. Version & Metadata Consistency
- [ ] `CHANGELOG.md` contains a finalized
- `## [v0.6.11] - 2026-04-01` section.
-- [ ] Stable docs reference `v0.6.10` consistently until `v0.6.11` publication
- completes, and the release helper packet references `v0.6.11`
+ `## [v0.6.12] - 2026-04-01` section.
+- [ ] Stable docs reference `v0.6.11` consistently until `v0.6.12` publication
+ completes, and the release helper packet references `v0.6.12`
consistently.
- [ ] `package.json`, `Directory.Build.props`, and
- `tools/CompareVI.Tools/CompareVI.Tools.psd1` all report `0.6.11`.
+ `tools/CompareVI.Tools/CompareVI.Tools.psd1` all report `0.6.12`.
- [ ] `docs/action-outputs.md` still matches `action.yml`.
- [ ] Update `docs/documentation-manifest.json` if release-doc coverage changed.
-## 3. Bundle Contract Regression Validation
+## 3. Windows Preflight Regression Validation
-- [ ] Focused bundle regression tests pass locally:
+- [ ] Focused Windows preflight/runtime-manager regression tests pass locally:
```bash
-pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1 -BundleArchivePath tests/results/_agent/bundle-fix/artifacts/CompareVI.Tools-v0.6.11.zip -ResultsDir tests/results/_agent/bundle-fix/certification -SummaryJsonPath tests/results/_agent/bundle-fix/certification/summary.json
+pwsh -NoLogo -NoProfile -File Invoke-PesterTests.ps1 -TestsPath tests/Invoke-DockerRuntimeManager.Tests.ps1
+pwsh -NoLogo -NoProfile -File Invoke-PesterTests.ps1 -TestsPath tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1
```
- [ ] Confirm the released Windows NI / LabVIEW Docker image proof surface is intact:
- [ ] The released surface still includes:
+ `tools/ProcessTimeoutHelper.ps1`
+ `tools/Invoke-DockerRuntimeManager.ps1`
+ and
`tools/Run-NIWindowsContainerCompare.ps1`
and
`tools/Test-WindowsNI2026q1HostPreflight.ps1`
@@ -56,9 +60,9 @@ pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1
## 4. Release Materials Review
- [ ] `PR_NOTES.md`, this checklist, and
- `../archive/releases/RELEASE_NOTES_v0.6.11.md` are consistent.
-- [ ] `README.md` and `docs/USAGE_GUIDE.md` still treat `v0.6.10` as the
- previously released stable pin until `v0.6.11` publication completes.
+ `../archive/releases/RELEASE_NOTES_v0.6.12.md` are consistent.
+- [ ] `README.md` and `docs/USAGE_GUIDE.md` still treat `v0.6.11` as the
+ previously released stable pin until `v0.6.12` publication completes.
## 5. Tag Creation
@@ -66,7 +70,7 @@ pwsh -NoLogo -NoProfile -File tools/Test-CompareVIHistoryBundleCertification.ps1
```pwsh
node tools/npm/run-script.mjs priority:release:signing:readiness
-node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel stable --version 0.6.11
+node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel stable --version 0.6.12
```
- [ ] Confirm `tests/results/_agent/release/release-signing-readiness.json`
@@ -77,35 +81,35 @@ node tools/npm/run-script.mjs priority:release:conductor -- --apply --channel st
- [ ] Create an annotated stable tag:
```pwsh
-git tag -a v0.6.11 -m "v0.6.11: repair VI history native paths on Windows proof surfaces"
+git tag -a v0.6.12 -m "v0.6.12: fix Windows preflight process capture deadlock"
```
- [ ] Push the tag:
```pwsh
-git push origin v0.6.11
+git push origin v0.6.12
```
## 6. Validation After Publish
-- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.11` from a
+- [ ] Run `node tools/npm/run-script.mjs release:finalize -- 0.6.12` from a
clean helper lane to fast-forward `main` and `develop`, then record the
finalize metadata.
-- [ ] Install the bundle via `@v0.6.11` in a sample workflow and confirm the
+- [ ] Install the bundle via `@v0.6.12` in a sample workflow and confirm the
released Windows NI and hosted VI-history contracts execute without a
local source-tree override.
- [ ] Optional maintainer fast loop: run the local Windows Docker replay lane
for `vi-history-scenarios-windows` and confirm it still mirrors the
hosted contract without replacing the hosted proof requirement.
-- [ ] Re-pin `comparevi-history` to `v0.6.11` and confirm the clone-backed
+- [ ] Re-pin `comparevi-history` to `v0.6.12` and confirm the clone-backed
`ni/labview-icon-editor` proof reaches real comparisons on the released
backend.
## 7. Communication
-- [ ] Announce the maintenance cut, calling out the promoted Windows NI Docker
- proof surface and the required `comparevi-history` repin.
-- [ ] Notify consumers that `v0.6.11` supersedes `v0.6.10` as the supported
+- [ ] Announce the maintenance cut, calling out the Windows preflight deadlock
+ fix and the required `comparevi-history` repin.
+- [ ] Notify consumers that `v0.6.12` supersedes `v0.6.11` as the supported
stable pin.
---- Updated: 2026-04-01 (prepared for the `v0.6.11` maintenance cut).
+--- Updated: 2026-04-01 (prepared for the `v0.6.12` maintenance cut).
diff --git a/package-lock.json b/package-lock.json
index 7dc06fb2a..c73cc2f1a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "compare-vi-cli-action",
- "version": "0.6.11",
+ "version": "0.6.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "compare-vi-cli-action",
- "version": "0.6.11",
+ "version": "0.6.12",
"license": "BSD-3-Clause",
"dependencies": {
"argparse": "^2.0.1",
diff --git a/package.json b/package.json
index 333ec937c..7516971f1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "compare-vi-cli-action",
"private": true,
- "version": "0.6.11",
+ "version": "0.6.12",
"license": "BSD-3-Clause",
"type": "module",
"scripts": {
diff --git a/tests/Assert-DockerRuntimeDeterminism.Tests.ps1 b/tests/Assert-DockerRuntimeDeterminism.Tests.ps1
index 498c949ff..bffbd1067 100644
--- a/tests/Assert-DockerRuntimeDeterminism.Tests.ps1
+++ b/tests/Assert-DockerRuntimeDeterminism.Tests.ps1
@@ -157,7 +157,7 @@ exit 0
"@
Set-Content -LiteralPath (Join-Path $binDir 'wsl.cmd') -Value $wslCmd -Encoding ascii
- $env:PATH = "{0};{1}" -f $binDir, $env:PATH
+ $env:PATH = "{0}{1}{2}" -f $binDir, [System.IO.Path]::PathSeparator, $env:PATH
}
}
@@ -173,6 +173,7 @@ exit 0
DOCKER_STUB_CONTEXT_USE_FAIL_TARGET = $env:DOCKER_STUB_CONTEXT_USE_FAIL_TARGET
DOCKER_COMMAND_OVERRIDE = $env:DOCKER_COMMAND_OVERRIDE
DOCKER_HOST = $env:DOCKER_HOST
+ RUNNER_OS = $env:RUNNER_OS
}
}
@@ -209,11 +210,13 @@ exit 0
Set-Item Env:DOCKER_STUB_INFO_MODE 'daemon-unavailable'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$false `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
@@ -259,11 +262,13 @@ exit 9
Set-Item Env:DOCKER_COMMAND_OVERRIDE (Join-Path $work 'bin' 'docker.ps1')
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-linux'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType linux `
-ExpectedContext desktop-linux `
+ -HostPlatformOverride Windows `
-AutoRepair:$false `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
@@ -281,12 +286,14 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'unparseable-success'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$githubOutput = Join-Path $work 'github-output.txt'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$false `
-SnapshotPath $snapshotPath `
-GitHubOutputPath $githubOutput 2>&1
@@ -308,11 +315,13 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'daemon-unavailable'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-ManageDockerEngine:$true `
-SnapshotPath $snapshotPath `
@@ -333,11 +342,13 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-linux'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-ManageDockerEngine:$true `
-AllowHostEngineMutation:$false `
@@ -365,6 +376,7 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows'
Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$githubOutput = Join-Path $work 'github-output.txt'
@@ -372,6 +384,7 @@ exit 9
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-SnapshotPath $snapshotPath `
-GitHubOutputPath $githubOutput 2>&1
@@ -399,11 +412,13 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows'
Set-Item Env:DOCKER_STUB_CONTEXT 'default'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
@@ -424,11 +439,13 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-windows'
Set-Item Env:DOCKER_STUB_CONTEXT 'default'
Set-Item Env:DOCKER_STUB_INFO_FAIL_CONTEXT 'desktop-windows'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType windows `
-ExpectedContext desktop-windows `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
@@ -448,12 +465,14 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux'
Set-Item Env:DOCKER_STUB_INFO_JSON '{"OSType":"linux","OperatingSystem":"Ubuntu 24.04.1 LTS","Name":"ubuntu-native","Platform":{"Name":"Docker Engine - Community"},"Labels":["maintainer=comparevi"]}'
Set-Item Env:DOCKER_HOST 'unix:///var/run/docker.sock'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType linux `
-RuntimeProvider native-wsl `
-ExpectedDockerHost 'unix:///var/run/docker.sock' `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
@@ -478,12 +497,14 @@ exit 9
Set-Item Env:DOCKER_STUB_INFO_MODE 'parsed-linux'
Set-Item Env:DOCKER_STUB_INFO_JSON '{"OSType":"linux","OperatingSystem":"Docker Desktop","Name":"docker-desktop","Platform":{"Name":"Docker Desktop 4.41.0"},"Labels":["com.docker.desktop.address=npipe://"]}'
Set-Item Env:DOCKER_HOST 'unix:///var/run/docker.sock'
+ Set-Item Env:RUNNER_OS 'Windows'
$snapshotPath = Join-Path $work 'runtime.json'
$output = & pwsh -NoLogo -NoProfile -File $script:GuardScript `
-ExpectedOsType linux `
-RuntimeProvider native-wsl `
-ExpectedDockerHost 'unix:///var/run/docker.sock' `
+ -HostPlatformOverride Windows `
-AutoRepair:$true `
-SnapshotPath $snapshotPath `
-GitHubOutputPath '' 2>&1
diff --git a/tests/FunctionShadowing.Nested.Tests.ps1 b/tests/FunctionShadowing.Nested.Tests.ps1
index de638d85e..f9bc87ad3 100644
--- a/tests/FunctionShadowing.Nested.Tests.ps1
+++ b/tests/FunctionShadowing.Nested.Tests.ps1
@@ -41,9 +41,19 @@ Describe "Inner Smoke" {
$toolsDir = Join-Path $workspace 'tools'
New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
- $trackerModule = Join-Path $repoRoot 'tools' 'LabVIEWPidTracker.psm1'
- if (Test-Path -LiteralPath $trackerModule -PathType Leaf) {
- Copy-Item -Path $trackerModule -Destination $toolsDir -Force
+ # Keep the nested workspace aligned with dispatcher-side tool growth.
+ $supportFiles = @(
+ 'LabVIEWPidTracker.psm1',
+ 'PesterExecutionPacks.ps1',
+ 'Invoke-PesterExecutionPostprocess.ps1',
+ 'PesterServiceModelSchema.ps1',
+ 'Get-PesterResultXmlSummary.ps1'
+ )
+ foreach ($supportFile in $supportFiles) {
+ $supportPath = Join-Path $repoRoot 'tools' $supportFile
+ if (Test-Path -LiteralPath $supportPath -PathType Leaf) {
+ Copy-Item -Path $supportPath -Destination $toolsDir -Force
+ }
}
$dispatcherTools = Join-Path $repoRoot 'tools' 'Dispatcher'
@@ -68,7 +78,12 @@ Describe "Inner Smoke" {
$nestedExit | Should -Be 0
$json.failed | Should -Be 0
$json.errors | Should -Be 0
- $json.discoveryFailures | Should -Be 0 -Because 'Shadowing smoke test should not introduce discovery failures'
+ $discoveryFailures = if ($json.PSObject.Properties.Name -contains 'discoveryFailures') {
+ [int]$json.discoveryFailures
+ } else {
+ 0
+ }
+ $discoveryFailures | Should -Be 0 -Because 'Shadowing smoke test should not introduce discovery failures'
if ($json.failed -gt 0 -or $json.errors -gt 0 -or $json.discoveryFailures -gt 0) {
Write-Host '[nested-shadow] DEBUG OUTPUT START' -ForegroundColor Yellow
($innerOutput | Out-String) | Write-Host
diff --git a/tests/Invoke-DockerRuntimeManager.Tests.ps1 b/tests/Invoke-DockerRuntimeManager.Tests.ps1
index 79753c7b9..f85067d63 100644
--- a/tests/Invoke-DockerRuntimeManager.Tests.ps1
+++ b/tests/Invoke-DockerRuntimeManager.Tests.ps1
@@ -109,8 +109,16 @@ if ($Args[0] -eq 'info') {
}
if ($Args[0] -eq 'manifest' -and $Args.Count -ge 3 -and $Args[1] -eq 'inspect') {
+ $paddingBytes = [Environment]::GetEnvironmentVariable('DOCKER_STUB_MANIFEST_PADDING_BYTES')
+ $padding = ''
+ if (-not [string]::IsNullOrWhiteSpace($paddingBytes)) {
+ $padding = ('x' * [int]$paddingBytes)
+ }
$manifest = [ordered]@{
schemaVersion = 2
+ annotations = [ordered]@{
+ padding = $padding
+ }
manifests = @(
[ordered]@{
digest = 'sha256:1111111111111111111111111111111111111111111111111111111111111111'
@@ -242,6 +250,7 @@ exit 0
DOCKER_STUB_RUN_FAIL_LINUX = $env:DOCKER_STUB_RUN_FAIL_LINUX
DOCKER_STUB_INFO_SLEEP_SECONDS = $env:DOCKER_STUB_INFO_SLEEP_SECONDS
DOCKER_STUB_INSPECT_SLEEP_SECONDS = $env:DOCKER_STUB_INSPECT_SLEEP_SECONDS
+ DOCKER_STUB_MANIFEST_PADDING_BYTES = $env:DOCKER_STUB_MANIFEST_PADDING_BYTES
DOCKER_STUB_PULL_SLEEP_WINDOWS = $env:DOCKER_STUB_PULL_SLEEP_WINDOWS
DOCKER_STUB_PULL_SLEEP_LINUX = $env:DOCKER_STUB_PULL_SLEEP_LINUX
DOCKER_STUB_RUN_SLEEP_WINDOWS = $env:DOCKER_STUB_RUN_SLEEP_WINDOWS
@@ -456,6 +465,32 @@ exit 0
$json.probes.windows.probe.status | Should -Be 'timeout'
}
+ It 'handles large manifest output without deadlocking the timeout helper' {
+ $work = Join-Path $TestDrive 'large-manifest-output'
+ New-Item -ItemType Directory -Path $work -Force | Out-Null
+ & $script:CreateDockerStub -WorkRoot $work
+
+ Set-Item Env:DOCKER_STUB_STATE_PATH (Join-Path $work 'docker-state.json')
+ Set-Item Env:DOCKER_STUB_INITIAL_CONTEXT 'desktop-windows'
+ Set-Item Env:DOCKER_STUB_MANIFEST_PADDING_BYTES '20000'
+ Set-Item Env:RUNNER_TEMP (Join-Path $work 'runner-temp')
+
+ $jsonPath = Join-Path $work 'docker-runtime-manager.json'
+ $output = @(& pwsh -NoLogo -NoProfile -File $script:ManagerScript `
+ -ProbeScope windows `
+ -OutputJsonPath $jsonPath `
+ -CommandTimeoutSeconds 5 `
+ -SwitchRetryCount 1 `
+ -SwitchTimeoutSeconds 30 2>&1)
+
+ $LASTEXITCODE | Should -Be 0 -Because ($output -join "`n")
+
+ $json = Get-Content -LiteralPath $jsonPath -Raw | ConvertFrom-Json -Depth 30
+ $json.status | Should -Be 'success'
+ $json.probes.windows.status | Should -Be 'success'
+ $json.probes.windows.digest | Should -Be 'sha256:1111111111111111111111111111111111111111111111111111111111111111'
+ }
+
It 'fails with lock timeout when the runtime manager lock is held by another process' {
$work = Join-Path $TestDrive 'lock-timeout'
New-Item -ItemType Directory -Path $work -Force | Out-Null
diff --git a/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1 b/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1
index c80f4da4d..e95c05cb1 100644
--- a/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1
+++ b/tests/NestedDispatcher.DiscoveryFailureRegression.Tests.ps1
@@ -23,12 +23,26 @@ Describe 'Nested Dispatcher Discovery Failure Regression' -Tag 'Unit' {
Set-Content -Path (Join-Path $testsDir 'Broken.Tests.ps1') -Value $badLines -Encoding UTF8
$dispatcherCopy = Join-Path $workspace 'Invoke-PesterTests.ps1'
- Copy-Item -Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'Invoke-PesterTests.ps1') -Destination $dispatcherCopy
+ $repoRoot = Split-Path $PSScriptRoot -Parent
+ Copy-Item -Path (Join-Path $repoRoot 'Invoke-PesterTests.ps1') -Destination $dispatcherCopy
$toolsDir = Join-Path $workspace 'tools'
- $trackerModule = Join-Path (Split-Path $PSScriptRoot -Parent) 'tools' 'LabVIEWPidTracker.psm1'
- if (Test-Path -LiteralPath $trackerModule -PathType Leaf) {
- New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
- Copy-Item -Path $trackerModule -Destination $toolsDir -Force
+ New-Item -ItemType Directory -Path $toolsDir -Force | Out-Null
+ $supportFiles = @(
+ 'LabVIEWPidTracker.psm1',
+ 'PesterExecutionPacks.ps1',
+ 'Invoke-PesterExecutionPostprocess.ps1',
+ 'PesterServiceModelSchema.ps1',
+ 'Get-PesterResultXmlSummary.ps1'
+ )
+ foreach ($supportFile in $supportFiles) {
+ $supportPath = Join-Path $repoRoot 'tools' $supportFile
+ if (Test-Path -LiteralPath $supportPath -PathType Leaf) {
+ Copy-Item -Path $supportPath -Destination $toolsDir -Force
+ }
+ }
+ $dispatcherTools = Join-Path $repoRoot 'tools' 'Dispatcher'
+ if (Test-Path -LiteralPath $dispatcherTools -PathType Container) {
+ Copy-Item -Path $dispatcherTools -Destination (Join-Path $toolsDir 'Dispatcher') -Recurse -Force
}
Push-Location $workspace
diff --git a/tests/Render-VIHistoryReport.Tests.ps1 b/tests/Render-VIHistoryReport.Tests.ps1
index d05351cd2..b216080da 100644
--- a/tests/Render-VIHistoryReport.Tests.ps1
+++ b/tests/Render-VIHistoryReport.Tests.ps1
@@ -380,4 +380,30 @@ Describe 'Render-VIHistoryReport.ps1' -Tag 'Unit' {
$facade.status | Should -Be 'ok'
$facade.reason | Should -Be 'within-limit'
}
+
+ It 'renders the checked-in offline corpus suite without an explicit history context' {
+ $manifestPath = Join-Path $script:repoRoot 'fixtures' 'cross-repo' 'labview-icon-editor' 'settings-init' 'manifest.json'
+ $resultsRoot = Join-Path $TestDrive 'history-results-offline-corpus'
+ $markdownPath = Join-Path $resultsRoot 'history-report.md'
+ $htmlPath = Join-Path $resultsRoot 'history-report.html'
+ $stepSummaryPath = Join-Path $resultsRoot 'history-summary.md'
+
+ & $script:scriptPath `
+ -ManifestPath $manifestPath `
+ -OutputDir $resultsRoot `
+ -MarkdownPath $markdownPath `
+ -EmitHtml `
+ -HtmlPath $htmlPath `
+ -StepSummaryPath $stepSummaryPath | Out-Null
+
+ Test-Path -LiteralPath $markdownPath | Should -BeTrue
+ Test-Path -LiteralPath $htmlPath | Should -BeTrue
+ Test-Path -LiteralPath $stepSummaryPath | Should -BeTrue
+
+ $markdown = Get-Content -LiteralPath $markdownPath -Raw
+ $markdown | Should -Match '## Observed interpretation'
+ $markdown | Should -Match '\| Coverage Class \| `[^`]+` \|'
+ $markdown | Should -Match '\| Mode Sensitivity \| `single-mode-observed` \|'
+ $markdown | Should -Match '\| Outcome Labels \| `clean`, `signal-diff` \|'
+ }
}
diff --git a/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1 b/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1
index 09e2dc340..b7510739a 100644
--- a/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1
+++ b/tests/Test-WindowsNI2026q1HostPreflight.Tests.ps1
@@ -76,6 +76,31 @@ if ($Args[0] -eq 'info') {
exit 0
}
+if ($Args[0] -eq 'manifest' -and $Args.Count -ge 3 -and $Args[1] -eq 'inspect') {
+ $paddingBytes = [Environment]::GetEnvironmentVariable('DOCKER_STUB_MANIFEST_PADDING_BYTES')
+ $padding = ''
+ if (-not [string]::IsNullOrWhiteSpace($paddingBytes)) {
+ $padding = ('x' * [int]$paddingBytes)
+ }
+ $manifest = [ordered]@{
+ schemaVersion = 2
+ annotations = [ordered]@{
+ padding = $padding
+ }
+ manifests = @(
+ [ordered]@{
+ digest = 'sha256:1111111111111111111111111111111111111111111111111111111111111111'
+ platform = [ordered]@{
+ os = 'windows'
+ architecture = 'amd64'
+ }
+ }
+ )
+ }
+ ($manifest | ConvertTo-Json -Depth 10) | Write-Output
+ exit 0
+}
+
if ($Args[0] -eq 'image' -and $Args.Count -ge 2 -and $Args[1] -eq 'inspect') {
$inspectSleep = [Environment]::GetEnvironmentVariable('DOCKER_STUB_INSPECT_SLEEP_SECONDS')
if (-not [string]::IsNullOrWhiteSpace($inspectSleep)) {
@@ -165,6 +190,7 @@ exit 0
DOCKER_STUB_INFO_EXITCODE = $env:DOCKER_STUB_INFO_EXITCODE
DOCKER_STUB_RUN_STDERR = $env:DOCKER_STUB_RUN_STDERR
DOCKER_STUB_RUN_EXITCODE = $env:DOCKER_STUB_RUN_EXITCODE
+ DOCKER_STUB_MANIFEST_PADDING_BYTES = $env:DOCKER_STUB_MANIFEST_PADDING_BYTES
DOCKER_STUB_INSPECT_SLEEP_SECONDS = $env:DOCKER_STUB_INSPECT_SLEEP_SECONDS
DOCKER_STUB_PULL_SLEEP_SECONDS = $env:DOCKER_STUB_PULL_SLEEP_SECONDS
DOCKER_STUB_RUN_SLEEP_SECONDS = $env:DOCKER_STUB_RUN_SLEEP_SECONDS
@@ -351,4 +377,34 @@ exit 0
$json.bootstrap.attempted | Should -BeFalse
$json.probe.attempted | Should -BeFalse
}
+
+ It 'keeps desktop-local preflight ready when manifest output is large' {
+ $work = Join-Path $TestDrive 'desktop-local-large-manifest'
+ New-Item -ItemType Directory -Path $work -Force | Out-Null
+ & $script:CreateDockerHostedStubs -WorkRoot $work
+
+ Set-Item Env:DOCKER_STUB_CONTEXT 'desktop-windows'
+ Set-Item Env:DOCKER_STUB_OSTYPE 'windows'
+ Set-Item Env:DOCKER_STUB_IMAGE_EXISTS '1'
+ Set-Item Env:DOCKER_STUB_MANIFEST_PADDING_BYTES '20000'
+ Set-Item Env:RUNNER_TEMP (Join-Path $work 'runner-temp')
+
+ $resultsRoot = Join-Path $work 'results'
+ $outputJsonPath = Join-Path $resultsRoot 'windows-ni-2026q1-host-preflight.json'
+
+ $output = @(& pwsh -NoLogo -NoProfile -File $script:ToolPath `
+ -Image 'nationalinstruments/labview:2026q1-windows' `
+ -ResultsDir $resultsRoot `
+ -ExecutionSurface 'desktop-local' `
+ -CommandTimeoutSeconds 5 `
+ -OutputJsonPath $outputJsonPath `
+ -GitHubOutputPath '' `
+ -StepSummaryPath '' 2>&1)
+ $LASTEXITCODE | Should -Be 0 -Because ($output -join "`n")
+
+ $json = Get-Content -LiteralPath $outputJsonPath -Raw | ConvertFrom-Json -Depth 20
+ $json.status | Should -Be 'ready'
+ $json.bootstrap.imagePresent | Should -BeTrue
+ $json.probe.status | Should -Be 'success'
+ }
}
diff --git a/tests/_helpers/DispatcherTestHelper.psm1 b/tests/_helpers/DispatcherTestHelper.psm1
index d73e50ae2..02d0a8cad 100644
--- a/tests/_helpers/DispatcherTestHelper.psm1
+++ b/tests/_helpers/DispatcherTestHelper.psm1
@@ -86,7 +86,6 @@ function Invoke-DispatcherSafe {
$psi.EnvironmentVariables['SINGLE_INVOKER'] = '1'
$psi.EnvironmentVariables['SUPPRESS_NESTED_DISCOVERY'] = '1'
$psi.EnvironmentVariables['STUCK_GUARD'] = '0'
- $psi.EnvironmentVariables['DISABLE_SINGLE_INVOKER'] = '1'
$psi.EnvironmentVariables['SUPPRESS_PATTERN_SELFTEST'] = '1'
$baseline = Get-PwshProcessIds
diff --git a/tools/Assert-DockerRuntimeDeterminism.ps1 b/tools/Assert-DockerRuntimeDeterminism.ps1
index fa6462244..b406bed6c 100644
--- a/tools/Assert-DockerRuntimeDeterminism.ps1
+++ b/tools/Assert-DockerRuntimeDeterminism.ps1
@@ -90,6 +90,8 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1')
+
function Write-GitHubOutput {
param(
[Parameter(Mandatory = $true)][string]$Key,
@@ -193,41 +195,13 @@ function Invoke-ProcessWithTimeout {
exception = ''
}
- $psi = [System.Diagnostics.ProcessStartInfo]::new()
- $psi.FileName = $resolvedFilePath
- $psi.UseShellExecute = $false
- $psi.RedirectStandardOutput = $true
- $psi.RedirectStandardError = $true
- $psi.CreateNoWindow = $true
- foreach ($arg in @($effectiveArguments)) {
- [void]$psi.ArgumentList.Add([string]$arg)
- }
-
- $proc = [System.Diagnostics.Process]::new()
- $proc.StartInfo = $psi
-
- try {
- [void]$proc.Start()
- $completed = $proc.WaitForExit($safeTimeout * 1000)
- if (-not $completed) {
- $result.timedOut = $true
- try { $proc.Kill($true) } catch {}
- return [pscustomobject]$result
- }
-
- $result.exitCode = [int]$proc.ExitCode
- $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd())
- $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd())
- } catch {
- $result.exception = [string]$_.Exception.Message
- try {
- if (-not $proc.HasExited) {
- $proc.Kill($true)
- }
- } catch {}
- } finally {
- $proc.Dispose()
- }
+ $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout
+ $result.timedOut = [bool]$invoke.TimedOut
+ $result.exitCode = $invoke.ExitCode
+ $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ })
+ $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ })
+ $result.command = [string]$invoke.Command
+ $result.exception = [string]$invoke.Exception
return [pscustomobject]$result
}
@@ -1000,9 +974,18 @@ if ($ExpectedOsType -eq 'windows') {
$hostAlignmentOk = $false
$reason = "RUNNER_OS is '$runnerOsRaw', expected Windows."
}
-} elseif (-not [string]::IsNullOrWhiteSpace($runnerOsNormalized) -and $runnerOsNormalized -ne 'linux') {
- $hostAlignmentOk = $false
- $reason = "RUNNER_OS is '$runnerOsRaw', expected Linux for linux lane."
+} elseif (-not [string]::IsNullOrWhiteSpace($runnerOsNormalized)) {
+ # Linux Docker lanes are valid on Windows Docker Desktop / native-wsl hosts,
+ # but non-Windows hosts still need a Linux runner identity.
+ if ($hostIsWindows) {
+ if ($runnerOsNormalized -notin @('windows', 'linux')) {
+ $hostAlignmentOk = $false
+ $reason = "RUNNER_OS is '$runnerOsRaw', expected Windows or Linux for linux lane on a Windows host."
+ }
+ } elseif ($runnerOsNormalized -ne 'linux') {
+ $hostAlignmentOk = $false
+ $reason = "RUNNER_OS is '$runnerOsRaw', expected Linux for linux lane."
+ }
}
$observedDockerHost = if ([string]::IsNullOrWhiteSpace($env:DOCKER_HOST)) { $null } else { $env:DOCKER_HOST.Trim() }
diff --git a/tools/CompareVI.Tools/CompareVI.Tools.psd1 b/tools/CompareVI.Tools/CompareVI.Tools.psd1
index 6eee6cc22..926dfc0bf 100644
--- a/tools/CompareVI.Tools/CompareVI.Tools.psd1
+++ b/tools/CompareVI.Tools/CompareVI.Tools.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'CompareVI.Tools.psm1'
- ModuleVersion = '0.6.11'
+ ModuleVersion = '0.6.12'
GUID = '1f9b5f7f-1ab6-4db9-8e36-6b7a6d5e9c8f'
Author = 'LabVIEW Community CI'
CompanyName = 'LabVIEW Community'
diff --git a/tools/Invoke-DockerRuntimeManager.ps1 b/tools/Invoke-DockerRuntimeManager.ps1
index fa59acfd2..c08920e12 100644
--- a/tools/Invoke-DockerRuntimeManager.ps1
+++ b/tools/Invoke-DockerRuntimeManager.ps1
@@ -44,6 +44,8 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1')
+
function Resolve-AbsolutePath {
param([Parameter(Mandatory)][string]$Path)
if ([System.IO.Path]::IsPathRooted($Path)) {
@@ -146,41 +148,13 @@ function Invoke-ProcessWithTimeout {
exception = ''
}
- $psi = [System.Diagnostics.ProcessStartInfo]::new()
- $psi.FileName = $resolvedFilePath
- $psi.UseShellExecute = $false
- $psi.RedirectStandardOutput = $true
- $psi.RedirectStandardError = $true
- $psi.CreateNoWindow = $true
- foreach ($arg in @($effectiveArguments)) {
- [void]$psi.ArgumentList.Add([string]$arg)
- }
-
- $proc = [System.Diagnostics.Process]::new()
- $proc.StartInfo = $psi
-
- try {
- [void]$proc.Start()
- $completed = $proc.WaitForExit($safeTimeout * 1000)
- if (-not $completed) {
- $result.timedOut = $true
- try { $proc.Kill($true) } catch {}
- return [pscustomobject]$result
- }
-
- $result.exitCode = [int]$proc.ExitCode
- $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd())
- $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd())
- } catch {
- $result.exception = [string]$_.Exception.Message
- try {
- if (-not $proc.HasExited) {
- $proc.Kill($true)
- }
- } catch {}
- } finally {
- $proc.Dispose()
- }
+ $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout
+ $result.timedOut = [bool]$invoke.TimedOut
+ $result.exitCode = $invoke.ExitCode
+ $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ })
+ $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ })
+ $result.command = [string]$invoke.Command
+ $result.exception = [string]$invoke.Exception
return [pscustomobject]$result
}
diff --git a/tools/ProcessTimeoutHelper.ps1 b/tools/ProcessTimeoutHelper.ps1
new file mode 100644
index 000000000..1b62d9a52
--- /dev/null
+++ b/tools/ProcessTimeoutHelper.ps1
@@ -0,0 +1,147 @@
+#Requires -Version 7.0
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+if (-not ('CompareVI.ProcessInvokeHelper' -as [type])) {
+ Add-Type -TypeDefinition @"
+using System;
+using System.Diagnostics;
+using System.Text;
+using System.Threading;
+
+namespace CompareVI {
+ public sealed class ProcessInvokeResult {
+ public bool TimedOut { get; set; }
+ public int? ExitCode { get; set; }
+ public string[] Stdout { get; set; } = Array.Empty();
+ public string[] Stderr { get; set; } = Array.Empty();
+ public string Command { get; set; } = "";
+ public string Exception { get; set; } = "";
+ }
+
+ public static class ProcessInvokeHelper {
+ private static string[] SplitLines(string value) {
+ if (string.IsNullOrEmpty(value)) {
+ return Array.Empty();
+ }
+
+ return value
+ .Replace("\r\n", "\n")
+ .Replace('\r', '\n')
+ .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ public static ProcessInvokeResult Run(string filePath, string[] arguments, int timeoutSeconds) {
+ var safeTimeoutSeconds = Math.Max(5, timeoutSeconds);
+ var result = new ProcessInvokeResult();
+
+ using var stdoutClosed = new ManualResetEventSlim(false);
+ using var stderrClosed = new ManualResetEventSlim(false);
+ var stdout = new StringBuilder();
+ var stderr = new StringBuilder();
+
+ var psi = new ProcessStartInfo {
+ FileName = filePath,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ if (arguments != null) {
+ foreach (var argument in arguments) {
+ psi.ArgumentList.Add(argument ?? string.Empty);
+ }
+ }
+
+ result.Command = psi.FileName + (psi.ArgumentList.Count > 0 ? " " + string.Join(" ", psi.ArgumentList) : string.Empty);
+
+ using var process = new Process {
+ StartInfo = psi,
+ EnableRaisingEvents = true
+ };
+
+ process.OutputDataReceived += (_, eventArgs) => {
+ if (eventArgs.Data == null) {
+ stdoutClosed.Set();
+ return;
+ }
+
+ lock (stdout) {
+ stdout.AppendLine(eventArgs.Data);
+ }
+ };
+
+ process.ErrorDataReceived += (_, eventArgs) => {
+ if (eventArgs.Data == null) {
+ stderrClosed.Set();
+ return;
+ }
+
+ lock (stderr) {
+ stderr.AppendLine(eventArgs.Data);
+ }
+ };
+
+ try {
+ if (!process.Start()) {
+ result.Exception = "Process failed to start.";
+ return result;
+ }
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ if (!process.WaitForExit(safeTimeoutSeconds * 1000)) {
+ result.TimedOut = true;
+ try {
+ process.Kill(true);
+ } catch {
+ }
+ }
+
+ try {
+ process.WaitForExit();
+ } catch {
+ }
+
+ stdoutClosed.Wait(2000);
+ stderrClosed.Wait(2000);
+
+ if (!result.TimedOut) {
+ result.ExitCode = process.ExitCode;
+ }
+ } catch (Exception ex) {
+ result.Exception = ex.Message;
+ try {
+ if (!process.HasExited) {
+ process.Kill(true);
+ }
+ } catch {
+ }
+ }
+
+ result.Stdout = SplitLines(stdout.ToString());
+ result.Stderr = SplitLines(stderr.ToString());
+ return result;
+ }
+ }
+}
+"@ -Language CSharp
+}
+
+function Invoke-ProcessWithTimeoutCore {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)][string]$FilePath,
+ [string[]]$Arguments = @(),
+ [int]$TimeoutSeconds = 45
+ )
+
+ return [CompareVI.ProcessInvokeHelper]::Run(
+ [string]$FilePath,
+ [string[]]@($Arguments),
+ [Math]::Max(5, [int]$TimeoutSeconds)
+ )
+}
diff --git a/tools/Render-VIHistoryReport.ps1 b/tools/Render-VIHistoryReport.ps1
index 3c7284a4f..6ddf973e9 100644
--- a/tools/Render-VIHistoryReport.ps1
+++ b/tools/Render-VIHistoryReport.ps1
@@ -952,12 +952,14 @@ function Build-FallbackHistoryContext {
}
$modeComparisons = New-Object System.Collections.Generic.List[object]
- foreach ($comparisonEntry in @($modeManifest.comparisons)) {
- if ($comparisonEntry) {
- $modeComparisons.Add($comparisonEntry) | Out-Null
- }
+ $comparisonEntries = @()
+ if ($modeManifest.PSObject.Properties['comparisons']) {
+ $comparisonEntries += @($modeManifest.comparisons)
+ }
+ if ($modeManifest.PSObject.Properties['collapsedComparisons']) {
+ $comparisonEntries += @($modeManifest.collapsedComparisons)
}
- foreach ($comparisonEntry in @($modeManifest.collapsedComparisons)) {
+ foreach ($comparisonEntry in @($comparisonEntries)) {
if ($comparisonEntry) {
$modeComparisons.Add($comparisonEntry) | Out-Null
}
diff --git a/tools/Test-WindowsNI2026q1HostPreflight.ps1 b/tools/Test-WindowsNI2026q1HostPreflight.ps1
index 81a863e47..4ed56c33b 100644
--- a/tools/Test-WindowsNI2026q1HostPreflight.ps1
+++ b/tools/Test-WindowsNI2026q1HostPreflight.ps1
@@ -39,6 +39,8 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
+. (Join-Path $PSScriptRoot 'ProcessTimeoutHelper.ps1')
+
function Resolve-AbsolutePath {
param([Parameter(Mandatory)][string]$Path)
return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
@@ -158,41 +160,13 @@ function Invoke-ProcessWithTimeout {
exception = ''
}
- $psi = [System.Diagnostics.ProcessStartInfo]::new()
- $psi.FileName = $resolvedFilePath
- $psi.UseShellExecute = $false
- $psi.RedirectStandardOutput = $true
- $psi.RedirectStandardError = $true
- $psi.CreateNoWindow = $true
- foreach ($arg in @($effectiveArguments)) {
- [void]$psi.ArgumentList.Add([string]$arg)
- }
-
- $proc = [System.Diagnostics.Process]::new()
- $proc.StartInfo = $psi
-
- try {
- [void]$proc.Start()
- $completed = $proc.WaitForExit($safeTimeout * 1000)
- if (-not $completed) {
- $result.timedOut = $true
- try { $proc.Kill($true) } catch {}
- return [pscustomobject]$result
- }
-
- $result.exitCode = [int]$proc.ExitCode
- $result.stdout = @(Split-OutputLines -Text $proc.StandardOutput.ReadToEnd())
- $result.stderr = @(Split-OutputLines -Text $proc.StandardError.ReadToEnd())
- } catch {
- $result.exception = [string]$_.Exception.Message
- try {
- if (-not $proc.HasExited) {
- $proc.Kill($true)
- }
- } catch {}
- } finally {
- $proc.Dispose()
- }
+ $invoke = Invoke-ProcessWithTimeoutCore -FilePath $resolvedFilePath -Arguments @($effectiveArguments) -TimeoutSeconds $safeTimeout
+ $result.timedOut = [bool]$invoke.TimedOut
+ $result.exitCode = $invoke.ExitCode
+ $result.stdout = @($invoke.Stdout | ForEach-Object { [string]$_ })
+ $result.stderr = @($invoke.Stderr | ForEach-Object { [string]$_ })
+ $result.command = [string]$invoke.Command
+ $result.exception = [string]$invoke.Exception
return [pscustomobject]$result
}