109 lines
4.1 KiB
PowerShell
109 lines
4.1 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RunDirectory,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern('^(T(0[1-9]|1[0-8])|Gate [A-D])$')]
|
|
[string]$ID,
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("PASS", "FAIL", "NOT_RUN")]
|
|
[string]$Status,
|
|
[string[]]$EvidencePath = @(),
|
|
[string]$Notes = "",
|
|
[string]$Operator = $env:USERNAME
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$runRoot = [System.IO.Path]::GetFullPath($RunDirectory).TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
|
|
$manifestPath = Join-Path $runRoot "acceptance-run.json"
|
|
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
|
throw "Acceptance manifest not found: $manifestPath"
|
|
}
|
|
$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json
|
|
if ($manifest.schema_version -ne 2) {
|
|
throw "Unsupported acceptance manifest schema version: $($manifest.schema_version)"
|
|
}
|
|
$entry = if ($ID.StartsWith("Gate ")) {
|
|
@($manifest.gates | Where-Object id -eq $ID)
|
|
} else {
|
|
@($manifest.scenarios | Where-Object id -eq $ID)
|
|
}
|
|
if ($entry.Count -ne 1) {
|
|
throw "Manifest must contain exactly one entry for $ID"
|
|
}
|
|
$entry = $entry[0]
|
|
|
|
$records = @()
|
|
foreach ($rawPath in $EvidencePath) {
|
|
$candidate = if ([System.IO.Path]::IsPathRooted($rawPath)) { $rawPath } else { Join-Path $runRoot $rawPath }
|
|
$fullPath = [System.IO.Path]::GetFullPath($candidate)
|
|
$rootPrefix = $runRoot + [System.IO.Path]::DirectorySeparatorChar
|
|
if (-not $fullPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "Evidence must be stored inside the acceptance run directory: $fullPath"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) {
|
|
throw "Evidence file not found: $fullPath"
|
|
}
|
|
if ($fullPath -eq $manifestPath) {
|
|
throw "The acceptance manifest cannot be used as its own evidence"
|
|
}
|
|
$item = Get-Item -LiteralPath $fullPath
|
|
$records += [ordered]@{
|
|
path = [System.IO.Path]::GetRelativePath($runRoot, $fullPath).Replace('\', '/')
|
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $fullPath).Hash.ToLowerInvariant()
|
|
bytes = $item.Length
|
|
}
|
|
}
|
|
if (($Status -eq "PASS" -or $Status -eq "FAIL") -and $records.Count -eq 0) {
|
|
throw "$Status requires at least one evidence file"
|
|
}
|
|
if ($Status -eq "NOT_RUN" -and $records.Count -ne 0) {
|
|
throw "NOT_RUN must not carry evidence; use PASS or FAIL after execution"
|
|
}
|
|
|
|
if ($Status -eq "PASS" -and $ID.StartsWith("Gate ")) {
|
|
$dependencies = switch ($ID) {
|
|
"Gate A" { @("T01", "T02") }
|
|
"Gate B" { @("Gate A") }
|
|
"Gate C" { @("Gate B", "T08", "T09") }
|
|
"Gate D" { @("Gate C", "T07", "T08", "T09") }
|
|
}
|
|
foreach ($dependency in $dependencies) {
|
|
$dependencyEntry = if ($dependency.StartsWith("Gate ")) {
|
|
@($manifest.gates | Where-Object id -eq $dependency)
|
|
} else {
|
|
@($manifest.scenarios | Where-Object id -eq $dependency)
|
|
}
|
|
if ($dependencyEntry.Count -ne 1 -or $dependencyEntry[0].status -ne "PASS") {
|
|
throw "$ID cannot be marked PASS until $dependency is PASS"
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($property in @("started_at", "completed_at", "operator", "notes")) {
|
|
if (-not $entry.PSObject.Properties[$property]) {
|
|
$entry | Add-Member -NotePropertyName $property -NotePropertyValue $null
|
|
}
|
|
}
|
|
$now = (Get-Date).ToUniversalTime().ToString("o")
|
|
$entry.status = $Status
|
|
$entry.evidence = $records
|
|
$entry.notes = $Notes
|
|
$entry.operator = $Operator
|
|
if ($Status -eq "NOT_RUN") {
|
|
$entry.started_at = $null
|
|
$entry.completed_at = $null
|
|
} else {
|
|
if (-not $entry.started_at) { $entry.started_at = $now }
|
|
$entry.completed_at = $now
|
|
}
|
|
|
|
$temporary = Join-Path $runRoot (".acceptance-run-" + [guid]::NewGuid().ToString("N") + ".tmp")
|
|
try {
|
|
$manifest | ConvertTo-Json -Depth 12 | Set-Content -Encoding utf8 -LiteralPath $temporary
|
|
[System.IO.File]::Move($temporary, $manifestPath, $true)
|
|
} finally {
|
|
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
|
}
|
|
Write-Host "Acceptance result recorded: $ID=$Status"
|