Files
cproudlock c890e5b46c test harness + Get-PCProfile: alias-aware lookups for rename reorg
Phase 5 + 6 of the gea-shopfloor-* rename.

Get-PCProfile.ps1: when the legacy profileKey ("Standard-Machine",
"CMM", etc.) is missing from siteConfig.pcProfiles, walks the alias
group and returns the first matching new key ("gea-shopfloor-collections",
"gea-shopfloor-cmm", etc.). Vice versa: a fleet PC writing the new
string finds its profile under the old key. Same alias map shape as
GE-Enforce + Install-FromManifest, kept in sync manually for now -
extract to shared file later if drift becomes a problem.

matrix.json: adds 3 new rows for gea-shopfloor-nocollections,
gea-shopfloor-common (Timeclock+Lab merge), gea-shopfloor-heattreat
(placeholder). Existing rows for legacy names retained; the new
verify-state alias resolution lets either be requested.

verify-state.ps1: Test-MatrixEntryMatches walks the alias map so
harness invocation with "Standard Machine" or "gea-shopfloor-collections"
both resolve to the same matrix row.

Smoke-tested via qga-as-SYSTEM on win11: legacy Standard/Machine,
new gea-shopfloor-collections, and new gea-shopfloor-nocollections
all return 10/10 pass against current VM state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 07:29:32 -04:00

160 lines
6.6 KiB
PowerShell

