diff --git a/Tools/windows/Invoke-MatchedActionSupplement.ps1 b/Tools/windows/Invoke-MatchedActionSupplement.ps1 index 923a6285..9fbd902a 100644 --- a/Tools/windows/Invoke-MatchedActionSupplement.ps1 +++ b/Tools/windows/Invoke-MatchedActionSupplement.ps1 @@ -109,7 +109,7 @@ $journalBinding=New-CapJournalBinding -RunId ([IO.Path]::GetFileName($runDirecto -ModuleSha256 $moduleHash -WorkerSha256 $workerHash ` -SupervisorSha256 $supervisorHash -Identity $selfIdentity $journalState=New-CapJournalState -Root $root -Directory $journalPath ` - -Binding $journalBinding -DeadlineUtc $leaseWindow.ExpiresUtc.UtcDateTime + -Binding $journalBinding -DeadlineUtc $leaseWindow.ExpiresUtc Write-CapWorkerJournalSnapshot -Kind 'Bootstrap' $startedUtc=[DateTime]::UtcNow $primaryFailure=$null diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index c97a625e..991d4d9f 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -542,7 +542,35 @@ function Assert-CapRejects { $path = Join-Path $c.State.Directory $reference.name $null = Assert-CapRejects { Write-CapImmutableJson -Root $c.Root -Path $path -Value @{ invalid = $true } } 'SequenceConflict' Assert-CapTest ((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ceq $reference.sha256) 'Collision changed prior bytes.' - return @{ subcases = 1; priorBytesUnchanged = $true; retainedStagingFault = $true } + $newState = { + param([string]$Name, $Deadline) + $directory = Join-Path $c.Root $Name + $null = New-Item -ItemType Directory -Path $directory + New-CapJournalState -Root $c.Root -Directory $directory -Binding $c.Binding -DeadlineUtc $Deadline + } + $deadlineNow = [DateTime]::UtcNow + $lease = Get-CapLeaseWindow -GrantedUtc $deadlineNow.AddMinutes(-1).ToString('o') ` + -ExpiresUtc $deadlineNow.AddMinutes(10).ToString('o') -MaximumMinutes 15 ` + -NowUtc $deadlineNow + $dateTimeState = & $newState 'datetime' $lease.ExpiresUtc + $offsetDeadline = ([DateTimeOffset]$lease.ExpiresUtc).ToOffset([TimeSpan]::FromHours(7)) + $offsetState = & $newState 'offset' $offsetDeadline + Assert-CapTest ($dateTimeState.DeadlineUtc.Kind -eq [DateTimeKind]::Utc -and + $offsetState.DeadlineUtc.Kind -eq [DateTimeKind]::Utc -and + $offsetState.DeadlineUtc -eq $dateTimeState.DeadlineUtc) ` + 'Journal deadline boundary did not preserve DateTime and DateTimeOffset UTC instants.' + $null = Assert-CapRejects { & $newState 'null' $null } 'Journal deadline is required' + foreach ($kind in @([DateTimeKind]::Local,[DateTimeKind]::Unspecified)) { + $null = Assert-CapRejects { + & $newState "kind-$kind" ([DateTime]::SpecifyKind($lease.ExpiresUtc,$kind)) + } 'explicitly UTC' + } + $null = Assert-CapRejects { + & $newState 'expired' ([DateTime]::UtcNow.AddSeconds(-1)) + } 'JournalExpired' + return @{ subcases = 6; priorBytesUnchanged = $true; retainedStagingFault = $true + dateTimeUtc = $dateTimeState.DeadlineUtc; dateTimeOffsetUtc = $offsetState.DeadlineUtc + nullRefused = $true; nonUtcKindsRefused = 2; hardDeadlineRefused = $true } } Invoke-CapTest 'numbered journal reader retries only transient Open failures within count and deadline' { $progress = Invoke-JournalConcurrentContract -Name 'retry-to2' -LockedFirst -Publications 2 @@ -3026,6 +3054,8 @@ function Invoke-CapMatchedContracts { $actionInvokeSource.Contains('-DefinitionsOnly') -and $actionInvokeSource.Contains('$actionSupplementMode=$true') -and $actionInvokeSource.Contains('$journalState=New-CapJournalState') -and + $actionInvokeSource.Contains('-DeadlineUtc $leaseWindow.ExpiresUtc') -and + -not $actionInvokeSource.Contains('$leaseWindow.ExpiresUtc.UtcDateTime') -and $actionInvokeSource.Contains("Write-CapWorkerJournalSnapshot -Kind 'Bootstrap'") -and $actionInvokeSource.Contains("Write-CapWorkerJournalSnapshot -Kind 'Terminal'") -and $workerSource.Contains("'s-tabs-hover-close-action-result'") -and diff --git a/Tools/windows/UI-CAPTURE-SCENARIOS.md b/Tools/windows/UI-CAPTURE-SCENARIOS.md index 1d8cdce9..7ca6afa0 100644 --- a/Tools/windows/UI-CAPTURE-SCENARIOS.md +++ b/Tools/windows/UI-CAPTURE-SCENARIOS.md @@ -113,6 +113,11 @@ publication is written only after held cleanup and stream EOF collection, and the sanitized action manifest retains its binding, publication chain and terminal hash without changing the static-capture inventory. +Journal state creation normalizes an accepted `DateTimeOffset` to its UTC +instant and accepts `DateTime` only when its `Kind` is explicitly UTC. Null, +local/unspecified `DateTime`, foreign types and already-expired deadlines fail +before any numbered publication or launcher release. + The focused contract reproduces the C8R terminal scale with exactly 679 process entries and a numbered terminal larger than 1.48 MB, then injects more than one second of post-copy validation. It also retains explicit refusals for a copy diff --git a/Tools/windows/UiCaptureDriver.psm1 b/Tools/windows/UiCaptureDriver.psm1 index 06af5d6f..4000863d 100644 --- a/Tools/windows/UiCaptureDriver.psm1 +++ b/Tools/windows/UiCaptureDriver.psm1 @@ -5167,6 +5167,24 @@ function ConvertTo-CapUtcInstant { } } +function ConvertTo-CapUtcDateTime { + param( + [Parameter(Mandatory)][AllowNull()]$Value, + [Parameter(Mandatory)][string]$Field + ) + Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + + if ($null -eq $Value) { throw "$Field is required." } + if ($Value -is [DateTime]) { + if ($Value.Kind -ne [DateTimeKind]::Utc) { + throw "$Field must be an explicitly UTC DateTime." + } + return $Value + } + if ($Value -is [DateTimeOffset]) { return $Value.UtcDateTime } + throw "$Field must be a UTC DateTime or DateTimeOffset." +} + function Get-CapLeaseWindow { [CmdletBinding()] param( @@ -5322,8 +5340,12 @@ function Assert-CapJournalBinding { function New-CapJournalState { [CmdletBinding()] param([string]$Root, [string]$Directory, [Parameter(Mandatory)]$Binding, - [Parameter(Mandatory)][DateTime]$DeadlineUtc, [string]$ObservationPath = '') + [Parameter(Mandatory)][AllowNull()]$DeadlineUtc, [string]$ObservationPath = '') Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + $normalizedDeadline = ConvertTo-CapUtcDateTime -Value $DeadlineUtc -Field 'Journal deadline' + if ($normalizedDeadline -le [DateTime]::UtcNow) { + throw (New-CapJournalFailure 'JournalExpired' 'Journal deadline reached before state initialization.') + } $directoryPath = Assert-CapArtifactPath -Root $Root -Path $Directory if (-not ([IO.File]::GetAttributes($directoryPath) -band [IO.FileAttributes]::Directory)) { throw 'Journal requires an existing dedicated directory.' @@ -5334,7 +5356,7 @@ function New-CapJournalState { Assert-CapJournalBinding $Binding $Binding return [pscustomobject]@{ Root = [IO.Path]::GetFullPath($Root); Directory = $directoryPath; Binding = $Binding - DeadlineUtc = $DeadlineUtc; ObservationPath = $ObservationPath + DeadlineUtc = $normalizedDeadline; ObservationPath = $ObservationPath HighestObserved = 0L; LastApplied = 0L; Sequence = 0L; PreviousHash = ('0' * 64) Published = [Collections.Generic.List[object]]::new() Accepted = [Collections.Generic.Dictionary[long,string]]::new()