# verify-state.ps1 - VM-side detection runner. Reads the harness matrix.json
# from the path given via -MatrixPath, runs the verify block of each app under
# the requested -PCType / -PCSubType, prints per-app PASS / FAIL / WARN, and
# exits 0 only if every check passes.
#
# Detection methods supported:
# Registry -> Get-ItemProperty $path[$name] -eq $value
# File -> Test-Path $path
# FileVersion -> (Get-Item $path).VersionInfo.FileVersion -eq $value
# Hash -> Get-FileHash SHA256 -eq $value
# FileGrep -> Get-Content $path -match $pattern (regex)
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)] [string]$MatrixPath,
[Parameter(Mandatory=$true)] [string]$PCType,
[string]$PCSubType
)
$ErrorActionPreference = 'Continue'
if (-not (Test-Path -LiteralPath $MatrixPath)) {
Write-Host "[FAIL] matrix not found at $MatrixPath"
exit 1
}
$matrix = Get-Content -LiteralPath $MatrixPath -Raw | ConvertFrom-Json
# Alias resolution mirrors the GE-Enforce + Get-PCProfile alias maps so
# the harness can be invoked with either legacy ("Standard","CMM",...)
# or new ("gea-shopfloor-collections","gea-shopfloor-cmm",...) names
# and find the right matrix row.
$pcTypeAliasGroups = @(
@{ keys = @('Standard-Machine','gea-shopfloor-collections') },
@{ keys = @('Standard-Machine-NoCollections','gea-shopfloor-nocollections') },
@{ keys = @('Standard-Timeclock','Lab','gea-shopfloor-common') },
@{ keys = @('CMM','gea-shopfloor-cmm') },
@{ keys = @('Keyence','gea-shopfloor-keyence') },
@{ keys = @('WaxAndTrace','gea-shopfloor-waxtrace') },
@{ keys = @('Genspect','gea-shopfloor-genspect') },
@{ keys = @('Display','gea-shopfloor-display') },
@{ keys = @('Heattreat','gea-shopfloor-heattreat') },
@{ keys = @('Shopfloor') }
)
function Test-MatrixEntryMatches {
param($Entry, [string]$Type, [string]$SubType)
$entryKey = if ($Entry.PCSubType) { "$($Entry.PCType)-$($Entry.PCSubType)" } else { $Entry.PCType }
$requestKey = if ($SubType) { "$Type-$SubType" } else { $Type }
if ($entryKey -ieq $requestKey) { return $true }
foreach ($g in $pcTypeAliasGroups) {
if ($g.keys -icontains $entryKey -and $g.keys -icontains $requestKey) { return $true }
}
return $false
}
$entry = $matrix.pcTypes | Where-Object { Test-MatrixEntryMatches -Entry $_ -Type $PCType -SubType $PCSubType } | Select-Object -First 1
if (-not $entry) {
Write-Host "[FAIL] no matrix entry for PCType=$PCType PCSubType=$PCSubType"
exit 1
}
# Resolve `{ "$ref": "common.<key>" }` entries: matrix.json uses $ref to dedup
# common app lists (e.g. "common.all", "common.fmsResolver") so per-PC-type
# rows can compose without copy/paste. Walk the apps list, expand each ref
# inline, drop the ref placeholder.
$resolvedApps = @()
foreach ($a in $entry.apps) {
$refVal = $a.PSObject.Properties['$ref']
if ($refVal) {
$parts = $refVal.Value -split '\.'
if ($parts.Count -eq 2 -and $parts[0] -eq 'common') {
$key = $parts[1]
$list = $matrix.common.$key
if ($list) {
foreach ($x in $list) { $resolvedApps += $x }
continue
}
Write-Host "[WARN] unresolved `$ref: $($refVal.Value)"
continue
}
Write-Host "[WARN] unsupported `$ref form: $($refVal.Value)"
continue
}
$resolvedApps += $a
}
function Test-AppState {
param($app)
$v = $app.verify
switch ($v.method) {
'Registry' {
if (-not (Test-Path -LiteralPath $v.path)) { return @{ pass=$false; detail="reg path missing: $($v.path)" } }
# No DetectionName/value -> key existence is enough (e.g. Protect Viewer entry)
if (-not $v.name) {
return @{ pass=$true; detail="reg key exists: $($v.path)" }
}
$val = (Get-ItemProperty -LiteralPath $v.path -Name $v.name -ErrorAction SilentlyContinue).$($v.name)
if ($null -eq $val) { return @{ pass=$false; detail="reg name $($v.name) not present" } }
if ($v.value -and $val -ne $v.value) {
return @{ pass=$false; detail="reg $($v.name) = '$val' (expected '$($v.value)')" }
}
return @{ pass=$true; detail="reg $($v.name) = '$val'" }
}
'File' {
if (Test-Path -LiteralPath $v.path) { return @{ pass=$true; detail="exists: $($v.path)" } }
return @{ pass=$false; detail="missing: $($v.path)" }
}
'FileVersion' {
if (-not (Test-Path -LiteralPath $v.path)) { return @{ pass=$false; detail="missing: $($v.path)" } }
$ver = (Get-Item -LiteralPath $v.path).VersionInfo.FileVersion
if ($v.value -and $ver -ne $v.value) {
return @{ pass=$false; detail="version $ver (expected $($v.value))" }
}
return @{ pass=$true; detail="version $ver" }
}
'Hash' {
if (-not (Test-Path -LiteralPath $v.path)) { return @{ pass=$false; detail="missing: $($v.path)" } }
$h = (Get-FileHash -LiteralPath $v.path -Algorithm SHA256).Hash
if ($v.value -and $h -ne $v.value) {
return @{ pass=$false; detail="hash $h (expected $($v.value))" }
}
return @{ pass=$true; detail="hash matches" }
}
'FileGrep' {
if (-not (Test-Path -LiteralPath $v.path)) { return @{ pass=$false; detail="missing: $($v.path)" } }
$hit = Get-Content -LiteralPath $v.path | Select-String -Pattern $v.pattern -Quiet
if ($hit) { return @{ pass=$true; detail="pattern matched: $($v.pattern)" } }
return @{ pass=$false; detail="pattern not found: $($v.pattern)" }
}
'PnpUtilGrep' {
$drivers = & pnputil /enum-drivers 2>&1 | Out-String
if ($drivers -match $v.pattern) {
return @{ pass=$true; detail="pnputil match: $($v.pattern)" }
}
return @{ pass=$false; detail="pnputil no match for: $($v.pattern)" }
}
default { return @{ pass=$false; detail="unknown method: $($v.method)" } }
}
}
$total = 0; $passed = 0; $failed = @()
foreach ($app in $resolvedApps) {
$total++
$r = Test-AppState -app $app
if ($r.pass) {
Write-Host " [PASS] $($app.name) - $($r.detail)"
$passed++
} else {
Write-Host " [FAIL] $($app.name) - $($r.detail)"
$failed += $app.name
}
}
Write-Host ""
Write-Host "=== verify summary: $passed/$total passed ==="
if ($failed.Count -gt 0) {
Write-Host "failed: $($failed -join ', ')"
exit 1
}
exit 0