.*?
CodexRemote-fix
)') 'Chinese README uses the centered public product heading'
$quickStart = [regex]::Match($readme, '(?ms)^## Quick start[^\r\n]*\r?\n(.*?)(?=^## )').Groups[1].Value
- Assert-CcodTrue ($quickStart -cmatch 'CodexRemote-fix-2\.5\.9-setup\.exe') 'English Quick Start names the setup installer'
- Assert-CcodTrue ($quickStart -cmatch 'CodexRemote-fix-2\.5\.9-windows-x64\.zip') 'English Quick Start names the exact portable artifact'
+ Assert-CcodTrue ($quickStart -cmatch 'CodexRemote-fix-2\.5\.21-setup\.exe') 'English Quick Start names the setup installer'
+ Assert-CcodTrue ($quickStart -cmatch 'CodexRemote-fix-2\.5\.21-windows-x64\.zip') 'English Quick Start names the exact portable artifact'
Assert-CcodTrue ($quickStart -cmatch 'CodexRemote-fix\.exe') 'English Quick Start names the portable double-click entrypoint'
Assert-CcodTrue ($quickStart -cmatch '\.sha256\.txt') 'English Quick Start names the checksum artifact'
$quickStartChineseMatch = [regex]::Match($readmeChinese, '(?ms)^## [^\r\n]+\r?\n(?:\r?\n)?(?=1\.[^\r\n]*\[Releases\])(.*?)(?=^## |\z)')
Assert-CcodTrue $quickStartChineseMatch.Success 'Chinese README exposes a Quick Start section'
$quickStartChinese = $quickStartChineseMatch.Groups[1].Value
- Assert-CcodTrue ($quickStartChinese -cmatch 'CodexRemote-fix-2\.5\.9-setup\.exe') 'Chinese Quick Start names the setup installer'
- Assert-CcodTrue ($quickStartChinese -cmatch 'CodexRemote-fix-2\.5\.9-windows-x64\.zip') 'Chinese Quick Start names the exact portable artifact'
+ Assert-CcodTrue ($quickStartChinese -cmatch 'CodexRemote-fix-2\.5\.21-setup\.exe') 'Chinese Quick Start names the setup installer'
+ Assert-CcodTrue ($quickStartChinese -cmatch 'CodexRemote-fix-2\.5\.21-windows-x64\.zip') 'Chinese Quick Start names the exact portable artifact'
Assert-CcodTrue ($quickStartChinese -cmatch 'CodexRemote-fix\.exe') 'Chinese Quick Start names the portable double-click entrypoint'
Assert-CcodTrue ($quickStartChinese -cmatch '\.sha256\.txt') 'Chinese Quick Start names the checksum artifact'
diff --git a/tests/persistence/InstalledLifecycleHarness.SelfTest.ps1 b/tests/persistence/InstalledLifecycleHarness.SelfTest.ps1
index b3ae0dc..ef0e4ed 100644
--- a/tests/persistence/InstalledLifecycleHarness.SelfTest.ps1
+++ b/tests/persistence/InstalledLifecycleHarness.SelfTest.ps1
@@ -30,18 +30,36 @@ function New-CcodHarnessCandidate {
}
function New-CcodHarnessFacts {
- param([string]$Version = '2.5.0')
+ param(
+ [string]$Version = '2.5.0',
+ [string]$RuntimeId = 'runtime-1',
+ [int]$CodexPid = 102,
+ [string]$CodexCreationTimeUtc = '2026-08-24T00:00:02.0000000Z',
+ [string]$StatusRuntimeId = $RuntimeId,
+ [int]$StatusCodexPid = $CodexPid,
+ [string]$StatusCodexCreationTimeUtc = $CodexCreationTimeUtc,
+ [string]$ReceiptPhase = 'Completed'
+ )
return [pscustomobject][ordered]@{
appPresent = $true
- activeRuntimeId = 'runtime-1'
+ activeRuntimeId = $RuntimeId
activeGeneration = [UInt64]1
runtimeManifestSha256 = ('a' * 64)
supervisor = @([pscustomobject]@{ pid = 100; creationTimeUtc = '2026-08-24T00:00:00.0000000Z' })
trayHost = @([pscustomobject]@{ pid = 101; creationTimeUtc = '2026-08-24T00:00:01.0000000Z' })
- codex = @([pscustomobject]@{ pid = 102; creationTimeUtc = '2026-08-24T00:00:02.0000000Z' })
+ codex = @([pscustomobject]@{ pid = $CodexPid; creationTimeUtc = $CodexCreationTimeUtc })
taskState = 'Ready'
statusPhase = 'Active'
- transitionStage = 'Completed'
+ statusRuntimeId = $StatusRuntimeId
+ statusCodex = [pscustomobject][ordered]@{ pid = $StatusCodexPid; creationTimeUtc = $StatusCodexCreationTimeUtc }
+ transitionStage = 'Idle'
+ lifecycleReceipt = [pscustomobject][ordered]@{
+ kind = 'RestartAndRepair'
+ origin = 'Installer'
+ runtimeId = $RuntimeId
+ runtimeGeneration = [UInt64]1
+ phase = $ReceiptPhase
+ }
aboutVersion = $Version
deviceKeyPresent = $true
deviceKeySha256 = ('b' * 64)
@@ -53,6 +71,139 @@ function New-CcodHarnessFacts {
}
}
+function Write-CcodHarnessJson {
+ param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)]$Value)
+ $null = [IO.Directory]::CreateDirectory((Split-Path $Path -Parent))
+ [IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 16 -Compress), [Text.UTF8Encoding]::new($false))
+}
+
+function New-CcodHarnessLifecycleReceipt {
+ param(
+ [Parameter(Mandatory)][string]$RuntimeId,
+ [Parameter(Mandatory)][UInt64]$RuntimeGeneration,
+ [Parameter(Mandatory)][string]$Phase,
+ [string]$TransactionId = '11111111-2222-3333-4444-555555555555',
+ [string]$UpdatedAtUtc = '2026-08-24T00:00:05.0000000Z'
+ )
+ return [ordered]@{
+ schemaVersion = 1
+ transactionId = $TransactionId
+ kind = 'RestartAndRepair'
+ origin = 'Installer'
+ runtimeId = $RuntimeId
+ runtimeGeneration = $RuntimeGeneration
+ leaseEpoch = [UInt64]9
+ ownerIdentity = [ordered]@{ pid = 700; creationTimeUtc = '2026-08-24T00:00:00.0000000Z' }
+ logonIdentity = [ordered]@{ authenticationId = '00000000:000003E7'; userSid = 'S-1-5-21-1-2-3-1001'; sessionId = 2 }
+ phase = $Phase
+ createdAtUtc = '2026-08-24T00:00:01.0000000Z'
+ updatedAtUtc = $UpdatedAtUtc
+ launchRequestedAtUtc = '2026-08-24T00:00:02.0000000Z'
+ manualLaunchExpiresAtUtc = $null
+ automaticLaunchAttempts = 1
+ error = if ($Phase -ceq 'Completed') { $null } else { 'CCOD_CLOSE_FAILED' }
+ }
+}
+
+function New-CcodHarnessInstalledStateFixture {
+ param(
+ [Parameter(Mandatory)][string]$Root,
+ [Parameter(Mandatory)][string]$ActiveRuntimeId,
+ [Parameter(Mandatory)][UInt64]$ActiveGeneration,
+ [Parameter(Mandatory)][string]$StatusRuntimeId,
+ [Parameter(Mandatory)][int]$StatusCodexPid,
+ [Parameter(Mandatory)][string]$StatusCodexCreationTimeUtc,
+ [Parameter(Mandatory)][string]$ReceiptPhase
+ )
+ $active = [ordered]@{
+ schemaVersion = 2
+ activeRuntime = $ActiveRuntimeId
+ previousRuntime = '2.5.19-old'
+ generation = $ActiveGeneration
+ updatedAtUtc = '2026-08-24T00:00:03.0000000Z'
+ }
+ $status = [ordered]@{
+ schemaVersion = 1
+ session = [ordered]@{
+ supervisorPid = 700
+ supervisorCreationTimeUtc = '2026-08-24T00:00:00.0000000Z'
+ sessionId = 'session-1'
+ runtimeId = $StatusRuntimeId
+ sessionState = 'Active'
+ codex = [ordered]@{
+ pid = $StatusCodexPid
+ creationTimeUtc = $StatusCodexCreationTimeUtc
+ packageFullName = 'OpenAI.Codex_1.0.0.0_x64__test'
+ packageVersion = '1.0.0.0'
+ appAsarSha256 = ('c' * 64)
+ mainPort = 41001
+ rendererPort = 41002
+ mainProbe = 'Closed'
+ rendererProbe = 'BridgeValid'
+ }
+ }
+ }
+ $transition = [ordered]@{ schemaVersion = 1; activeTransaction = $null }
+ $receipt = New-CcodHarnessLifecycleReceipt -RuntimeId $ActiveRuntimeId -RuntimeGeneration $ActiveGeneration -Phase $ReceiptPhase
+ Write-CcodHarnessJson -Path (Join-Path $Root 'active.json') -Value $active
+ $manifestPath = Join-Path (Join-Path (Join-Path $Root 'runtime') $ActiveRuntimeId) 'manifest.json'
+ Write-CcodHarnessJson -Path $manifestPath -Value ([ordered]@{ schemaVersion = 1; runtimeId = $ActiveRuntimeId })
+ Write-CcodHarnessJson -Path (Join-Path $Root 'state\status.json') -Value $status
+ Write-CcodHarnessJson -Path (Join-Path $Root 'state\transition.json') -Value $transition
+ Write-CcodHarnessJson -Path (Join-Path $Root ("state\lifecycle\receipts\{0}.json" -f $receipt.transactionId)) -Value $receipt
+}
+
+function Set-CcodHarnessProcessFixture {
+ param([object[]]$ChatGPT, [object[]]$Codex = @())
+ $script:CcodHarnessProcessFixture = [pscustomobject]@{ ChatGPT = @($ChatGPT); Codex = @($Codex) }
+ function global:Get-CimInstance {
+ param($ClassName, $Filter, $ErrorAction)
+ if ([string]$Filter -cmatch "Name = 'ChatGPT.exe'") { return @($script:CcodHarnessProcessFixture.ChatGPT) }
+ if ([string]$Filter -cmatch "Name = 'Codex.exe'") { return @($script:CcodHarnessProcessFixture.Codex) }
+ return @()
+ }
+ function global:Get-ScheduledTask {
+ param($ErrorAction)
+ return @([pscustomobject]@{ TaskName = 'Codex Control Other Devices Supervisor'; State = 'Ready' })
+ }
+}
+
+function Clear-CcodHarnessProcessFixture {
+ Remove-Item -LiteralPath Function:\Get-CimInstance -Force -ErrorAction SilentlyContinue
+ Remove-Item -LiteralPath Function:\Get-ScheduledTask -Force -ErrorAction SilentlyContinue
+ $script:CcodHarnessProcessFixture = $null
+}
+
+function New-CcodHarnessCimProcess {
+ param(
+ [Parameter(Mandatory)][string]$Name,
+ [Parameter(Mandatory)][int]$ProcessId,
+ [Parameter(Mandatory)][string]$CreationTimeUtc,
+ [Parameter(Mandatory)][AllowNull()][AllowEmptyString()][string]$CommandLine,
+ [int]$ParentProcessId = 0
+ )
+ $created = [datetime]::ParseExact($CreationTimeUtc, 'o', [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind)
+ return [pscustomobject][ordered]@{
+ Name = $Name
+ ProcessId = $ProcessId
+ ParentProcessId = $ParentProcessId
+ CreationDate = [Management.ManagementDateTimeConverter]::ToDmtfDateTime($created)
+ CommandLine = $CommandLine
+ ExecutablePath = "C:\Program Files\WindowsApps\OpenAI.Codex\$Name"
+ }
+}
+
+function Invoke-CcodHarnessWithCapturedFacts {
+ param([Parameter(Mandatory)]$Context, [Parameter(Mandatory)]$BeforeFacts, [Parameter(Mandatory)]$AfterFacts)
+ $original = ${function:Get-CcodInstalledLifecycleFacts}
+ try {
+ Set-Item -LiteralPath Function:\Get-CcodInstalledLifecycleFacts -Value ({ param($InstallRoot) return $AfterFacts }.GetNewClosure())
+ return Test-CcodInstalledLifecycleScenario -Context $Context -BeforeFacts $BeforeFacts -RunResult ([pscustomobject]@{ code = 'CCOD_INTEGRATION_OPERATOR_COMPLETED' })
+ } finally {
+ Set-Item -LiteralPath Function:\Get-CcodInstalledLifecycleFacts -Value $original
+ }
+}
+
function New-CcodHarnessAdapters {
param(
[Parameter(Mandatory)]$Fixture,
@@ -137,6 +288,349 @@ Invoke-CcodTest 'installed lifecycle harness exposes the guarded library interfa
Assert-CcodTrue ($null -ne (Get-Command Invoke-CcodInstalledLifecycleIntegration -ErrorAction SilentlyContinue)) 'harness exports its invocation function when loaded as a library'
}
+# Production mutation caught: FreshRestart ignores stale status and a current-runtime CloseFailed receipt.
+Invoke-CcodTest 'rejects FreshRestart when current active runtime has stale status and a CloseFailed installer receipt' {
+ . $harnessPath -Library
+ $before = New-CcodHarnessFacts -RuntimeId '2.5.19-old' -CodexPid 10664
+ $after = New-CcodHarnessFacts -RuntimeId '2.5.21-new' -CodexPid 13948 `
+ -StatusRuntimeId '2.5.19-old' -StatusCodexPid 10664 -ReceiptPhase 'CloseFailed'
+ $context = [pscustomobject]@{ scenario = 'FreshRestart'; expectedVersion = '2.5.0'; installRoot = 'C:\fixture' }
+ $verification = Invoke-CcodHarnessWithCapturedFacts -Context $context -BeforeFacts $before -AfterFacts $after
+ Assert-CcodEqual $false $verification.verified 'stale status and CloseFailed terminal state leave FreshRestart unverified'
+ Assert-CcodEqual 'CCOD_INTEGRATION_OBSERVATION_UNPROVEN' $verification.code 'rejection uses the stable observation code'
+}
+
+# Production mutation caught: querying Codex.exe instead of the Windows app root ChatGPT.exe.
+Invoke-CcodTest 'captures exactly the top-level ChatGPT root and excludes Electron type children' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-capture-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ $created = '2026-08-24T00:00:04.0000000Z'
+ Set-CcodHarnessProcessFixture -ChatGPT @(
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc $created -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"'),
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13949 -CreationTimeUtc '2026-08-24T00:00:04.1000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" --type=renderer' -ParentProcessId 13948),
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13950 -CreationTimeUtc '2026-08-24T00:00:04.2000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" --TYPE=utility' -ParentProcessId 13948),
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13953 -CreationTimeUtc '2026-08-24T00:00:04.5000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" "--type=renderer"' -ParentProcessId 13948)
+ )
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 1 $facts.codex.Count 'only one ordinary ChatGPT root is retained'
+ Assert-CcodEqual 13948 $facts.codex[0].pid 'the ordinary ChatGPT root identity is captured'
+ Assert-CcodEqual $created $facts.codex[0].creationTimeUtc 'the root creation time is canonical and retained'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'normalizes only supported CIM creation-time evidence for complete ChatGPT roots' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-cim-creation-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ $commandLine = '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"'
+ $local = [DateTime]::SpecifyKind([DateTime]'2030-02-03T12:05:06', [DateTimeKind]::Local)
+ $utc = [DateTime]::SpecifyKind([DateTime]'2030-02-03T12:05:06', [DateTimeKind]::Utc)
+ $dmtf = [Management.ManagementDateTimeConverter]::ToDmtfDateTime($utc)
+ $successCases = @(
+ [pscustomobject]@{ Name = 'Local DateTime'; Value = $local; Expected = $local.ToUniversalTime().ToString('o', [Globalization.CultureInfo]::InvariantCulture) },
+ [pscustomobject]@{ Name = 'Utc DateTime'; Value = $utc; Expected = $utc.ToUniversalTime().ToString('o', [Globalization.CultureInfo]::InvariantCulture) },
+ [pscustomobject]@{ Name = 'complete DMTF string'; Value = $dmtf; Expected = $utc.ToUniversalTime().ToString('o', [Globalization.CultureInfo]::InvariantCulture) }
+ )
+ foreach ($case in $successCases) {
+ $process = New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine $commandLine
+ $process.CreationDate = $case.Value
+ Set-CcodHarnessProcessFixture -ChatGPT @($process)
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 1 $facts.codex.Count ("{0} keeps one root" -f $case.Name)
+ Assert-CcodEqual 13948 $facts.codex[0].pid ("{0} retains the root PID" -f $case.Name)
+ Assert-CcodEqual $case.Expected $facts.codex[0].creationTimeUtc ("{0} becomes canonical UTC" -f $case.Name)
+ }
+ foreach ($case in @(
+ [pscustomobject]@{ Name = 'null'; Value = $null },
+ [pscustomobject]@{ Name = 'Unspecified DateTime'; Value = [DateTime]::SpecifyKind([DateTime]'2030-02-03T12:05:06', [DateTimeKind]::Unspecified) },
+ [pscustomobject]@{ Name = 'malformed DMTF'; Value = '20300203120506.000000+08' },
+ [pscustomobject]@{ Name = 'lexically complete invalid DMTF'; Value = '20301303120506.000000+000' },
+ [pscustomobject]@{ Name = 'integer'; Value = 1 },
+ [pscustomobject]@{ Name = 'arbitrary string'; Value = 'not-a-dmtf-time' }
+ )) {
+ $process = New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine $commandLine
+ $process.CreationDate = $case.Value
+ Set-CcodHarnessProcessFixture -ChatGPT @($process)
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_INTEGRATION_FACTS_INVALID'
+ }
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'fails closed when any enumerated ChatGPT process cannot be proven root or Electron child' {
+ . $harnessPath -Library
+ foreach($case in @('null command line','empty command line','unparsable command line','invalid creation time','zero PID','string PID','nonstring command line','missing command line')){
+ $root=Join-Path $env:TEMP ("ccod-installed-indeterminate-$($case.Replace(' ','-'))-"+[guid]::NewGuid().ToString('N'))
+ try{
+ $null=[IO.Directory]::CreateDirectory($root)
+ $valid=New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"'
+ $indeterminate=New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13949 -CreationTimeUtc '2026-08-24T00:00:04.1000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" --type=renderer' -ParentProcessId 13948
+ switch($case){
+ 'null command line'{$indeterminate.CommandLine=$null}
+ 'empty command line'{$indeterminate.CommandLine=''}
+ 'unparsable command line'{$indeterminate.CommandLine=' '}
+ 'invalid creation time'{$indeterminate.CreationDate='not-a-dmtf-time'}
+ 'zero PID'{$indeterminate.ProcessId=0}
+ 'string PID'{$indeterminate.ProcessId='13949'}
+ 'nonstring command line'{$indeterminate.CommandLine=42}
+ 'missing command line'{$indeterminate.PSObject.Properties.Remove('CommandLine')}
+ }
+ Set-CcodHarnessProcessFixture -ChatGPT @($valid,$indeterminate)
+ Assert-CcodThrows {Get-CcodInstalledLifecycleFacts -InstallRoot $root} 'CCOD_INTEGRATION_FACTS_INVALID'
+ }finally{
+ Clear-CcodHarnessProcessFixture
+ if(Test-Path -LiteralPath $root){Remove-Item -LiteralPath $root -Recurse -Force}
+ }
+ }
+}
+
+Invoke-CcodTest 'captures bounded current-runtime status transition and latest installer receipt facts from complete schemas' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-state-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.19-old' -StatusCodexPid 10664 -StatusCodexCreationTimeUtc '2026-08-24T00:00:02.0000000Z' -ReceiptPhase 'CloseFailed'
+ Set-CcodHarnessProcessFixture -ChatGPT @(
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"'),
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13949 -CreationTimeUtc '2026-08-24T00:00:04.1000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" --type=renderer' -ParentProcessId 13948)
+ )
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual '2.5.21-new' $facts.activeRuntimeId 'active schema 2 supplies the current runtime'
+ Assert-CcodEqual 8 ([UInt64]$facts.activeGeneration) 'active schema 2 supplies the current generation'
+ Assert-CcodEqual 'Active' $facts.statusPhase 'complete status schema 1 supplies the session state'
+ Assert-CcodEqual '2.5.19-old' $facts.statusRuntimeId 'stale status runtime remains visible for correlation rejection'
+ Assert-CcodEqual 10664 $facts.statusCodex.pid 'stale status Codex identity remains visible for correlation rejection'
+ Assert-CcodEqual 'Idle' $facts.transitionStage 'null active transaction is captured as idle'
+ Assert-CcodEqual 'RestartAndRepair' $facts.lifecycleReceipt.kind 'receipt kind is bounded and retained'
+ Assert-CcodEqual 'Installer' $facts.lifecycleReceipt.origin 'receipt origin is bounded and retained'
+ Assert-CcodEqual '2.5.21-new' $facts.lifecycleReceipt.runtimeId 'latest receipt is bound to the active runtime'
+ Assert-CcodEqual 'CloseFailed' $facts.lifecycleReceipt.phase 'terminal receipt failure remains visible'
+ $serialized = $facts | ConvertTo-Json -Depth 16 -Compress
+ foreach ($forbidden in @('00000000:000003E7','S-1-5-21-1-2-3-1001','OpenAI.Codex_1.0.0.0_x64__test','41001','--type=renderer','C:\Program Files\WindowsApps')) {
+ Assert-CcodTrue (-not $serialized.Contains($forbidden)) "captured facts omit private receipt status and command-line data: $forbidden"
+ }
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'selects only an unambiguous latest current-runtime installer restart receipt' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-receipts-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'CloseFailed'
+ $completed = New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]8) -Phase 'Completed' `
+ -TransactionId '22222222-3333-4444-5555-666666666666' -UpdatedAtUtc '2026-08-24T00:00:06.0000000Z'
+ Write-CcodHarnessJson -Path (Join-Path $root ("state\lifecycle\receipts\{0}.json" -f $completed.transactionId)) -Value $completed
+ Set-CcodHarnessProcessFixture -ChatGPT @(
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"')
+ )
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 'Completed' $facts.lifecycleReceipt.phase 'strict receipt selection uses the unique latest updatedAtUtc'
+
+ $ambiguous = New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]8) -Phase 'CloseFailed' `
+ -TransactionId '33333333-4444-5555-6666-777777777777' -UpdatedAtUtc '2026-08-24T00:00:06.0000000Z'
+ Write-CcodHarnessJson -Path (Join-Path $root ("state\lifecycle\receipts\{0}.json" -f $ambiguous.transactionId)) -Value $ambiguous
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_INTEGRATION_FACTS_INVALID'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+# Production mutation caught: choosing latest by runtime before filtering the active generation.
+Invoke-CcodTest 'ignores a newer wrong-generation receipt when selecting the current-generation terminal result' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-receipt-generation-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'Completed'
+ $wrongGeneration = New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]7) -Phase 'CloseFailed' `
+ -TransactionId '44444444-5555-6666-7777-888888888888' -UpdatedAtUtc '2026-08-24T00:00:07.0000000Z'
+ Write-CcodHarnessJson -Path (Join-Path $root ("state\lifecycle\receipts\{0}.json" -f $wrongGeneration.transactionId)) -Value $wrongGeneration
+ Set-CcodHarnessProcessFixture -ChatGPT @()
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 8 ([UInt64]$facts.lifecycleReceipt.runtimeGeneration) 'receipt selection stays bound to the active generation'
+ Assert-CcodEqual 'Completed' $facts.lifecycleReceipt.phase 'newer stale-generation failure cannot mask current-generation completion'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'does not treat tied wrong-generation receipts as current-generation ambiguity' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-receipt-generation-tie-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'Completed'
+ foreach ($record in @(
+ (New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]7) -Phase 'CloseFailed' -TransactionId '55555555-6666-7777-8888-999999999999' -UpdatedAtUtc '2026-08-24T00:00:07.0000000Z'),
+ (New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]7) -Phase 'RepairFailed' -TransactionId '66666666-7777-8888-9999-aaaaaaaaaaaa' -UpdatedAtUtc '2026-08-24T00:00:07.0000000Z')
+ )) {
+ Write-CcodHarnessJson -Path (Join-Path $root ("state\lifecycle\receipts\{0}.json" -f $record.transactionId)) -Value $record
+ }
+ Set-CcodHarnessProcessFixture -ChatGPT @()
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 'Completed' $facts.lifecycleReceipt.phase 'only current-generation timestamps participate in ambiguity detection'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'fails closed on receipt filename duplicate-property and nonterminal boundaries' {
+ . $harnessPath -Library
+ foreach ($case in @('filename','duplicate-json','nonterminal')) {
+ $root = Join-Path $env:TEMP ("ccod-installed-receipt-$case-" + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'Completed'
+ switch ($case) {
+ 'filename' {
+ $receipt = New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]8) -Phase 'Completed' `
+ -TransactionId '77777777-8888-9999-aaaa-bbbbbbbbbbbb'
+ Write-CcodHarnessJson -Path (Join-Path $root 'state\lifecycle\receipts\88888888-9999-aaaa-bbbb-cccccccccccc.json') -Value $receipt
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_INTEGRATION_FACTS_INVALID'
+ }
+ 'duplicate-json' {
+ $path = Join-Path $root 'state\lifecycle\receipts\11111111-2222-3333-4444-555555555555.json'
+ $json = [IO.File]::ReadAllText($path).Replace('"schemaVersion":1', '"schemaVersion":1,"schemaVersion":1')
+ [IO.File]::WriteAllText($path, $json, [Text.UTF8Encoding]::new($false))
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_STATE_MALFORMED'
+ }
+ 'nonterminal' {
+ $receipt = New-CcodHarnessLifecycleReceipt -RuntimeId '2.5.21-new' -RuntimeGeneration ([UInt64]8) -Phase 'Completed' `
+ -TransactionId '99999999-aaaa-bbbb-cccc-dddddddddddd'
+ $receipt.phase = 'Requested'
+ Write-CcodHarnessJson -Path (Join-Path $root ("state\lifecycle\receipts\{0}.json" -f $receipt.transactionId)) -Value $receipt
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_INTEGRATION_FACTS_INVALID'
+ }
+ }
+ } finally {
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+ }
+}
+
+Invoke-CcodTest 'fails closed on malformed active status and transition schemas' {
+ . $harnessPath -Library
+ foreach ($case in @('active','status','transition')) {
+ $root = Join-Path $env:TEMP ("ccod-installed-state-$case-" + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'Completed'
+ switch ($case) {
+ 'active' {
+ $path = Join-Path $root 'active.json'
+ $value = [IO.File]::ReadAllText($path) | ConvertFrom-Json
+ $value.generation = 'eight'
+ Write-CcodHarnessJson -Path $path -Value $value
+ }
+ 'status' {
+ $path = Join-Path $root 'state\status.json'
+ $value = [IO.File]::ReadAllText($path) | ConvertFrom-Json
+ $value.session.codex.rendererProbe = 'Open'
+ Write-CcodHarnessJson -Path $path -Value $value
+ }
+ 'transition' {
+ $path = Join-Path $root 'state\transition.json'
+ $value = [IO.File]::ReadAllText($path) | ConvertFrom-Json
+ $value.activeTransaction = [pscustomobject][ordered]@{}
+ Write-CcodHarnessJson -Path $path -Value $value
+ }
+ }
+ Assert-CcodThrows { Get-CcodInstalledLifecycleFacts -InstallRoot $root } 'CCOD_INTEGRATION_FACTS_INVALID'
+ } finally {
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+ }
+}
+
+# Production mutation caught: treating a Codex.exe-only observation as installed app process evidence.
+Invoke-CcodTest 'does not accept a Codex.exe-only fixture as Codex process evidence' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-capture-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ Set-CcodHarnessProcessFixture -ChatGPT @() -Codex @(
+ (New-CcodHarnessCimProcess -Name 'Codex.exe' -ProcessId 10664 -CreationTimeUtc '2026-08-24T00:00:02.0000000Z' -CommandLine '"C:\Program Files\Codex.exe"')
+ )
+ $facts = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ Assert-CcodEqual 0 $facts.codex.Count 'Codex.exe is not the Windows app root and cannot satisfy process evidence'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'verifies FreshRestart only with one correlated ChatGPT root and Completed current-runtime installer receipt' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-success-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid 13948 -StatusCodexCreationTimeUtc '2026-08-24T00:00:04.0000000Z' -ReceiptPhase 'Completed'
+ Set-CcodHarnessProcessFixture -ChatGPT @(
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13948 -CreationTimeUtc '2026-08-24T00:00:04.0000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"'),
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId 13949 -CreationTimeUtc '2026-08-24T00:00:04.1000000Z' -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe" --type=renderer' -ParentProcessId 13948)
+ )
+ $after = Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ $after.appPresent = $true
+ $after.aboutVersion = '2.5.0'
+ $after.deviceKeyPresent = $true
+ $after.deviceKeySha256 = ('b' * 64)
+ $before = New-CcodHarnessFacts -RuntimeId '2.5.19-old' -CodexPid 10664
+ $context = [pscustomobject]@{ scenario = 'FreshRestart'; expectedVersion = '2.5.0'; installRoot = $root }
+ $verification = Invoke-CcodHarnessWithCapturedFacts -Context $context -BeforeFacts $before -AfterFacts $after
+ Assert-CcodEqual $true $verification.verified 'fully correlated current runtime verifies FreshRestart'
+ Assert-CcodEqual 'CCOD_INTEGRATION_VERIFIED' $verification.code 'success retains the stable verification code'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
+Invoke-CcodTest 'rejects FreshRestart when the fully correlated after root is unchanged from before' {
+ . $harnessPath -Library
+ $root = Join-Path $env:TEMP ('ccod-installed-unchanged-root-' + [guid]::NewGuid().ToString('N'))
+ try {
+ $null = [IO.Directory]::CreateDirectory($root)
+ $unchangedPid=13948;$unchangedCreation='2026-08-24T00:00:04.0000000Z'
+ New-CcodHarnessInstalledStateFixture -Root $root -ActiveRuntimeId '2.5.21-new' -ActiveGeneration ([UInt64]8) `
+ -StatusRuntimeId '2.5.21-new' -StatusCodexPid $unchangedPid -StatusCodexCreationTimeUtc $unchangedCreation -ReceiptPhase 'Completed'
+ Set-CcodHarnessProcessFixture -ChatGPT @(
+ (New-CcodHarnessCimProcess -Name 'ChatGPT.exe' -ProcessId $unchangedPid -CreationTimeUtc $unchangedCreation -CommandLine '"C:\Program Files\WindowsApps\OpenAI.Codex\ChatGPT.exe"')
+ )
+ $after=Get-CcodInstalledLifecycleFacts -InstallRoot $root
+ $after.appPresent=$true;$after.aboutVersion='2.5.0';$after.deviceKeyPresent=$true;$after.deviceKeySha256=('b'*64)
+ $before=New-CcodHarnessFacts -RuntimeId '2.5.19-old' -CodexPid $unchangedPid -CodexCreationTimeUtc $unchangedCreation
+ $context=[pscustomobject]@{scenario='FreshRestart';expectedVersion='2.5.0';installRoot=$root}
+ $verification=Invoke-CcodHarnessWithCapturedFacts -Context $context -BeforeFacts $before -AfterFacts $after
+ Assert-CcodEqual $false $verification.verified 'FreshRestart requires the root identity to change even when status and receipt fully correlate'
+ Assert-CcodEqual 'CCOD_INTEGRATION_OBSERVATION_UNPROVEN' $verification.code 'unchanged root rejection uses the stable observation code'
+ } finally {
+ Clear-CcodHarnessProcessFixture
+ if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
+ }
+}
+
Invoke-CcodTest 'requires mutation and Codex-restart consent before any adapter or filesystem action' {
. $harnessPath -Library
$fixture = New-CcodHarnessFixture
diff --git a/tests/persistence/LifecycleCoordinator.SelfTest.ps1 b/tests/persistence/LifecycleCoordinator.SelfTest.ps1
index 17a4f16..afc6dd7 100644
--- a/tests/persistence/LifecycleCoordinator.SelfTest.ps1
+++ b/tests/persistence/LifecycleCoordinator.SelfTest.ps1
@@ -59,9 +59,21 @@ $results += Invoke-CcodTest 'phase table selects one exact lifecycle operation'
}
}
+$results += Invoke-CcodTest 'a recovered close request cannot skip a still-running ordinary root' {
+ $request = New-CcodCoordinatorFixture -Phase CloseRequested -Kind RestartAndRepair
+ $step = Get-CcodLifecycleStep -Request $request -Observation Ordinary -NowUtc '2030-02-03T04:06:00.0000000Z'
+ Assert-CcodCoordinatorStep $step Operation $null Close $null $null 'recovered close request with ordinary root'
+}
+
$results += Invoke-CcodTest 'repair is idempotent and safe exit never selects Apply' {
$connected = Get-CcodLifecycleStep -Request (New-CcodCoordinatorFixture -Kind CheckAndRepair) -Observation RemoteVerified -NowUtc '2030-02-03T04:06:00.0000000Z'
Assert-CcodCoordinatorStep $connected Completed CancelledBeforeClose None $null $null 'already connected repair'
+ $inspectRequest=New-CcodCoordinatorFixture -Kind CheckAndRepair
+ $inspect=Get-CcodLifecycleStep -Request $inspectRequest -Observation Special -NowUtc '2030-02-03T04:06:00.0000000Z'
+ Assert-CcodCoordinatorStep $inspect Operation $null Inspect $null $null 'unproven persisted special inspection'
+ $inspectResult=[pscustomobject][ordered]@{schemaVersion=1;transactionId=$inspectRequest.transactionId;action='Inspect';ok=$true;outcome='Inspected';observation='Special';error=$null}
+ $repair=Reduce-CcodLifecycleWorkerResult -Request $inspectRequest -Result $inspectResult -NowUtc '2030-02-03T04:06:01.0000000Z'
+ Assert-CcodEqual CloseRequested $repair.phase 'renderer-invalid strong Inspect advances to the normal repair close boundary'
foreach ($observation in @('Ordinary','NoCodex')) {
$safe = Get-CcodLifecycleStep -Request (New-CcodCoordinatorFixture -Kind SafeExit) -Observation $observation -NowUtc '2030-02-03T04:06:00.0000000Z'
Assert-CcodCoordinatorStep $safe Completed CancelledBeforeClose None $null $null "safe exit $observation"
diff --git a/tests/persistence/LifecycleWorker.SelfTest.ps1 b/tests/persistence/LifecycleWorker.SelfTest.ps1
index 9ac5bca..39166ee 100644
--- a/tests/persistence/LifecycleWorker.SelfTest.ps1
+++ b/tests/persistence/LifecycleWorker.SelfTest.ps1
@@ -55,6 +55,71 @@ function Get-CcodWorkerErrorCode($Run){ if($null -eq $Run.Result.error){return $
$results=@();$roots=[Collections.Generic.List[string]]::new()
try {
+ $results += Invoke-CcodTest 'default worker adapter reads a strict JSON request in the production script scope' {
+ $h=New-CcodLifecycleWorkerHarness -Name production-adapter
+ $roots.Add($h.Install)
+ $adapters=Get-CcodLifecycleWorkerAdapters $null
+ $parsed=& $adapters.ReadRequest $h.RequestPath
+ Assert-CcodEqual $h.Request.transactionId $parsed.transactionId 'production ReadRequest preserves request correlation'
+ Assert-CcodEqual $h.Request.action $parsed.action 'production ReadRequest preserves the worker action'
+ }
+
+ $results += Invoke-CcodTest 'default ordinary observation supplies an explicit empty status instead of null' {
+ $binding=$script:CcodLifecycleWorkerModuleBindings['ProcessControl'];$original=$binding['Wait-CcodVerifiedOrdinaryRoot']
+ $capture=[pscustomobject]@{Status=$null}
+ try{
+ $binding['Wait-CcodVerifiedOrdinaryRoot']={param($NotBefore,$Sid,$SessionId,$Status,$Timeout)$capture.Status=$Status;$null}.GetNewClosure()
+ $adapter=Get-CcodLifecycleWorkerAdapters $null
+ $request=New-CcodLifecycleWorkerFixtureRequest -Action ObserveOrdinary
+ $context=[pscustomobject]@{InstallRoot='C:\install';RuntimeRoot='C:\runtime'}
+ $null=& $adapter.ObserveOrdinary $request $context
+ Assert-CcodTrue ($null-ne$capture.Status) 'ordinary observation passes non-null status evidence'
+ Assert-CcodEqual 'schemaVersion,session' (($capture.Status.PSObject.Properties.Name)-join ',') 'ordinary observation status shape is exact'
+ Assert-CcodEqual 1 $capture.Status.schemaVersion 'ordinary observation status schema is one'
+ Assert-CcodEqual $null $capture.Status.session 'ordinary observation has no persisted special session'
+ }finally{$binding['Wait-CcodVerifiedOrdinaryRoot']=$original}
+ }
+
+ $results += Invoke-CcodTest 'controller facade binds delegation before a dot-sourced Action parameter changes scope metadata' {
+ $root=[IO.Path]::GetFullPath((Join-Path ([IO.Path]::GetTempPath()) ('ccod-lifecycle-facade-'+[guid]::NewGuid().ToString('N'))));$roots.Add($root)
+ $runtime=Join-Path $root 'runtime\2.5.0-a';$controllerDirectory=Join-Path $runtime 'src\persistence';[IO.Directory]::CreateDirectory($controllerDirectory)|Out-Null
+ $controllerPath=Join-Path $controllerDirectory 'SessionController.ps1'
+ $fixture=@'
+[CmdletBinding(DefaultParameterSetName='Manual')]
+param(
+ [Parameter(ParameterSetName='Manual')]
+ [ValidateSet('Inspect','Close','Apply','RepairRenderer')]
+ [string]$Action
+)
+function Invoke-CcodSessionController {
+ param($Request,$Paths,[string]$ResultPath,[hashtable]$Adapters)
+ $delegated=& $Adapters.GetDelegatedOwnership
+ if($null-eq$delegated-or$delegated.marker-cne'delegated'){throw 'delegation was not bound'}
+ $expectedRestart=$Request.action-cne'Close'
+ if($Request.restartOrdinary-ne$expectedRestart){throw "restartOrdinary mismatch for $($Request.action)"}
+ $outcome=switch($Request.action){'Close'{'Closed'}'Apply'{'Activated'}'Inspect'{'Inspected'}default{throw 'unexpected action'}}
+ $safeState=switch($Request.action){'Close'{'Closed'}default{'SpecialValidated'}}
+ $result=[pscustomobject][ordered]@{
+ schemaVersion=1;action=$Request.action;ok=$true;outcome=$outcome;safeState=$safeState;stage='Completed';transactionId=$Request.transactionId
+ package=$null;source=$null;special=$null;probes=$null;recovery=$null;error=$null;logFile=$null
+ }
+ [pscustomobject][ordered]@{Result=$result;ExitCode=0}
+}
+'@
+ [IO.File]::WriteAllText($controllerPath,$fixture,[Text.UTF8Encoding]::new($false))
+ $request=New-CcodLifecycleWorkerFixtureRequest -Action Close
+ $context=[pscustomobject][ordered]@{InstallRoot=$root;RuntimeRoot=$runtime}
+ $delegation=[pscustomobject][ordered]@{marker='delegated'}
+ $controller=Invoke-CcodLifecycleControllerFacade -Action Close -Request $request -Context $context -Delegation $delegation
+ Assert-CcodEqual Close $controller.action 'dot-sourced controller receives the Close action'
+ Assert-CcodEqual $true $controller.ok 'dot-sourced controller returns one successful result'
+ Assert-CcodEqual $request.transactionId $controller.transactionId 'dot-sourced controller result remains correlated'
+ $apply=Invoke-CcodLifecycleControllerFacade -Action Apply -Request $request -Context $context -Delegation $delegation
+ Assert-CcodEqual Apply $apply.action 'Apply uses the restart-enabled controller contract'
+ $verify=Invoke-CcodLifecycleControllerFacade -Action VerifyRemote -Request $request -Context $context -Delegation $delegation
+ Assert-CcodEqual Inspect $verify.action 'VerifyRemote maps to the restart-enabled Inspect contract'
+ }
+
$results += Invoke-CcodTest 'worker rejects runtime generation and exact request-shape mismatches before dispatch' {
foreach ($case in @('Runtime','Generation','Extra','UnknownAction')) {
$h=New-CcodLifecycleWorkerHarness -Name $case;$roots.Add($h.Install)
diff --git a/tests/persistence/ProcessControl.SelfTest.ps1 b/tests/persistence/ProcessControl.SelfTest.ps1
index db8c199..20564a9 100644
--- a/tests/persistence/ProcessControl.SelfTest.ps1
+++ b/tests/persistence/ProcessControl.SelfTest.ps1
@@ -139,6 +139,14 @@ function New-CcodSpecialStatus {
}
}
+function Assert-CcodIdentityObservationThrows {
+ param([Parameter(Mandatory)][scriptblock]$Action, [Parameter(Mandatory)][string]$Message)
+
+ $threw = $false
+ try { & $Action | Out-Null } catch { $threw = $true }
+ Assert-CcodTrue $threw $Message
+}
+
try {
Invoke-CcodTest 'default process adapter recognizes exactly one Codex renderer target' {
$module = Get-Module ProcessControl
@@ -157,6 +165,96 @@ try {
$probe = & $adapters.ProbeSpecial 100 9335 52359
Assert-CcodTrue (-not $probe.Valid) 'duplicate renderer targets fail closed'
}
+
+ Invoke-CcodTest 'normalizes native error 31 only when an independent PID probe proves exit' {
+ $module = Get-Module ProcessControl | Select-Object -First 1
+ $failure31 = [Management.Automation.ErrorRecord]::new(
+ [ComponentModel.Win32Exception]::new(31), 'Win32Exception', [Management.Automation.ErrorCategory]::ReadError, 700)
+ $failure5 = [Management.Automation.ErrorRecord]::new(
+ [ComponentModel.Win32Exception]::new(5), 'Win32Exception', [Management.Automation.ErrorCategory]::PermissionDenied, 700)
+ $nonTerminatingFailure = & $module {
+ param($Failure)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId 700 -GetProcess { param($Id) Write-Error 'probe failed' -ErrorAction Continue }
+ } $failure31
+ Assert-CcodEqual $false $nonTerminatingFailure 'a nonterminating PID probe error is never absence proof'
+ $gone = & $module {
+ param($Failure)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId 700 -GetProcess { param($Id) [pscustomobject][ordered]@{Outcome='Absent';Process=$null} }
+ } $failure31
+ Assert-CcodEqual $true $gone 'error 31 plus an absent PID is a proven exit race'
+ $live = & $module {
+ param($Failure,$ProcessId)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId $ProcessId -GetProcess { param($Id) [pscustomobject][ordered]@{Outcome='Found';Process=(Get-Process -Id $Id -ErrorAction Stop)} }
+ } $failure31 ([int]$PID)
+ Assert-CcodEqual $false $live 'error 31 never hides a live or PID-reused process'
+ $terminatingFailure = & $module {
+ param($Failure)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId 700 -GetProcess { param($Id) throw 'probe failed' }
+ } $failure31
+ Assert-CcodEqual $false $terminatingFailure 'a terminating PID probe error is never absence proof'
+ $malformed = & $module {
+ param($Failure)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId 700 -GetProcess { param($Id) $null }
+ } $failure31
+ Assert-CcodEqual $false $malformed 'an empty untyped PID probe result is not absence proof'
+ $wrongError = & $module {
+ param($Failure)
+ Test-CcodNativeProcessQueryProvesExit -Failure $Failure -ProcessId 700 -GetProcess { param($Id) [pscustomobject][ordered]@{Outcome='Absent';Process=$null} }
+ } $failure5
+ Assert-CcodEqual $false $wrongError 'other native failures stay fail-closed even when the PID is absent'
+ }
+
+ Invoke-CcodTest 'observes absence same identity and PID reuse through one minimal native PID and creation query' {
+ $cases = @(
+ @{ Name='absent at handle-open boundary'; Native=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}; Want='Absent'; Creation=$null },
+ @{ Name='same'; Native=[pscustomobject][ordered]@{Outcome='Found';Pid=10664;CreationTimeUtc='2026-08-26T04:49:22.1551350Z'}; Want='SameIdentity'; Creation='2026-08-26T04:49:22.1551350Z' },
+ @{ Name='reused'; Native=[pscustomobject][ordered]@{Outcome='Found';Pid=10664;CreationTimeUtc='2026-08-27T04:49:22.1551350Z'}; Want='IdentityChanged'; Creation='2026-08-27T04:49:22.1551350Z' }
+ )
+ foreach ($case in $cases) {
+ $fixture = [pscustomobject]@{ Value=$case.Native }
+ $calls = [pscustomobject]@{ Count=0 }
+ try {
+ $result = Get-CcodProcessIdentityObservation -ProcessId 10664 -ExpectedCreationTimeUtc '2026-08-26T04:49:22.1551350Z' -Adapters @{
+ GetMinimalNativeProcess = { param($ProcessId) [void]($calls.Count++); $fixture.Value }.GetNewClosure()
+ GetNativeProcess = { throw 'rich path SID package query must not run for PID identity observation' }
+ }
+ } catch { throw "$($case.Name) observation failed: $($_.Exception.Message)" }
+ Assert-CcodEqual 'Outcome,Pid,CreationTimeUtc' (($result.PSObject.Properties.Name) -join ',') "$($case.Name) observation exposes only the ordered identity contract"
+ Assert-CcodEqual $case.Want $result.Outcome "$($case.Name) outcome is hand-derived"
+ Assert-CcodEqual 10664 $result.Pid "$($case.Name) preserves the requested PID"
+ Assert-CcodEqual $case.Creation $result.CreationTimeUtc "$($case.Name) preserves only a proven native creation time"
+ Assert-CcodEqual 1 $calls.Count "$($case.Name) invokes exactly one native identity query"
+ }
+ }
+
+ Invoke-CcodTest 'rejects malformed failed or incidental minimal native identity observations' {
+ $same = [pscustomobject][ordered]@{Outcome='Found';Pid=10664;CreationTimeUtc='2026-08-26T04:49:22.1551350Z'}
+ $wrongPid = $same | ConvertTo-Json -Depth 4 | ConvertFrom-Json
+ $wrongPid.Pid = 10665
+ $noncanonical = $same | ConvertTo-Json -Depth 4 | ConvertFrom-Json
+ $noncanonical.CreationTimeUtc = '2026-08-26T04:49:22Z'
+ $extra = $same | ConvertTo-Json -Depth 4 | ConvertFrom-Json
+ $extra | Add-Member Extra 'unexpected'
+ $missing = $same | ConvertTo-Json -Depth 4 | ConvertFrom-Json
+ $missing.PSObject.Properties.Remove('CreationTimeUtc')
+ foreach ($case in @(
+ @{ Name='access failure'; Query={ param($ProcessId) throw [UnauthorizedAccessException]::new('denied') } },
+ @{ Name='post-handle GetProcessTimes error 87'; Query={ param($ProcessId) throw [ComponentModel.Win32Exception]::new(87) } },
+ @{ Name='wrong PID'; Value=$wrongPid },
+ @{ Name='noncanonical creation time'; Value=$noncanonical },
+ @{ Name='extra native field'; Value=$extra },
+ @{ Name='missing native field'; Value=$missing },
+ @{ Name='success-stream diagnostic'; Query={ param($ProcessId) 'diagnostic'; $null } }
+ )) {
+ $query = if ($null -ne $case.Query) { $case.Query } else { $value=$case.Value; { param($ProcessId) $value }.GetNewClosure() }
+ Assert-CcodIdentityObservationThrows {
+ Get-CcodProcessIdentityObservation -ProcessId 10664 -ExpectedCreationTimeUtc '2026-08-26T04:49:22.1551350Z' -Adapters @{GetMinimalNativeProcess=$query;GetNativeProcess={throw 'rich query must not run'}}
+ } "$($case.Name) cannot be reported as native absence"
+ }
+ Assert-CcodIdentityObservationThrows {
+ Get-CcodProcessIdentityObservation -ProcessId 10664 -ExpectedCreationTimeUtc '2026-08-26T04:49:22Z' -Adapters @{GetMinimalNativeProcess={param($ProcessId)[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}}}
+ } 'a noncanonical expected identity is rejected before absence can be reported'
+ }
} catch {
throw
}
@@ -255,6 +353,15 @@ try {
Assert-CcodEqual 4596 $result.Snapshot.Pid 'the exact old root is retained'
Assert-CcodEqual 6 $calls.Reads 'candidate identity is bracketed and reread before it can be closed'
+ $indeterminateAdapters = $oldAdapters.Clone()
+ $indeterminateAdapters.GetNativeProcess = {
+ param($ProcessId)
+ if ([int]$ProcessId -eq 4597) { return $null }
+ [pscustomobject]@{Pid=[int]$ProcessId;CreationTimeUtc='2026-08-02T00:00:00.0000000Z';SessionId=1;UserSid='S-1-5-21-test';Path=$oldPath;PackageFamilyName='OpenAI.Codex_2p2nqsd0c76g0'}
+ }.GetNewClosure()
+ $incompleteRoots = Get-CcodStalePackageRootResult -Package $current -ProcessIds @(4596,4597) -Adapters $indeterminateAdapters
+ Assert-CcodEqual 'Incomplete' $incompleteRoots.Outcome 'one visible root plus one unclassified enumerated PID cannot authorize stale-root adoption'
+
$gracefulCalls=[pscustomobject]@{Close=0;Dispose=0};$fakeStaleProcess=[pscustomobject]@{Id=4596}
$oldAdapters.GetGracefulCloseProcess={param($ProcessId)$fakeStaleProcess}.GetNewClosure()
$oldAdapters.GetGracefulCloseCreationTimeUtc={param($Process)'2026-08-02T00:00:01.0000000Z'}
@@ -269,6 +376,35 @@ try {
$staleTree=Get-CcodVerifiedStaleProcessTree -Root $result.Snapshot -Package $current -Adapters $oldAdapters
Assert-CcodEqual '4596' (($staleTree|ForEach-Object Pid)-join ',') 'default stale-tree composition retains the exact raw root before any close'
+ $staleChildCommand='"' + $oldPath + '" --type=renderer'
+ $staleTreeReads=@{4596=0;4597=0}
+ $strictStaleTreeAdapters=$oldAdapters.Clone()
+ $strictStaleTreeAdapters.ListProcessIds={@(4596,4597)}
+ $strictStaleTreeAdapters.GetNativeProcess={
+ param($ProcessId)
+ $pidValue=[int]$ProcessId;$staleTreeReads[$pidValue]++
+ if($pidValue -eq 4597 -and $staleTreeReads[$pidValue] -gt 2){return $null}
+ [pscustomobject]@{Pid=$pidValue;CreationTimeUtc=if($pidValue -eq 4596){'2026-08-02T00:00:00.0000000Z'}else{'2026-08-02T00:00:01.0000000Z'};SessionId=1;UserSid='S-1-5-21-test';Path=$oldPath;PackageFamilyName='OpenAI.Codex_2p2nqsd0c76g0'}
+ }.GetNewClosure()
+ $strictStaleTreeAdapters.GetCimProcess={
+ param($ProcessId)
+ [pscustomobject]@{ProcessId=[uint32]$ProcessId;ParentProcessId=if([int]$ProcessId -eq 4596){[uint32]0}else{[uint32]4596};CommandLine=if([int]$ProcessId -eq 4596){$oldCommand}else{$staleChildCommand}}
+ }.GetNewClosure()
+ $strictStaleTreeAdapters.ParseCommandLine={param($CommandLine)if($CommandLine -ceq $oldCommand){$oldArgs}else{@($oldPath,'--type=renderer')}}.GetNewClosure()
+ $indeterminateStaleTree=@(Get-CcodVerifiedStaleProcessTree -Root $result.Snapshot -Package $current -Adapters $strictStaleTreeAdapters)
+ Assert-CcodEqual 0 $indeterminateStaleTree.Count 'one stale root plus one indeterminate reachable child cannot authorize a complete close tree'
+
+ $initialStaleTreeAdapters=$oldAdapters.Clone()
+ $initialStaleTreeAdapters.ListProcessIds={@(4596,4598)}
+ $initialStaleTreeAdapters.GetNativeProcess={
+ param($ProcessId)
+ if([int]$ProcessId -eq 4598){return $null}
+ [pscustomobject]@{Pid=4596;CreationTimeUtc='2026-08-02T00:00:00.0000000Z';SessionId=1;UserSid='S-1-5-21-test';Path=$oldPath;PackageFamilyName='OpenAI.Codex_2p2nqsd0c76g0'}
+ }.GetNewClosure()
+ $initialStaleTreeAdapters.GetMinimalNativeProcess={param($ProcessId)[pscustomobject][ordered]@{Outcome='Found';Pid=[int]$ProcessId;CreationTimeUtc=if([int]$ProcessId -eq 4596){'2026-08-02T00:00:00.0000000Z'}else{'2026-08-02T00:00:01.0000000Z'}}}
+ $initialIndeterminateStaleTree=@(Get-CcodVerifiedStaleProcessTree -Root $result.Snapshot -Package $current -Adapters $initialStaleTreeAdapters)
+ Assert-CcodEqual 0 $initialIndeterminateStaleTree.Count 'an initially indeterminate live stale-scope PID rejects the complete stale close tree'
+
$currentRoot = New-CcodSnapshot -ProcessId 26700 -Path $current.ExecutablePath -Mode Unrelated -RendererPort 41003 -MainPort 41004 -CommandLine ('"' + $current.ExecutablePath + '" --remote-debugging-address=127.0.0.1 --remote-debugging-port=41003 --inspect=127.0.0.1:41004')
$currentRoot.IsTopLevel = $true
$currentResult = Get-CcodStalePackageRootResult -Package $current -Snapshots @($currentRoot) -Adapters @{ GetCurrentSessionId={1};GetCurrentUserSid={'S-1-5-21-test'};GetProcess = { param($ProcessId, $StatusEvidence) $currentRoot } }
@@ -513,6 +649,7 @@ try {
$calls = [pscustomobject]@{ Stop = 0 }
$exited = Stop-CcodProcessIfMatch -Expected $expected -Adapters @{
GetProcess = { param($ProcessId) $null }
+ ObserveProcessIdentity = { param($ProcessId,$ExpectedCreationTimeUtc) [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null} }
StopProcess = { $calls.Stop++; throw 'must not run' }.GetNewClosure()
}
Assert-CcodStopResultContract -Result $exited -Outcome SourceExited -Stopped $false -Snapshot $null -Message 'natural exit'
@@ -524,6 +661,20 @@ try {
}
Assert-CcodStopResultContract -Result $changed -Outcome IdentityChanged -Stopped $false -Snapshot $reused -Message 'PID reuse'
Assert-CcodEqual 0 $calls.Stop 'dangerous boundary is unreachable without an exact reread'
+
+ foreach ($case in @(
+ @{Name='same identity after rich null';Observe={param($ProcessId,$ExpectedCreationTimeUtc)[pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc}}},
+ @{Name='indeterminate observer';Observe={param($ProcessId,$ExpectedCreationTimeUtc)throw 'native creation query failed'}}
+ )) {
+ $observer=$case.Observe
+ $unproven = Stop-CcodProcessIfMatch -Expected $expected -Adapters @{
+ GetProcess = { param($ProcessId,$StatusEvidence) $null }
+ ObserveProcessIdentity = $observer
+ StopProcess = { $calls.Stop++; throw 'must not run' }.GetNewClosure()
+ }
+ Assert-CcodStopResultContract -Result $unproven -Outcome StopUnconfirmed -Stopped $false -Snapshot $null -Message $case.Name
+ }
+ Assert-CcodEqual 0 $calls.Stop 'rich null never reaches the stop boundary without strict absence or reuse proof'
}
Invoke-CcodTest 'default graceful-close adapter rejects PID reuse on the same process object before signaling it' {
@@ -598,8 +749,16 @@ try {
$exitedReceipt = Stop-CcodProcessIfMatch -Expected $expected -Adapters @{
GetProcess = { param($ProcessId, $StatusEvidence) $expected }.GetNewClosure()
StopProcess = { param($Snapshot, $TimeoutMilliseconds) [pscustomobject]@{ Outcome='ExitedBeforeStop'; StoppedByController=$false } }
+ ObserveProcessIdentity = { param($ProcessId,$ExpectedCreationTimeUtc) [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null} }
}
Assert-CcodStopResultContract -Result $exitedReceipt -Outcome SourceExited -Stopped $false -Snapshot $null -Message 'internal pre-stop exit'
+
+ $sameAfterReceipt = Stop-CcodProcessIfMatch -Expected $expected -Adapters @{
+ GetProcess = { param($ProcessId, $StatusEvidence) $expected }.GetNewClosure()
+ StopProcess = { param($Snapshot, $TimeoutMilliseconds) [pscustomobject]@{ Outcome='ExitedBeforeStop'; StoppedByController=$false } }
+ ObserveProcessIdentity = { param($ProcessId,$ExpectedCreationTimeUtc) [pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc} }
+ }
+ Assert-CcodStopResultContract -Result $sameAfterReceipt -Outcome StopUnconfirmed -Stopped $false -Snapshot $expected -Message 'post-handle exit receipt without strict exit proof'
}
Invoke-CcodTest 'distinguishes access denial and delayed exit' {
@@ -657,7 +816,24 @@ try {
Assert-CcodTrue ($reads.Count -ge 10) 'included tree identities are reread before return'
}
- Invoke-CcodTest 'rejects a child older than its exact current parent and drops mutated rereads' {
+ Invoke-CcodTest 'rejects an initially indeterminate live current-scope tree member but ignores proven foreign scope' {
+ $root=New-CcodSnapshot
+ foreach($case in @(
+ @{Name='current scope';UserSid='S-1-5-21-test';Want=0},
+ @{Name='foreign scope';UserSid='S-1-5-21-other';Want=1}
+ )){
+ $sid=[string]$case.UserSid
+ $tree=@(Get-CcodVerifiedProcessTree -Root $root -Adapters @{
+ ListProcessIds={@(100,101)}
+ GetProcess={param($ProcessId,$StatusEvidence)if([int]$ProcessId -eq 100){$root}else{$null}}.GetNewClosure()
+ GetMinimalNativeProcess={param($ProcessId)[pscustomobject][ordered]@{Outcome='Found';Pid=[int]$ProcessId;CreationTimeUtc='2026-08-02T00:00:01.0000000Z'}}
+ GetNativeProcess={param($ProcessId)[pscustomobject][ordered]@{Pid=[int]$ProcessId;CreationTimeUtc='2026-08-02T00:00:01.0000000Z';SessionId=1;UserSid=$sid;Path='C:\Codex\ChatGPT.exe';PackageFamilyName='OpenAI.Codex_2p2nqsd0c76g0'}}.GetNewClosure()
+ })
+ Assert-CcodEqual $case.Want $tree.Count "$($case.Name) indeterminate PID is gated only by exact current scope"
+ }
+ }
+
+ Invoke-CcodTest 'rejects a child older than its exact current parent and rejects an indeterminate reachable reread' {
$root = New-CcodSnapshot
$reusedParent = New-CcodSnapshot -ProcessId 101 -CreationTimeUtc '2026-08-02T00:00:10.0000000Z' -ParentPid 100 -IsTopLevel $false -Mode Unrelated
$staleChild = New-CcodSnapshot -ProcessId 102 -CreationTimeUtc '2026-08-02T00:00:05.0000000Z' -ParentPid 101 -IsTopLevel $false -Mode Unrelated
@@ -681,8 +857,21 @@ try {
$root
}.GetNewClosure()
})
- Assert-CcodEqual '100' (($mutationTree.Pid | Sort-Object) -join ',') 'identity mutation on final reread removes the node'
+ Assert-CcodEqual '' (($mutationTree.Pid | Sort-Object) -join ',') 'identity mutation on a reachable final reread rejects the complete tree'
Assert-CcodTrue ($counts[103] -ge 2) 'included child is actually reread'
+
+ $nullCounts = @{100=0;103=0}
+ $indeterminateTree = @(Get-CcodVerifiedProcessTree -Root $root -Adapters @{
+ ListProcessIds = { @(100,103) }
+ GetProcess = {
+ param($ProcessId,$StatusEvidence)
+ $nullCounts[[int]$ProcessId]++
+ if ([int]$ProcessId -eq 103 -and $nullCounts[103] -gt 1) { return $null }
+ if ([int]$ProcessId -eq 103) { return $stableChild }
+ $root
+ }.GetNewClosure()
+ })
+ Assert-CcodEqual 0 $indeterminateTree.Count 'one verified root plus one indeterminate reachable child cannot authorize a complete close tree'
}
Invoke-CcodTest 'passes status evidence while rereading a Special tree root' {
diff --git a/tests/persistence/ReleaseWorkflow.SelfTest.ps1 b/tests/persistence/ReleaseWorkflow.SelfTest.ps1
index 3d4ec8f..2fc3b87 100644
--- a/tests/persistence/ReleaseWorkflow.SelfTest.ps1
+++ b/tests/persistence/ReleaseWorkflow.SelfTest.ps1
@@ -229,22 +229,30 @@ Invoke-CcodTest 'package scripts build provenance and workflows retain the relea
Assert-CcodTrue ($release -cmatch '(?ms)^ publish:\r?\n needs: build\r?\n runs-on: windows-latest\r?\n permissions:\r?\n contents: write\s*$') 'only the publish job receives release-write permission'
}
-Invoke-CcodTest '2.5.9 documentation matches the portable release, Defender gate, and protected uninstall contract' {
+$iss = Get-Content -LiteralPath (Join-Path $repositoryRoot 'build\CodexControlOtherDevices.iss') -Raw; Assert-CcodTrue ($iss -match 'PortableArtifactDirectory' -and $iss -match 'CodexRemote.Portable.exe' -and $iss -match 'portable-launcher-provenance.json') 'Inno installer packages the portable launcher into the verified bin set';
+Invoke-CcodTest '2.5.21 documentation matches the stable portable release, Defender gate, and protected uninstall contract' {
$package = Get-Content -LiteralPath (Join-Path $repositoryRoot 'package.json') -Raw | ConvertFrom-Json
- Assert-CcodEqual '2.5.9' ([string]$package.version) 'package metadata is the 2.5.9 release'
+ Assert-CcodEqual '2.5.21' ([string]$package.version) 'package metadata is the 2.5.21 release'
$changelog = Get-Content -LiteralPath (Join-Path $repositoryRoot 'CHANGELOG.md') -Raw
- $releaseSection = [regex]::Match($changelog, '(?ms)^## v2\.5\.9\s*\r?\n(?.*?)(?=^## |\z)')
- Assert-CcodTrue $releaseSection.Success 'v2.5.9 release section exists'
- Assert-CcodTrue ($releaseSection.Groups['body'].Value.Contains('Inno Setup installer executable')) 'v2.5.9 changelog records the upload release fix'
+ $releaseSection = [regex]::Match($changelog, '(?ms)^## v2\.5\.21\s*\r?\n(?.*?)(?=^## |\z)')
+ Assert-CcodTrue $releaseSection.Success 'v2.5.21 release section exists'
+ Assert-CcodTrue ($releaseSection.Groups['body'].Value -match '(?m)^### English\s*$') 'v2.5.21 changelog has concise English release notes'
+ Assert-CcodTrue ($releaseSection.Groups['body'].Value.Contains('stale-status recovery')) 'v2.5.21 changelog records stale-status repair'
+ Assert-CcodTrue ($releaseSection.Groups['body'].Value.Contains('safe lifecycle diagnostics')) 'v2.5.21 changelog records exact safe diagnostics'
+ Assert-CcodTrue (-not $releaseSection.Groups['body'].Value.Contains('installed Supervisor and TrayHost lifecycle evidence')) 'v2.5.21 changelog omits the unsupported Supervisor and TrayHost validation claim'
$readme = Get-Content -LiteralPath (Join-Path $repositoryRoot 'README.md') -Raw
$quickStart = [regex]::Match($readme, '(?ms)^## Quick start\s*\r?\n(?.*?)(?=^## |\z)').Groups['body'].Value
- Assert-CcodTrue ($quickStart.Contains('CodexRemote-fix-2.5.9-setup.exe')) 'English Quick Start names the setup installer'
- Assert-CcodTrue ($quickStart.Contains('CodexRemote-fix-2.5.9-windows-x64.zip')) 'English Quick Start names the portable ZIP'
+ Assert-CcodTrue ($readme.Contains('v2.5.21 is the current stable release')) 'English README marks v2.5.21 stable without a version-by-version What''s new block'
+ Assert-CcodTrue (-not $readme.Contains("## What's new")) 'English README keeps release details off the home page'
+ Assert-CcodTrue ($quickStart.Contains('CodexRemote-fix-2.5.21-setup.exe')) 'English Quick Start names the setup installer'
+ Assert-CcodTrue ($quickStart.Contains('CodexRemote-fix-2.5.21-windows-x64.zip')) 'English Quick Start names the portable ZIP'
Assert-CcodTrue ($quickStart.Contains('CodexRemote-fix.exe')) 'English Quick Start names the portable double-click entrypoint'
Assert-CcodTrue ($quickStart.Contains('Microsoft Defender')) 'English Quick Start documents the local Defender gate'
$readmeZh = Get-Content -LiteralPath (Join-Path $repositoryRoot 'README.zh-CN.md') -Raw -Encoding UTF8
- Assert-CcodTrue ($readmeZh.Contains('CodexRemote-fix-2.5.9-setup.exe')) 'Chinese Quick Start names the setup installer'
- Assert-CcodTrue ($readmeZh.Contains('CodexRemote-fix-2.5.9-windows-x64.zip')) 'Chinese Quick Start names the portable ZIP'
+ Assert-CcodTrue ($readmeZh -match 'v2\.5\.21 \u662F\u5F53\u524D\u7A33\u5B9A\u7248') 'Chinese README marks v2.5.21 stable'
+ Assert-CcodTrue (-not $readmeZh.Contains("## What's new")) 'Chinese README keeps release details off the home page'
+ Assert-CcodTrue ($readmeZh.Contains('CodexRemote-fix-2.5.21-setup.exe')) 'Chinese Quick Start names the setup installer'
+ Assert-CcodTrue ($readmeZh.Contains('CodexRemote-fix-2.5.21-windows-x64.zip')) 'Chinese Quick Start names the portable ZIP'
Assert-CcodTrue ($readmeZh.Contains('CodexRemote-fix.exe')) 'Chinese Quick Start names the portable double-click entrypoint'
Assert-CcodTrue ($readmeZh.Contains('Microsoft Defender')) 'Chinese Quick Start documents the local Defender gate'
$technical = Get-Content -LiteralPath (Join-Path $repositoryRoot 'docs\TECHNICAL.md') -Raw
diff --git a/tests/persistence/SessionEngine.SelfTest.ps1 b/tests/persistence/SessionEngine.SelfTest.ps1
index 2e840d7..ff0c1d6 100644
--- a/tests/persistence/SessionEngine.SelfTest.ps1
+++ b/tests/persistence/SessionEngine.SelfTest.ps1
@@ -274,6 +274,7 @@ function New-CcodEngineAdapters {
StaticProbe={ param($NodeCandidates,$CheckerPath) $eventsValue.Add('StaticProbe'); $probeValue }.GetNewClosure()
ListProcesses={ param($StatusEvidence) @($processValues) }.GetNewClosure()
GetProcess={ param($Pid,$StatusEvidence) @($processValues | Where-Object { $_.Pid -eq $Pid } | Select-Object -First 1) }.GetNewClosure()
+ ObserveProcessIdentity={ param($ProcessId,$ExpectedCreationTimeUtc) [pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc} }
ProcessMatch={ param($Expected,$Actual) $null -ne $Actual -and $Expected.Pid -eq $Actual.Pid -and $Expected.CreationTimeUtc -ceq $Actual.CreationTimeUtc -and $Expected.Mode -ceq $Actual.Mode }
NewTransition={
param($Path,$Source,$Package,$RuntimeId,$RendererPort,$MainPort,$TransactionId)
@@ -321,6 +322,17 @@ function New-CcodEngineAdapters {
}
}
+function Set-CcodEngineAliveIdentityObserver {
+ param([Parameter(Mandatory)][hashtable]$Adapters,[Parameter(Mandatory)][hashtable]$Alive)
+ $aliveMap=$Alive
+ $Adapters.ObserveProcessIdentity={
+ param($ProcessId,$ExpectedCreationTimeUtc)
+ if(-not $aliveMap.ContainsKey([int]$ProcessId)){return [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null}}
+ $creation=[string]$aliveMap[[int]$ProcessId].CreationTimeUtc
+ [pscustomobject][ordered]@{Outcome=if($creation -ceq $ExpectedCreationTimeUtc){'SameIdentity'}else{'IdentityChanged'};Pid=[int]$ProcessId;CreationTimeUtc=$creation}
+ }.GetNewClosure()
+}
+
function Assert-CcodEngineResultContract($Result,[string]$TransactionId,[string]$Message) {
Assert-CcodEqual 'schemaVersion,action,ok,outcome,safeState,stage,transactionId,package,source,special,probes,recovery,error,logFile' (($Result.PSObject.Properties.Name) -join ',') "$Message exact 14 fields"
Assert-CcodEqual $TransactionId $Result.transactionId "$Message request correlation"
@@ -460,7 +472,8 @@ try {
Assert-CcodEqual 1 $messages.Count 'one core failure writes one diagnostic record'
Assert-CcodTrue ($messages[0] -cnotmatch 'secret|hunter2|swordfish|command\.exe|[\r\n]') 'diagnostic record excludes raw path command secret and multiline text'
$record=$messages[0]|ConvertFrom-Json
- Assert-CcodEqual 'schemaVersion,timestampUtc,action,transactionId,stage,code' (($record.PSObject.Properties.Name)-join ',') 'diagnostic log uses the fixed allowlist'
+ Assert-CcodEqual 'schemaVersion,timestampUtc,action,transactionId,stage,code,reason' (($record.PSObject.Properties.Name)-join ',') 'diagnostic log uses the fixed allowlist'
+ Assert-CcodEqual 'Unclassified' $record.reason 'unknown adapter failure records only the allow-listed fallback reason'
[IO.Directory]::CreateDirectory((Split-Path $paths.SessionLogPath -Parent))|Out-Null
[IO.File]::WriteAllText($paths.SessionLogPath,('x'*(2MB+1)),[Text.UTF8Encoding]::new($false))
@@ -473,6 +486,34 @@ try {
Assert-CcodEqual $false (Test-Path -LiteralPath ($paths.SessionLogPath+'.11')) 'rotation never creates an eleventh history generation'
}
+ Invoke-CcodTest 'records only exact safe close failure reasons without exception text' {
+ $cases=@(
+ [pscustomobject]@{Message='Recorded Active root is missing, changed, or accompanied by another root';Reason='RecordedActiveMismatch'},
+ [pscustomobject]@{Message='Recorded Active root cannot be safely replaced';Reason='RecordedActiveMismatch'},
+ [pscustomobject]@{Message='Multiple current-package close roots are ambiguous';Reason='MultipleRoots'},
+ [pscustomobject]@{Message='A status-less debug root lacks one valid distinct port pair';Reason='MissingPortPair'},
+ [pscustomobject]@{Message='A replacement debug root lacks one valid distinct port pair';Reason='MissingPortPair'},
+ [pscustomobject]@{Message='Requested close source is not the one verified current root';Reason='RequestedSourceMismatch'},
+ [pscustomobject]@{Message='Current close target tree is not exact and verified';Reason='EmptyVerifiedTree'},
+ [pscustomobject]@{Message="unknown C:\\private\\target`n--token hunter2";Reason='Unclassified'}
+ )
+ foreach($case in $cases){
+ $messages=[Collections.Generic.List[string]]::new()
+ $record=[Management.Automation.ErrorRecord]::new([InvalidOperationException]::new($case.Message),'CCOD_CLOSE_UNPROVEN',[Management.Automation.ErrorCategory]::InvalidData,$null)
+ $result=Invoke-CcodApplySession -Request (New-CcodEngineRequest -Action Apply -Source (New-CcodEngineSnapshot) -TransactionId ([guid]::NewGuid().ToString('D'))) -Paths $paths -Adapters @{
+ ReadState={throw $record}.GetNewClosure()
+ WriteLog={param($Path,$Message)$messages.Add($Message)}.GetNewClosure()
+ }
+ Assert-CcodEqual 'CCOD_CLOSE_UNPROVEN' $result.error.code "$($case.Reason) retains the stable failure code"
+ Assert-CcodEqual 1 $messages.Count "$($case.Reason) writes one diagnostic record"
+ $diagnostic=$messages[0]|ConvertFrom-Json
+ Assert-CcodEqual 'schemaVersion,timestampUtc,action,transactionId,stage,code,reason' (($diagnostic.PSObject.Properties.Name)-join ',') "$($case.Reason) uses only the diagnostic allowlist"
+ Assert-CcodEqual 2 $diagnostic.schemaVersion "$($case.Reason) uses diagnostic schema 2"
+ Assert-CcodEqual $case.Reason $diagnostic.reason "$($case.Reason) maps only the exact stable predicate"
+ Assert-CcodTrue ($messages[0] -cnotmatch 'private|target|hunter2|[\r\n]') "$($case.Reason) never logs raw exception text"
+ }
+ }
+
Invoke-CcodTest 'maps every unrecognized prefixed error ID to the stable generic code' {
foreach($case in @(
[pscustomobject]@{Name='token';Id='CCOD_LEAK_PRIVATE_TOKEN';Marker='LEAK_PRIVATE_TOKEN'},
@@ -1145,6 +1186,7 @@ try {
$adapters=New-CcodEngineAdapters -State (New-CcodEngineState -Status $status) -Processes @($special) -Events $events -Counters $counters
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure()
$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure()
+ $adapters.ObserveProcessIdentity={param($ProcessId,$ExpectedCreationTimeUtc)if($alive.ContainsKey([int]$ProcessId)){[pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc}}else{[pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null}}}.GetNewClosure()
$adapters.GetTree={param($Root,$StatusEvidence)@($special,$child)}.GetNewClosure()
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$events.Add("Stop:$($Expected.Pid)");$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
$request=New-CcodEngineRequest -SchemaVersion 2 -Action Close -Source $special -RestartOrdinary $false
@@ -1154,6 +1196,148 @@ try {
Assert-CcodEqual 0 $counters.OrdinaryStart 'Close never requests ordinary activation'
}
+ Invoke-CcodTest 'proven stale status adopts and closes the unique current ordinary root' {
+ $oldStatus = New-CcodEngineActiveStatus -RuntimeId '2.5.19-old'
+ $oldStatus.session.codex.pid = 10664
+ $oldStatus.session.codex.creationTimeUtc = '2026-08-26T04:49:22.1551350Z'
+ $oldStatus.session.codex.packageFullName = 'OpenAI.Codex_26.820.7780.0_x64__2p2nqsd0c76g0'
+ $oldStatus.session.codex.packageVersion = '26.820.7780.0'
+ $replacement = New-CcodEngineSnapshot -Pid 13948 -CreationTimeUtc '2026-08-27T07:00:17.0000000Z' -Mode Ordinary
+ $probe = New-CcodEngineProbe
+ $probe.PackageFullName = 'OpenAI.Codex_26.820.9563.0_x64__2p2nqsd0c76g0'
+ $probe.PackageVersion = '26.820.9563.0'
+ $events = [Collections.Generic.List[string]]::new()
+ $alive = @{13948=$replacement}
+ $adapters = New-CcodEngineAdapters -State (New-CcodEngineState -Status $oldStatus) -Probe $probe -Processes @($replacement) -Events $events
+ $adapters.StaticProbe = { param($NodeCandidates,$CheckerPath) $probe }.GetNewClosure()
+ $adapters.ListProcesses = { param($StatusEvidence) @($alive.Values) }.GetNewClosure()
+ $adapters.GetProcess = { param($ProcessId,$StatusEvidence) if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null} }.GetNewClosure()
+ $adapters.ObserveProcessIdentity = { param($ProcessId,$ExpectedCreationTimeUtc) [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null} }
+ $adapters.GetTree = { param($Root,$StatusEvidence) @($Root) }
+ $adapters.StopProcess = { param($Expected,$StatusEvidence,$TimeoutMilliseconds) $events.Add("Stop:$($Expected.Pid)");$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected} }.GetNewClosure()
+
+ $result = Invoke-CcodCloseSession -Request (New-CcodEngineRequest -SchemaVersion 2 -Action Close -RestartOrdinary $false) -Paths $paths -Adapters $adapters
+ Assert-CcodEqual 'Closed' $result.outcome "proven stale status adopts and closes the unique current ordinary root error=$($result.error.code):$($result.error.message)"
+ Assert-CcodEqual 'IntentWritten,CloseRequested,Stop:13948,Closed,WriteStatus,Complete:Closed' ($events -join ',') 'journal precedes stop and stale status is cleared only at durable completion'
+ }
+
+ Invoke-CcodTest 'stale status replacement remains fail-closed without one strict replacement root and tree' {
+ $oldStatus = New-CcodEngineActiveStatus -RuntimeId '2.5.19-old'
+ $oldStatus.session.codex.pid = 10664
+ $oldStatus.session.codex.creationTimeUtc = '2026-08-26T04:49:22.1551350Z'
+ $oldStatus.session.codex.packageFullName = 'OpenAI.Codex_26.820.7780.0_x64__2p2nqsd0c76g0'
+ $oldStatus.session.codex.packageVersion = '26.820.7780.0'
+ $probe = New-CcodEngineProbe
+ $probe.PackageFullName = 'OpenAI.Codex_26.820.9563.0_x64__2p2nqsd0c76g0'
+ $probe.PackageVersion = '26.820.9563.0'
+ $ordinary = New-CcodEngineSnapshot -Pid 13948 -CreationTimeUtc '2026-08-27T07:00:17.0000000Z' -Mode Ordinary
+ $second = New-CcodEngineSnapshot -Pid 13949 -CreationTimeUtc '2026-08-27T07:00:18.0000000Z' -Mode Ordinary
+ $missingPort = New-CcodEngineSnapshot -Pid 13950 -CreationTimeUtc '2026-08-27T07:00:19.0000000Z' -Mode Unrelated -RendererPort 42001
+ $equalPorts = New-CcodEngineSnapshot -Pid 13951 -CreationTimeUtc '2026-08-27T07:00:20.0000000Z' -Mode Unrelated -RendererPort 42001 -MainPort 42001
+ $cases = @(
+ @{Name='same native identity';Processes=@($ordinary);Observation=[pscustomobject][ordered]@{Outcome='SameIdentity';Pid=10664;CreationTimeUtc='2026-08-26T04:49:22.1551350Z'}},
+ @{Name='native query exception';Processes=@($ordinary);Throw=$true},
+ @{Name='invalid native receipt';Processes=@($ordinary);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null;Extra=$true}},
+ @{Name='zero roots';Processes=@();Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}},
+ @{Name='two top-level roots';Processes=@($ordinary,$second);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}},
+ @{Name='one visible root plus one indeterminate root';Processes=@($ordinary);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null};EnumerationIncomplete=$true},
+ @{Name='debug root missing one port';Processes=@($missingPort);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}},
+ @{Name='debug root equal ports';Processes=@($equalPorts);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null}},
+ @{Name='empty verified tree';Processes=@($ordinary);Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null};EmptyTree=$true}
+ )
+ foreach($case in $cases){
+ $events=[Collections.Generic.List[string]]::new();$processes=@($case.Processes);$alive=@{};foreach($process in $processes){$alive[[int]$process.Pid]=$process}
+ $adapters=New-CcodEngineAdapters -State (New-CcodEngineState -Status $oldStatus) -Probe $probe -Processes $processes -Events $events
+ $adapters.StaticProbe={param($NodeCandidates,$CheckerPath)$probe}.GetNewClosure()
+ $adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure()
+ if($case.ContainsKey('EnumerationIncomplete')){
+ $visible=@($alive.Values)
+ $adapters.ListProcessResult={param($StatusEvidence)[pscustomobject][ordered]@{Outcome='Incomplete';Snapshots=@($visible)}}.GetNewClosure()
+ }
+ $adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure()
+ if($case.Throw){$adapters.ObserveProcessIdentity={param($ProcessId,$ExpectedCreationTimeUtc)throw 'native query failed'}}else{$observation=$case.Observation;$adapters.ObserveProcessIdentity={param($ProcessId,$ExpectedCreationTimeUtc)$observation}.GetNewClosure()}
+ if($case.EmptyTree){$adapters.GetTree={param($Root,$StatusEvidence)@()}}
+ $adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$events.Add("Stop:$($Expected.Pid)");throw 'must not stop'}.GetNewClosure()
+ $result=Invoke-CcodCloseSession -Request (New-CcodEngineRequest -SchemaVersion 2 -Action Close -RestartOrdinary $false -TransactionId ([guid]::NewGuid().ToString('D'))) -Paths $paths -Adapters $adapters
+ Assert-CcodEqual 'Error' $result.outcome "$($case.Name) fails closed"
+ Assert-CcodEqual 'CCOD_CLOSE_UNPROVEN' $result.error.code "$($case.Name) returns the exact stable close rejection"
+ Assert-CcodEqual '' ($events -join ',') "$($case.Name) performs zero journal status or process mutations"
+ }
+ }
+
+ Invoke-CcodTest 'rich null never proves tree exit before stop after stop or during final verification' {
+ $probe=New-CcodEngineProbe
+ $rootProcess=New-CcodEngineSnapshot -Pid 13948 -CreationTimeUtc '2026-08-27T07:00:17.0000000Z' -Mode Ordinary
+ foreach($case in @(
+ @{Name='pre-stop rich null plus SameIdentity';Stage='Pre';Indeterminate=$false},
+ @{Name='post-stop rich null plus SameIdentity';Stage='Post';Indeterminate=$false},
+ @{Name='observer indeterminate';Stage='Pre';Indeterminate=$true},
+ @{Name='final rich null plus SameIdentity';Stage='Final';Indeterminate=$false}
+ )){
+ $events=[Collections.Generic.List[string]]::new();$reads=[pscustomobject]@{Count=0};$observations=[pscustomobject]@{Count=0}
+ $adapters=New-CcodEngineAdapters -State (New-CcodEngineState) -Probe $probe -Processes @($rootProcess) -Events $events
+ $adapters.StaticProbe={param($NodeCandidates,$CheckerPath)$probe}.GetNewClosure()
+ $adapters.ListProcesses={param($StatusEvidence)@($rootProcess)}.GetNewClosure()
+ $stage=[string]$case.Stage
+ $adapters.GetProcess={
+ param($ProcessId,$StatusEvidence)
+ $reads.Count++
+ if($stage -ceq 'Pre'){return $null}
+ if($reads.Count -eq 1){return $rootProcess}
+ return $null
+ }.GetNewClosure()
+ $indeterminate=[bool]$case.Indeterminate
+ $adapters.ObserveProcessIdentity={
+ param($ProcessId,$ExpectedCreationTimeUtc)
+ $observations.Count++
+ if($indeterminate){throw 'native creation observation failed'}
+ if($stage -ceq 'Final' -and $observations.Count -eq 1){return [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null}}
+ [pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc}
+ }.GetNewClosure()
+ $adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ $adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$events.Add("Stop:$($Expected.Pid)");[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
+ $result=Invoke-CcodCloseSession -Request (New-CcodEngineRequest -SchemaVersion 2 -Action Close -RestartOrdinary $false -TransactionId ([guid]::NewGuid().ToString('D'))) -Paths $paths -Adapters $adapters
+ Assert-CcodEqual 'Error' $result.outcome "$($case.Name) fails closed"
+ Assert-CcodEqual 'CCOD_CLOSE_UNPROVEN' $result.error.code "$($case.Name) uses the stable close proof error"
+ Assert-CcodTrue (($events -join ',') -cmatch 'IntentWritten,CloseRequested') "$($case.Name) retains the durable CloseRequested checkpoint"
+ Assert-CcodTrue (($events -join ',') -cnotmatch 'Closed|WriteStatus|Complete:') "$($case.Name) never writes Closed clears status or archives completion"
+ Assert-CcodTrue ($observations.Count -ge 1) "$($case.Name) invokes the strict PID and creation observer"
+ }
+ }
+
+ Invoke-CcodTest 'stale status accepts PID reuse and one debug replacement only with its new distinct ports' {
+ $oldStatus = New-CcodEngineActiveStatus -RuntimeId '2.5.19-old'
+ $oldStatus.session.codex.pid = 10664
+ $oldStatus.session.codex.creationTimeUtc = '2026-08-26T04:49:22.1551350Z'
+ $oldStatus.session.codex.packageFullName = 'OpenAI.Codex_26.820.7780.0_x64__2p2nqsd0c76g0'
+ $oldStatus.session.codex.packageVersion = '26.820.7780.0'
+ $probe = New-CcodEngineProbe
+ $probe.PackageFullName = 'OpenAI.Codex_26.820.9563.0_x64__2p2nqsd0c76g0'
+ $probe.PackageVersion = '26.820.9563.0'
+ $reuse = New-CcodEngineSnapshot -Pid 10664 -CreationTimeUtc '2026-08-27T07:00:17.0000000Z' -Mode Ordinary
+ $debug = New-CcodEngineSnapshot -Pid 13948 -CreationTimeUtc '2026-08-27T07:00:18.0000000Z' -Mode Unrelated -RendererPort 42001 -MainPort 42002
+ $debug.CommandLine='"C:\Codex\ChatGPT.exe" --remote-debugging-address=127.0.0.1 --remote-debugging-port=42001 --inspect=127.0.0.1:42002'
+ foreach($case in @(
+ @{Name='PID reuse';Root=$reuse;Observation=[pscustomobject][ordered]@{Outcome='IdentityChanged';Pid=10664;CreationTimeUtc='2026-08-27T07:00:17.0000000Z'};Ports=''},
+ @{Name='debug replacement';Root=$debug;Observation=[pscustomobject][ordered]@{Outcome='Absent';Pid=10664;CreationTimeUtc=$null};Ports='42001,42002'}
+ )){
+ $events=[Collections.Generic.List[string]]::new();$ports=[Collections.Generic.List[int]]::new();$rootValue=$case.Root;$alive=@{([int]$rootValue.Pid)=$rootValue}
+ $adapters=New-CcodEngineAdapters -State (New-CcodEngineState -Status $oldStatus) -Probe $probe -Processes @($rootValue) -Events $events
+ $adapters.StaticProbe={param($NodeCandidates,$CheckerPath)$probe}.GetNewClosure();$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure()
+ $adapters.ObserveProcessIdentity={
+ param($ProcessId,$ExpectedCreationTimeUtc)
+ if(-not $alive.ContainsKey([int]$ProcessId)){return [pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null}}
+ $creation=[string]$alive[[int]$ProcessId].CreationTimeUtc
+ [pscustomobject][ordered]@{Outcome=if($creation -ceq $ExpectedCreationTimeUtc){'SameIdentity'}else{'IdentityChanged'};Pid=[int]$ProcessId;CreationTimeUtc=$creation}
+ }.GetNewClosure();$adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ $adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$events.Add("Stop:$($Expected.Pid)");$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
+ $adapters.WaitPortClosed={param($Port,$TimeoutMilliseconds)$ports.Add([int]$Port);$true}.GetNewClosure()
+ $result=Invoke-CcodCloseSession -Request (New-CcodEngineRequest -SchemaVersion 2 -Action Close -RestartOrdinary $false -TransactionId ([guid]::NewGuid().ToString('D'))) -Paths $paths -Adapters $adapters
+ Assert-CcodEqual 'Closed' $result.outcome "$($case.Name) closes the one proven replacement"
+ Assert-CcodEqual $case.Ports ($ports -join ',') "$($case.Name) uses only the replacement command-line ports"
+ }
+ }
+
Invoke-CcodTest 'schema-v2 mutations revalidate the lifecycle fence immediately before stop launch bridge status and terminal commit' {
$targets=@(
@{Name='stop';Prior='StopRequested';Mutation='StopProcess'},
@@ -1199,6 +1383,7 @@ try {
$adapters=New-CcodEngineAdapters -State (New-CcodEngineState -Status $status) -Processes @($rootProcess) -Events $events -Counters $counters
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure()
$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure()
+ $adapters.ObserveProcessIdentity={param($ProcessId,$ExpectedCreationTimeUtc)if($alive.ContainsKey([int]$ProcessId)){[pscustomobject][ordered]@{Outcome='SameIdentity';Pid=[int]$ProcessId;CreationTimeUtc=[string]$ExpectedCreationTimeUtc}}else{[pscustomobject][ordered]@{Outcome='Absent';Pid=[int]$ProcessId;CreationTimeUtc=$null}}}.GetNewClosure()
$adapters.GetTree={param($Root,$StatusEvidence)@($rootProcess,$child)}.GetNewClosure()
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$events.Add("Stop:$($Expected.Pid)");$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
$result=Invoke-CcodRecoverSession -Request (New-CcodEngineRequest -Action Recover -RestartOrdinary $false) -Paths $paths -Adapters $adapters
@@ -1232,6 +1417,7 @@ try {
$debug=New-CcodEngineSnapshot -Mode Unrelated -RendererPort 41001 -MainPort 41002;$alive=@{100=$debug};$events=[Collections.Generic.List[string]]::new();$stops=[Collections.Generic.List[int]]::new()
$counts=[pscustomobject]@{SpecialStart=0;OrdinaryStart=0;Recover=0;Node=0};$adapters=New-CcodEngineAdapters -Processes @($debug) -Events $events -Counters $counts
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure();$adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ Set-CcodEngineAliveIdentityObserver -Adapters $adapters -Alive $alive
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$stops.Add([int]$Expected.Pid);$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure();$adapters.WaitPortClosed={param($Port,$TimeoutMilliseconds)$true}
$closed=Invoke-CcodRecoverSession -Request (New-CcodEngineRequest -Action Recover -RestartOrdinary $false) -Paths $paths -Adapters $adapters
Assert-CcodEqual 'Closed' $closed.outcome 'one exact status-less debug root is durably close-owned'
@@ -1240,6 +1426,7 @@ try {
$openDebug=New-CcodEngineSnapshot -Mode Unrelated -RendererPort 41001 -MainPort 41002;$openAlive=@{100=$openDebug};$openAdapters=New-CcodEngineAdapters -Processes @($openDebug) -Counters $counts
$openAdapters.ListProcesses={param($StatusEvidence)@($openAlive.Values)}.GetNewClosure();$openAdapters.GetProcess={param($ProcessId,$StatusEvidence)if($openAlive.ContainsKey([int]$ProcessId)){$openAlive[[int]$ProcessId]}else{$null}}.GetNewClosure();$openAdapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ Set-CcodEngineAliveIdentityObserver -Adapters $openAdapters -Alive $openAlive
$openAdapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$openAlive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure();$openAdapters.WaitPortClosed={param($Port,$TimeoutMilliseconds)$false}
$open=Invoke-CcodRecoverSession -Request (New-CcodEngineRequest -Action Recover -RestartOrdinary $false -TransactionId '9c2324b9-07a4-4ad3-9de5-c48dde73c713') -Paths $paths -Adapters $openAdapters
Assert-CcodEqual 'CCOD_CLOSE_UNPROVEN' $open.error.code 'open debug ports prevent a false Closed result'
@@ -1275,6 +1462,7 @@ try {
$special=New-CcodEngineSnapshot -Pid 201 -CreationTimeUtc '2030-02-03T04:05:07.0000000Z' -Mode Special -RendererPort 41001 -MainPort 41002
$alive=@{201=$special};$events=[Collections.Generic.List[string]]::new();$liveAdapters=New-CcodEngineAdapters -Processes @($special) -Events $events -Counters $counters
$liveAdapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$liveAdapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure()
+ Set-CcodEngineAliveIdentityObserver -Adapters $liveAdapters -Alive $alive
$liveAdapters.GetTree={param($Root,$StatusEvidence)@($Root)};$liveAdapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure();$liveAdapters.WaitPortClosed={param($Port,$TimeoutMilliseconds)$true}
$live=Invoke-CcodReplayTransition -Request $request -Paths $paths -Transition $cold -Adapters $liveAdapters
Assert-CcodEqual 'Closed' $live.outcome 'same-execution retained exact tree can prove absence and complete close'
@@ -1350,6 +1538,7 @@ try {
$adapters=New-CcodEngineAdapters -State $withOld -Processes @($ordinary) -Events $events -Counters $counters
$adapters.ReadState={param($StateRoot,$SuppressionKey)$reads.Count++;if($reads.Count -le 2){$withOld}else{$withoutOld}}.GetNewClosure()
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure();$adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ Set-CcodEngineAliveIdentityObserver -Adapters $adapters -Alive $alive
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
$request=New-CcodEngineRequest -Action Recover -RestartOrdinary $false -TransactionId '16df4637-47f5-4758-89a4-f04c7e7375cf'
$result=Invoke-CcodRecoverSession -Request $request -Paths $paths -Adapters $adapters
@@ -1390,6 +1579,7 @@ try {
$adapters.WaitPortClosed={param($Port,$TimeoutMilliseconds)$counts.Wait++;return ($counts.Wait -gt 1)}.GetNewClosure()
$adapters.InvokeNode={param($NodePath,$Arguments)$counts.Node++;New-CcodFullBridgeInvocation}.GetNewClosure()
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure();$adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ Set-CcodEngineAliveIdentityObserver -Adapters $adapters -Alive $alive
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
$baseSet=$adapters.SetTransition;$adapters.SetTransition={param($Path,$TransactionId,$ExpectedStage,$NewStage,$SpecialIdentity,$RecoveryIdentity,$RendererPort,$MainPort)if($NewStage -ceq 'RecoveryLaunchRequested'){$counts.RecoveryStages++};& $baseSet $Path $TransactionId $ExpectedStage $NewStage $SpecialIdentity $RecoveryIdentity $RendererPort $MainPort}.GetNewClosure()
$adapters.WriteStatus={param($StateRoot,$Status,$LiveProbe)if($null -ne $LiveProbe){$counts.ActiveWrites++}}
@@ -1456,6 +1646,7 @@ try {
$writtenVerified=[pscustomobject]@{Value=$null}
$adapters.InvokeNode={param($NodePath,$Arguments)[pscustomobject][ordered]@{ExitCode=0;Stdout='{}';Stderr=''}}
$adapters.ListProcesses={param($StatusEvidence)@($alive.Values)}.GetNewClosure();$adapters.GetProcess={param($ProcessId,$StatusEvidence)if($alive.ContainsKey([int]$ProcessId)){$alive[[int]$ProcessId]}else{$null}}.GetNewClosure();$adapters.GetTree={param($Root,$StatusEvidence)@($Root)}
+ Set-CcodEngineAliveIdentityObserver -Adapters $adapters -Alive $alive
$adapters.StopProcess={param($Expected,$StatusEvidence,$TimeoutMilliseconds)$alive.Remove([int]$Expected.Pid);[pscustomobject]@{Outcome='Stopped';StoppedByController=$true;Snapshot=$Expected}}.GetNewClosure()
$adapters.ReadVerified={param($StateRoot)$existingVerified}.GetNewClosure()
$adapters.WriteVerified={param($StateRoot,$Verified)$writtenVerified.Value=$Verified}.GetNewClosure()
diff --git a/tests/persistence/Supervisor.SelfTest.ps1 b/tests/persistence/Supervisor.SelfTest.ps1
index 43e79b4..fae9fd7 100644
--- a/tests/persistence/Supervisor.SelfTest.ps1
+++ b/tests/persistence/Supervisor.SelfTest.ps1
@@ -178,7 +178,15 @@ function New-CcodSupervisorFake {
$adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');$world.Decision}.GetNewClosure()
$adapters.AddObservedEvent={param($Observed,$Pid,$Created)$world.Calls.Add("Observed:$Pid");$true}.GetNewClosure()
$adapters.CompleteControllerRun={param($Result,$TransactionId,$Action,$RuntimeId)$world.Calls.Add("Reduce:$Action");[pscustomobject][ordered]@{SessionState='Idle';BlockAutomaticActions=$false;AttemptKey=$null;RecoveryIgnoreKey=$null;SuppressionKey=$null;ErrorCode=$null;Reason='Reduced'}}.GetNewClosure()
- $adapters.GetTrayPresentation={param($Arguments)$world.PresentationInputs.Add($Arguments);[pscustomobject][ordered]@{Color='Gray';ConnectionState=$Arguments.ConnectionState;ProtectionState=$Arguments.ProtectionState;RepairEnabled=$false;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true;Busy=[bool]$Arguments.Busy}}.GetNewClosure()
+ $adapters.GetTrayPresentation={
+ param($Arguments)
+ $world.PresentationInputs.Add($Arguments);$blocked=[bool]($Arguments.Busy-or$Arguments.StateDamageBlocksActions)
+ [pscustomobject][ordered]@{
+ Color='Gray';ConnectionState=$Arguments.ConnectionState;ProtectionState=$Arguments.ProtectionState
+ RepairEnabled=[bool]($Arguments.ConnectionState-ceq'RepairNeeded'-and-not$blocked);LanguageEnabled=[bool](-not$blocked);OpenLogsEnabled=$true;AboutEnabled=$true
+ ExitEnabled=[bool]($Arguments.ConnectionState-ceq'Connected'-and$Arguments.ProtectionState-ceq'Running'-and-not$blocked);Busy=[bool]$Arguments.Busy
+ }
+ }.GetNewClosure()
$adapters.NewQueue={param($Kind)$world.Calls.Add("Queue:$Kind");if($Kind -ceq 'Command'){Write-Output -NoEnumerate $world.CommandQueue}else{Write-Output -NoEnumerate $world.EventQueue}}.GetNewClosure()
$adapters.GetQueueCount={param($Queue)[int]$Queue.Count}.GetNewClosure()
$adapters.TryDequeue={param($Queue)$world.TryDequeueSawRealQueue=[object]::ReferenceEquals($Queue,$world.CommandQueue);if($Queue.Count){[pscustomobject][ordered]@{Succeeded=$true;Value=$Queue.Dequeue()}}else{[pscustomobject][ordered]@{Succeeded=$false;Value=$null}}}.GetNewClosure()
@@ -199,7 +207,17 @@ function New-CcodSupervisorFake {
$world.Calls.Add("Poll:$($Slot.Kind)")
if($Slot.Kind -ceq 'Lifecycle' -and $world.AutoCompleteLifecycleWorkers){
$world.WorkerResult=switch($Slot.Action){
- 'Apply' {[pscustomobject][ordered]@{schemaVersion=1;transactionId=$Slot.Request.transactionId;action='Apply';ok=$true;outcome='Activated';observation='Special';error=$null}}
+ 'Apply' {
+ foreach($processId in @($world.ProcessIds)){
+ if(-not$world.Snapshots.ContainsKey([int]$processId)){continue}
+ $snapshot=$world.Snapshots[[int]$processId]
+ if($snapshot.Mode-cne'Ordinary'){continue}
+ $special=$snapshot.PSObject.Copy();$special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.Snapshots[[int]$processId]=$special
+ }
+ [pscustomobject][ordered]@{schemaVersion=1;transactionId=$Slot.Request.transactionId;action='Apply';ok=$true;outcome='Activated';observation='Special';error=$null}
+ }
+ 'Inspect' {[pscustomobject][ordered]@{schemaVersion=1;transactionId=$Slot.Request.transactionId;action='Inspect';ok=$true;outcome='Inspected';observation='RemoteVerified';error=$null}}
'VerifyRemote' {[pscustomobject][ordered]@{schemaVersion=1;transactionId=$Slot.Request.transactionId;action='VerifyRemote';ok=$true;outcome='Inspected';observation='RemoteVerified';error=$null}}
default {throw "UNEXPECTED_LIFECYCLE_ACTION_$($Slot.Action)"}
}
@@ -230,7 +248,9 @@ function New-CcodTickFixture {
[void](& $fake.Adapters.EnterLifecycleOwnership $fake.Layout.InstallRoot $fake.Layout.RuntimeId ([UInt64]7) ([pscustomobject][ordered]@{pid=$fake.Identity.Pid;creationTimeUtc=$fake.Identity.CreationTimeUtc}) $fake.Identity.UserSid $fake.Identity.SessionId 5000)
$wake=& $fake.Adapters.OpenLifecycleWakeEvent $fake.Identity.UserSid $fake.Identity.SessionId
$hostState=New-CcodSupervisorHostState -Identity $fake.Identity -Layout $fake.Layout -Clock ([pscustomobject]@{Kind='Clock'}) -ShutdownEvent $shutdown -LifecycleWakeEvent $wake -CommandQueue $fake.World.CommandQueue -EventQueue $fake.World.EventQueue -State $state -Journal $null -LifecycleOwnership $fake.World.LifecycleOwnership -LifecycleRequest $fake.World.ActiveLifecycleRequest -LogonIdentity $fake.World.LogonIdentity
- $hostState.Tray=[pscustomobject]@{Kind='Tray';CurrentRevision=[UInt64]1;RenderedLanguageMode='System';RenderedCatalog=$script:TestSystemCatalog;RenderedText='old-stable'}
+ $initialPresentation=[pscustomobject][ordered]@{Color='Green';ConnectionState='Connected';ProtectionState='Running';RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true;Busy=$false}
+ $hostState.Tray=[pscustomobject]@{Kind='Tray';CurrentRevision=[UInt64]1;AcknowledgedPresentations=[ordered]@{'1'=$initialPresentation};RenderedLanguageMode='System';RenderedCatalog=$script:TestSystemCatalog;RenderedText='old-stable'}
+ $hostState.LastAcknowledgedPresentation=$initialPresentation
$hostState.UiLanguageMode='System';$hostState.UiCatalog=$script:TestSystemCatalog
[pscustomobject]@{Fake=$fake;Host=$hostState}
}
@@ -359,7 +379,7 @@ Invoke-CcodTest 'acquires both lifetime leases and signals Ready only after all
Assert-CcodTrue $fake.World.NewTraySawRealQueue 'tray receives the command queue itself'
Assert-CcodTrue $fake.World.NewWatcherSawRealQueue 'watcher receives the event queue itself'
$calls=@($fake.World.Calls)
- foreach($before in @('Enter:AccountSupervisor:5000','Enter:Supervisor:5000','Read:ActiveRuntime','Read:LogonIdentity','Enter:LifecycleOwnership:7','Open:Ready','Open:Shutdown','Open:LifecycleWake','Read:State','Read:Journal','Read:LifecycleRequest','Read:UiPreference','Get:SystemCulture','Get:UiCatalog:System','Queue:Command','Queue:Event','New:Tray','New:Watcher')){
+ foreach($before in @('Enter:AccountSupervisor:30000','Enter:Supervisor:30000','Read:ActiveRuntime','Read:LogonIdentity','Enter:LifecycleOwnership:7','Open:Ready','Open:Shutdown','Open:LifecycleWake','Read:State','Read:Journal','Read:LifecycleRequest','Read:UiPreference','Get:SystemCulture','Get:UiCatalog:System','Queue:Command','Queue:Event','New:Tray','New:Watcher')){
Assert-CcodTrue ([Array]::IndexOf($calls,$before) -ge 0) "$before occurs"
Assert-CcodTrue ([Array]::IndexOf($calls,$before) -lt [Array]::IndexOf($calls,'Signal:Ready')) "$before precedes Ready"
}
@@ -373,19 +393,19 @@ Invoke-CcodTest 'acquires both lifetime leases and signals Ready only after all
Assert-CcodTrue ([Array]::IndexOf($calls,'Exit:Supervisor') -lt [Array]::IndexOf($calls,'Exit:AccountSupervisor')) 'leases release in reverse order'
}
-Invoke-CcodTest 'uses one monotonic 5000ms acquisition budget' {
+Invoke-CcodTest 'uses one monotonic 30000ms acquisition budget' {
$fake=New-CcodSupervisorFake
$fake.World.Elapsed.Enqueue([long]0);$fake.World.Elapsed.Enqueue([long]4200)
$receipt=Invoke-CcodSupervisorHost -ReadyToken $readyToken -Adapters $fake.Adapters
Assert-CcodReceipt $receipt 'Stopped' 0
- Assert-CcodTrue ($fake.World.Calls.Contains('Enter:AccountSupervisor:5000')) 'first lease receives full budget'
- Assert-CcodTrue ($fake.World.Calls.Contains('Enter:Supervisor:800')) 'second lease receives only remainder'
+ Assert-CcodTrue ($fake.World.Calls.Contains('Enter:AccountSupervisor:30000')) 'first lease receives full budget'
+ Assert-CcodTrue ($fake.World.Calls.Contains('Enter:Supervisor:25800')) 'second lease receives only remainder'
}
Invoke-CcodTest 'does not take over or signal Ready when either lifetime lease times out' {
foreach($case in @(
- [pscustomobject]@{First='TimedOut';Second='Acquired';Expected='Enter:AccountSupervisor:5000';Forbidden='Enter:Supervisor:5000'},
- [pscustomobject]@{First='Acquired';Second='TimedOut';Expected='Enter:Supervisor:5000';Forbidden='Open:Ready'}
+ [pscustomobject]@{First='TimedOut';Second='Acquired';Expected='Enter:AccountSupervisor:30000';Forbidden='Enter:Supervisor:30000'},
+ [pscustomobject]@{First='Acquired';Second='TimedOut';Expected='Enter:Supervisor:30000';Forbidden='Open:Ready'}
)){
$fake=New-CcodSupervisorFake -FirstLeaseOutcome $case.First -SecondLeaseOutcome $case.Second
$receipt=Invoke-CcodSupervisorHost -ReadyToken $readyToken -Adapters $fake.Adapters
@@ -639,6 +659,26 @@ Invoke-CcodTest 'Exit is rejected unless the acknowledged presentation enabled i
Assert-CcodEqual 0 @($world.Calls|Where-Object { $_ -like 'New:Lifecycle:SafeExit' }).Count 'disabled presentation creates no SafeExit request'
}
+Invoke-CcodTest 'initial acknowledged presentation authorizes About before the first current projection' {
+ # Production mutation caught: requiring LastAcknowledgedPresentation even though the native rev1 handshake is already authoritative.
+ $fixture=New-CcodTickFixture;$hostState=$fixture.Host;$world=$fixture.Fake.World
+ $hostState.LastAcknowledgedPresentation=$null
+ $action=[pscustomobject][ordered]@{ActionId=[guid]::NewGuid();Command='ShowAbout';Revision=[UInt64]1}
+ $result=Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action
+ Assert-CcodEqual 'Completed' $result.Status 'manifest-verified About remains available from the acknowledged initial menu'
+ Assert-CcodEqual 1 @($world.Calls|Where-Object{$_-ceq'Verify:About'}).Count 'initial About reaches manifest verification instead of current-capability rejection'
+}
+
+Invoke-CcodTest 'initial acknowledged presentation authorizes language before the first current projection' {
+ # Production mutation caught: rejecting initial language solely because the first reconciliation tick has not projected again.
+ $fixture=New-CcodTickFixture;$hostState=$fixture.Host;$world=$fixture.Fake.World
+ $hostState.LastAcknowledgedPresentation=$null
+ $action=[pscustomobject][ordered]@{ActionId=[guid]::NewGuid();Command='SetLanguageEnglish';Revision=[UInt64]1}
+ $result=Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action
+ Assert-CcodEqual 'Completed' $result.Status 'language remains available from the acknowledged initial menu'
+ Assert-CcodEqual 1 @($world.SetUiLanguageModes|Where-Object{$_-ceq'en-US'}).Count 'initial language reaches preference persistence instead of current-capability rejection'
+}
+
Invoke-CcodTest 'adds the exact localization adapters and host fields at the frozen boundaries' {
$names=Get-CcodSupervisorAdapterNames
$first=[Array]::IndexOf($names,'ReadUiPreference')
@@ -712,6 +752,171 @@ Invoke-CcodTest 'resumes WaitingForManualLaunch after Supervisor restart and com
Assert-CcodEqual 0 @($fake.World.Calls|Where-Object{$_ -like 'Start:Controller:*'}).Count 'resume performs no mutation outside LifecycleWorker slot'
}
+Invoke-CcodTest 'supervisor restart performs one strong Inspect before adopting a persisted special session' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special;$world.AutoCompleteLifecycleWorkers=$true
+ $hostState.SpecialProof=$null;$hostState.ForceReconcile=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual $null $hostState.SpecialProof 'weak ProcessControl observation never becomes a strong proof by itself'
+ Assert-CcodTrue ($null-ne$hostState.LifecycleRequest-and$hostState.LifecycleRequest.kind-ceq'CheckAndRepair') 'restart creates one durable guardian inspection'
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 'Inspect' $hostState.LifecycleWorkerSlot.Action 'healthy persisted special runs read-only strong Inspect instead of Close'
+ Assert-CcodEqual 201 $hostState.LifecycleWorkerSlot.ProofCandidate.Pid 'strong Inspect is correlated to the exact pre-probe root'
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 1 $world.CompletedLifecycleRequests.Count 'one strong Inspect produces one terminal lifecycle receipt'
+ Assert-CcodEqual 'CancelledBeforeClose' $world.CompletedLifecycleRequests[0].phase 'healthy special inspection completes without process mutation'
+ Assert-CcodTrue ($null-ne$hostState.SpecialProof) 'strong Inspect binds the exact post-probe special identity'
+ Assert-CcodEqual 201 $hostState.SpecialProof.Pid 'strong Inspect proof names the exact special process'
+
+ $world.Calls.Clear()
+ foreach($iteration in 1..5){$hostState.ForceReconcile=$true;Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters}
+ Assert-CcodEqual 1 $world.CompletedLifecycleRequests.Count 'later guardian intervals create no additional terminal receipts'
+ Assert-CcodEqual 0 @($world.Calls|Where-Object{$_ -like 'New:Lifecycle:*'}).Count 'later guardian intervals create no lifecycle submissions'
+ Assert-CcodEqual 'AdoptSpecial' $hostState.LastDecision.Action 'later guardian intervals adopt the strong proof'
+}
+
+Invoke-CcodTest 'successful lifecycle verification binds a fresh special proof before later guardian decisions' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
+ $request=New-CcodPersistedLifecycleRequest -Kind RestartAndRepair
+ foreach($phase in @('CloseRequested','CloseConfirmed','RepairRequested')){
+ $request=& $transactionModule {param($Value,$NextPhase)Move-CcodLifecyclePhase -Request $Value -NextPhase $NextPhase -NowUtc '2030-02-03T03:01:05.0000000Z'} $request $phase
+ }
+ $request.ownerIdentity=$hostState.LifecycleOwnership.ownerIdentity;$request.leaseEpoch=[UInt64]$hostState.LifecycleOwnership.epoch
+ $world.ActiveLifecycleRequest=$request;$hostState.LifecycleRequest=$request
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special;$world.AutoCompleteLifecycleWorkers=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+
+ Start-CcodSupervisorLifecycleWorkerSlot $hostState $fixture.Fake.Adapters VerifyRemote $null|Out-Null
+ Invoke-CcodSupervisorPollLifecycleSlot $hostState $fixture.Fake.Adapters
+
+ Assert-CcodTrue ($null-ne$hostState.SpecialProof) 'successful verification binds one freshly revalidated special identity'
+ Assert-CcodEqual 201 $hostState.SpecialProof.Pid 'bound proof names the exact revalidated special process'
+ Assert-CcodEqual $true $hostState.Special[0].ProbeValid 'freshly bound proof is visible to the decision engine'
+ Assert-CcodEqual $false $hostState.SpecialNeedsInspect 'freshly bound proof suppresses redundant inspection'
+ [void](Invoke-CcodSupervisorDriveLifecycle $hostState $fixture.Fake.Adapters)
+ Assert-CcodEqual $null $hostState.LifecycleRequest 'verified lifecycle reaches one terminal completion'
+
+ $world.Calls.Clear();$hostState.ForceReconcile=$true
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 'AdoptSpecial' $hostState.LastDecision.Action 'later guardian decision adopts the bound special proof'
+ Assert-CcodEqual $null $hostState.LifecycleRequest 'later guardian decision creates no redundant repair lifecycle'
+}
+
+Invoke-CcodTest 'post-verify identity drift becomes one terminal failure and suppresses same-root guardian retries' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
+ $request=New-CcodPersistedLifecycleRequest -Kind RestartAndRepair
+ foreach($phase in @('CloseRequested','CloseConfirmed','RepairRequested')){
+ $request=& $transactionModule {param($Value,$NextPhase)Move-CcodLifecyclePhase -Request $Value -NextPhase $NextPhase -NowUtc '2030-02-03T03:01:05.0000000Z'} $request $phase
+ }
+ $request.ownerIdentity=$hostState.LifecycleOwnership.ownerIdentity;$request.leaseEpoch=[UInt64]$hostState.LifecycleOwnership.epoch
+ $world.ActiveLifecycleRequest=$request;$hostState.LifecycleRequest=$request
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special;$world.AutoCompleteLifecycleWorkers=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ Start-CcodSupervisorLifecycleWorkerSlot $hostState $fixture.Fake.Adapters VerifyRemote $null|Out-Null
+ $drift=$special.PSObject.Copy();$drift.RendererPort=[int]42001;$drift.MainPort=[int]42002;$world.Snapshots[201]=$drift
+
+ Invoke-CcodSupervisorPollLifecycleSlot $hostState $fixture.Fake.Adapters
+
+ Assert-CcodEqual 1 $world.CompletedLifecycleRequests.Count 'rebind drift produces one terminal lifecycle receipt'
+ Assert-CcodEqual 'VerificationFailed' $world.CompletedLifecycleRequests[0].phase 'rebind drift fails verification closed'
+ Assert-CcodEqual 'CCOD_REMOTE_PROOF_REBIND_FAILED' $world.CompletedLifecycleRequests[0].error 'rebind drift uses one stable terminal code'
+ Assert-CcodEqual $null $hostState.SpecialProof 'rebind drift never publishes a strong proof'
+ Assert-CcodEqual ('201|'+$special.CreationTimeUtc) $hostState.FailedSpecialProofKey 'failed proof is bounded to the exact root identity'
+ Assert-CcodEqual 'RepairNeeded' $hostState.ConnectionState 'bounded strong-proof failure remains manually repairable'
+ $world.Calls.Clear();$hostState.ForceReconcile=$true
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual ('201|'+$special.CreationTimeUtc) $hostState.FailedSpecialProofKey 'same-root failure key survives reconciliation'
+ Assert-CcodEqual $hostState.FailedSpecialProofKey (Get-CcodSupervisorSpecialProofKey $hostState.Special[0].Snapshot) 'same-root reconciled snapshot has the identical suppression key'
+ Assert-CcodEqual $false $hostState.SpecialNeedsInspect 'same-root failure remains suppressed after reconciliation'
+ Assert-CcodEqual 0 @($world.Calls|Where-Object{$_ -like 'New:Lifecycle:*'}).Count 'same-root failure creates no automatic retry loop'
+ $manual=[pscustomobject][ordered]@{ActionId=[guid]'22222222-3333-4444-5555-666666666666';Command='CheckAndRepair';Revision=[UInt64]1}
+ $manualResult=Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $manual
+ Assert-CcodEqual Accepted $manualResult.Status 'manual repair remains available after automatic suppression'
+ Assert-CcodEqual $null $hostState.FailedSpecialProofKey 'manual repair clears the same-root automatic suppression'
+}
+
+Invoke-CcodTest 'post-verify state reread failure becomes a terminal receipt instead of escaping the supervisor loop' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
+ $request=New-CcodPersistedLifecycleRequest -Kind RestartAndRepair
+ foreach($phase in @('CloseRequested','CloseConfirmed','RepairRequested')){
+ $request=& $transactionModule {param($Value,$NextPhase)Move-CcodLifecyclePhase -Request $Value -NextPhase $NextPhase -NowUtc '2030-02-03T03:01:05.0000000Z'} $request $phase
+ }
+ $request.ownerIdentity=$hostState.LifecycleOwnership.ownerIdentity;$request.leaseEpoch=[UInt64]$hostState.LifecycleOwnership.epoch
+ $world.ActiveLifecycleRequest=$request;$hostState.LifecycleRequest=$request
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special;$world.AutoCompleteLifecycleWorkers=$true
+ $reads=[pscustomobject]@{Count=0}
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$reads.Count++;if($reads.Count-eq1){return $hostState.State};throw 'injected post-worker reread failure'}.GetNewClosure()
+ Start-CcodSupervisorLifecycleWorkerSlot $hostState $fixture.Fake.Adapters VerifyRemote $null|Out-Null
+
+ Invoke-CcodSupervisorPollLifecycleSlot $hostState $fixture.Fake.Adapters
+
+ Assert-CcodEqual 1 $world.CompletedLifecycleRequests.Count 'reread failure produces one terminal lifecycle receipt'
+ Assert-CcodEqual 'VerificationFailed' $world.CompletedLifecycleRequests[0].phase 'reread failure is a verification failure'
+ Assert-CcodEqual 'CCOD_REMOTE_PROOF_REBIND_FAILED' $world.CompletedLifecycleRequests[0].error 'reread failure exposes only the stable rebind code'
+ Assert-CcodEqual $null $hostState.LifecycleRequest 'reread failure leaves no nonterminal request to restart-loop'
+}
+
+Invoke-CcodTest 'pre-verify candidate reread failure remains contained and cannot publish an unbound worker success' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
+ $request=New-CcodPersistedLifecycleRequest -Kind RestartAndRepair
+ foreach($phase in @('CloseRequested','CloseConfirmed','RepairRequested')){
+ $request=& $transactionModule {param($Value,$NextPhase)Move-CcodLifecyclePhase -Request $Value -NextPhase $NextPhase -NowUtc '2030-02-03T03:01:05.0000000Z'} $request $phase
+ }
+ $request.ownerIdentity=$hostState.LifecycleOwnership.ownerIdentity;$request.leaseEpoch=[UInt64]$hostState.LifecycleOwnership.epoch
+ $world.ActiveLifecycleRequest=$request;$hostState.LifecycleRequest=$request;$world.AutoCompleteLifecycleWorkers=$true
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special
+ $hostState.Special=[object[]]@([pscustomobject][ordered]@{Snapshot=$special;IdentityValid=$true;ProbeValid=$false});$hostState.SpecialNeedsInspect=$true
+ $reads=[pscustomobject]@{Count=0}
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$reads.Count++;if($reads.Count-le2){throw 'injected proof reread failure'};return $hostState.State}.GetNewClosure()
+
+ Start-CcodSupervisorLifecycleWorkerSlot $hostState $fixture.Fake.Adapters VerifyRemote $null|Out-Null
+ Assert-CcodEqual 201 $hostState.LifecycleWorkerSlot.ProofCandidate.Pid 'failed pre-read retains only the already-observed identity candidate'
+ Invoke-CcodSupervisorPollLifecycleSlot $hostState $fixture.Fake.Adapters
+
+ Assert-CcodEqual 1 $world.CompletedLifecycleRequests.Count 'unbound worker success produces one terminal receipt'
+ Assert-CcodEqual 'VerificationFailed' $world.CompletedLifecycleRequests[0].phase 'unbound worker success fails verification closed'
+ Assert-CcodEqual 'CCOD_REMOTE_PROOF_REBIND_FAILED' $world.CompletedLifecycleRequests[0].error 'unbound worker success uses the stable rebind code'
+ Assert-CcodEqual 'RepairNeeded' $hostState.ConnectionState 'pre-read failure remains manually repairable'
+ $world.Calls.Clear();$hostState.ForceReconcile=$true
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 0 @($world.Calls|Where-Object{$_ -like 'New:Lifecycle:*'}).Count 'pre-read failure creates no later guardian receipt loop'
+}
+
+Invoke-CcodTest 'ordinary observation uses the durable transaction start as its launch-race lower bound' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $request=New-CcodPersistedLifecycleRequest -Kind RestartAndRepair
+ $transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
+ foreach($phase in @('CloseRequested','CloseConfirmed','OrdinaryLaunchRequested')){
+ $request=& $transactionModule {param($Value,$Next)Move-CcodLifecyclePhase -Request $Value -NextPhase $Next -NowUtc '2030-02-03T03:02:00.0000000Z'} $request $phase
+ }
+ $request.ownerIdentity=$hostState.LifecycleOwnership.ownerIdentity
+ $request.leaseEpoch=[UInt64]$hostState.LifecycleOwnership.epoch
+ $world.ActiveLifecycleRequest=$request;$hostState.LifecycleRequest=$request
+ Start-CcodSupervisorLifecycleWorkerSlot $hostState $fixture.Fake.Adapters ObserveOrdinary $null|Out-Null
+ Assert-CcodEqual 1 $world.StartedLifecycleRequests.Count 'one ordinary observation worker request is persisted'
+ $workerRequest=$world.StartedLifecycleRequests[0]
+ Assert-CcodEqual $request.createdAtUtc $workerRequest.notBeforeUtc 'observation accepts an ordinary root created after the verified lifecycle began'
+ Assert-CcodTrue ($workerRequest.notBeforeUtc -cne '2030-02-03T03:04:05.0000000Z') 'observation does not move the lower bound to the current worker tick'
+}
+
Invoke-CcodTest 'rejects a nonzero lifecycle worker exit even when its result frame claims success' {
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$transactionModule=Import-Module $lifecycleTransactionPath -Force -PassThru
@@ -988,6 +1193,24 @@ Invoke-CcodTest 'converts persisted-special inspection into one durable lifecycl
Assert-CcodEqual 1 $world.CommandQueue.Count 'durable lifecycle leaves command queued'
}
+Invoke-CcodTest 'matching special proof is projected into the decision and does not schedule redundant repair' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $special.Mode='Special';$special.RendererPort=[int]41001;$special.MainPort=[int]41002
+ $world.ProcessIds=@(201);$world.Snapshots[201]=$special
+ $hostState.SpecialProof=$special;$hostState.SessionState='Active';$hostState.Reason='SpecialValidated';$hostState.ForceReconcile=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+
+ Assert-CcodEqual 1 @($hostState.Special).Count 'one matching special process remains observed'
+ Assert-CcodEqual $true $hostState.Special[0].ProbeValid 'matching durable proof is visible to the decision engine'
+ Assert-CcodEqual 'AdoptSpecial' $hostState.LastDecision.Action 'verified special process is adopted without repair'
+ Assert-CcodEqual $null $hostState.LifecycleRequest 'verified special process creates no redundant lifecycle'
+ Assert-CcodEqual 0 $world.LifecycleWrites.Count 'verified special process writes no cancellation receipt precursor'
+}
+
Invoke-CcodTest 'bounds stale-package special reconciliation to one live process identity without clearing state or keys' {
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$special=New-CcodSupervisorTestSnapshot -ProcessId 201 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
@@ -1181,6 +1404,7 @@ Invoke-CcodTest 'authorizes a correlated CheckAndRepair action through the durab
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$hostState.ConnectionState='RepairNeeded'
$hostState.Tray.CurrentRevision=[UInt64]12
+ $hostState.Tray.AcknowledgedPresentations['12']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
$action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';Command='CheckAndRepair';Revision=[UInt64]12}
$results=@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)
Assert-CcodEqual 1 $results.Count 'authorized repair returns one action result'
@@ -1197,6 +1421,7 @@ Invoke-CcodTest 'authorizes a correlated CheckAndRepair action through the durab
Invoke-CcodTest 'completes a correlated CheckAndRepair action when its lifecycle reaches a terminal phase' {
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$hostState.ConnectionState='RepairNeeded';$hostState.Tray.CurrentRevision=[UInt64]13
+ $hostState.Tray.AcknowledgedPresentations['13']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
$action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeef';Command='CheckAndRepair';Revision=[UInt64]13}
$accepted=@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)[0]
$hostState.LifecycleRequest.phase='Completed'
@@ -1208,6 +1433,71 @@ Invoke-CcodTest 'completes a correlated CheckAndRepair action when its lifecycle
Assert-CcodEqual 'Completed' $terminal.Status 'successful lifecycle sends completed action result'
}
+Invoke-CcodTest 'propagates a validated CloseFailed lifecycle error to its correlated terminal tray action' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $hostState.ConnectionState='RepairNeeded';$hostState.Tray.CurrentRevision=[UInt64]13
+ $hostState.Tray.AcknowledgedPresentations['13']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
+ $action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeece';Command='CheckAndRepair';Revision=[UInt64]13}
+ [void]@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)
+ $hostState.LifecycleRequest.phase='CloseFailed';$hostState.LifecycleRequest.error='CCOD_CLOSE_UNPROVEN'
+ Complete-CcodSupervisorLifecycleTerminal $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 2 $world.TrayActionResults.Count 'failed repair lifecycle emits accepted and terminal correlated results'
+ $terminal=$world.TrayActionResults[1]
+ Assert-CcodEqual 'Failed' $terminal.Status 'CloseFailed lifecycle sends a failed action result'
+ Assert-CcodEqual 'CCOD_CLOSE_UNPROVEN' $terminal.ErrorCode 'validated terminal lifecycle error is preserved exactly'
+}
+
+Invoke-CcodTest 'falls back for malformed terminal lifecycle errors' {
+ foreach($case in @(
+ [pscustomobject]@{Name='CRLF';Error="CCOD_CLOSE_UNPROVEN`r`n"},
+ [pscustomobject]@{Name='lowercase';Error='ccod_close_unproven'},
+ [pscustomobject]@{Name='overlength';Error=('CCOD_'+('A'*92))},
+ [pscustomobject]@{Name='terminal LF';Error="CCOD_CLOSE_UNPROVEN`n"}
+ )){
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $hostState.ConnectionState='RepairNeeded';$hostState.Tray.CurrentRevision=[UInt64]13
+ $hostState.Tray.AcknowledgedPresentations['13']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
+ $action=[pscustomobject][ordered]@{ActionId=[guid]::NewGuid();Command='CheckAndRepair';Revision=[UInt64]13}
+ [void]@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)
+ $hostState.LifecycleRequest.phase='CloseFailed';$hostState.LifecycleRequest.error=$case.Error
+ Complete-CcodSupervisorLifecycleTerminal $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 2 $world.TrayActionResults.Count "$($case.Name) failure emits accepted and terminal correlated results"
+ Assert-CcodEqual 'CCOD_LIFECYCLE_ACTION_FAILED' $world.TrayActionResults[1].ErrorCode "$($case.Name) malformed terminal error retains the generic fallback"
+ }
+}
+
+Invoke-CcodTest 'authorizes an acknowledged displayed revision after a newer capable presentation is published' {
+ $fixture=New-CcodTickFixture;$hostState=$fixture.Host
+ $hostState.ConnectionState='RepairNeeded';$hostState.Tray.CurrentRevision=[UInt64]8
+ $hostState.Tray.AcknowledgedPresentations['7']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
+ $hostState.LastAcknowledgedPresentation=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
+ $action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee07';Command='CheckAndRepair';Revision=[UInt64]7}
+ $result=@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)[0]
+ Assert-CcodEqual 'Accepted' $result.Status 'the displayed and acknowledged revision remains action authority after a newer capable projection'
+}
+
+Invoke-CcodTest 'rejects an action revision that was published but never acknowledged as displayed' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $hostState.Tray.CurrentRevision=[UInt64]9
+ $action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee09';Command='OpenLogs';Revision=[UInt64]9}
+ $result=@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)[0]
+ Assert-CcodEqual 'Rejected' $result.Status 'unacknowledged presentation revision is rejected'
+ Assert-CcodEqual 'CCOD_TRAY_ACTION_STALE' $result.ErrorCode 'unacknowledged presentation revision has the stable stale error'
+ Assert-CcodEqual 0 @($world.Calls|Where-Object{$_ -eq 'Open:Logs'}).Count 'unacknowledged action performs no side effect'
+}
+
+Invoke-CcodTest 'rejects an old displayed action after the current presentation revokes its capability' {
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $hostState.Tray.CurrentRevision=[UInt64]8
+ $hostState.Tray.AcknowledgedPresentations['7']=[pscustomobject][ordered]@{RepairEnabled=$true;LanguageEnabled=$true;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$true}
+ $hostState.LastAcknowledgedPresentation=[pscustomobject][ordered]@{RepairEnabled=$false;LanguageEnabled=$false;OpenLogsEnabled=$true;AboutEnabled=$true;ExitEnabled=$false}
+ $action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee17';Command='SetLanguageEnglish';Revision=[UInt64]7}
+ $result=@(Invoke-CcodSupervisorCommand $hostState $fixture.Fake.Adapters $action)[0]
+ Assert-CcodEqual 'Rejected' $result.Status 'current capability revocation fails closed'
+ Assert-CcodEqual 'CCOD_TRAY_ACTION_UNAVAILABLE' $result.ErrorCode 'current capability revocation is unavailable rather than stale'
+ Assert-CcodEqual 0 @($world.Calls|Where-Object{$_ -like 'Persist:UiLanguage:*'}).Count 'revoked action performs no preference mutation'
+}
+
Invoke-CcodTest 'completes About only after validating the active runtime manifest' {
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$action=[pscustomobject][ordered]@{ActionId=[guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeea';Command='ShowAbout';Revision=[UInt64]1}
@@ -1322,6 +1612,118 @@ Invoke-CcodTest 'worker request ids are canonical D-format GUIDs for probe frami
Assert-CcodTrue ($slot.RequestPath.EndsWith("static-probe-$($slot.RequestId).request.json")) 'request path matches StaticProbeWorker framing'
}
+Invoke-CcodTest 'transient static target failure remains retryable for a new ordinary root' {
+ # Production mutation caught: poisoning the Supervisor lifetime as UnknownOrIncompatible after one startup PID race.
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $first=New-CcodSupervisorTestSnapshot -ProcessId 71 -CreationTimeUtc '2030-02-03T03:01:00.0000000Z'
+ $slot=Start-CcodSupervisorWorkerSlot $hostState $fixture.Fake.Adapters 'StaticProbe' 'StaticProbe' $first
+ $world.WorkerResult=[pscustomobject][ordered]@{
+ schemaVersion=1;action='StaticProbe';ok=$false;requestId=$slot.Request.requestId;runtimeId=$slot.Request.runtimeId
+ targetIdentity=[pscustomobject][ordered]@{pid=$slot.Request.targetIdentity.pid;creationTimeUtc=$slot.Request.targetIdentity.creationTimeUtc}
+ probe=$null;error=[pscustomobject][ordered]@{code='CCOD_STATIC_TARGET_CHANGED';stage='TargetPostflight';message='The static probe worker failed safely.'}
+ }
+ $stdout=$world.WorkerResult|ConvertTo-Json -Depth 20 -Compress
+ $world.Poll=[pscustomobject][ordered]@{Completed=$true;ExitCode=[int]1;StdoutText=$stdout;StdoutByteCount=[int]$stdout.Length;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}
+ Invoke-CcodSupervisorPollSlot $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual $null $hostState.Classification 'a correlated transient probe failure does not become a permanent compatibility verdict'
+
+ $second=New-CcodSupervisorTestSnapshot -ProcessId 72 -CreationTimeUtc '2030-02-03T03:02:00.0000000Z'
+ $world.ProcessIds=@([int]$second.Pid);$world.Snapshots[[int]$second.Pid]=$second
+ $hostState.ForceReconcile=$true;$hostState.ObservationDirty=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodTrue ($null-ne$hostState.WorkerSlot -and $hostState.WorkerSlot.Kind-ceq'StaticProbe') 'a new stable ordinary root is probed again'
+ Assert-CcodEqual 72 $hostState.WorkerSlot.Request.targetIdentity.pid 'retry is bound to the replacement ordinary root'
+}
+
+Invoke-CcodTest 'valid nonretryable static failure is bound to one ordinary root without breaking the next tick' {
+ # Production mutation caught: setting global UnknownOrIncompatible without the package tuple required by the decision context.
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $target=New-CcodSupervisorTestSnapshot -ProcessId 73 -CreationTimeUtc '2030-02-03T03:03:00.0000000Z'
+ $slot=Start-CcodSupervisorWorkerSlot $hostState $fixture.Fake.Adapters 'StaticProbe' 'StaticProbe' $target
+ $world.WorkerResult=[pscustomobject][ordered]@{
+ schemaVersion=1;action='StaticProbe';ok=$false;requestId=$slot.Request.requestId;runtimeId=$slot.Request.runtimeId
+ targetIdentity=[pscustomobject][ordered]@{pid=$slot.Request.targetIdentity.pid;creationTimeUtc=$slot.Request.targetIdentity.creationTimeUtc}
+ probe=$null;error=[pscustomobject][ordered]@{code='CCOD_STATIC_RUNTIME_UNAUTHORIZED';stage='RuntimePostflight';message='The static probe worker failed safely.'}
+ }
+ $stdout=$world.WorkerResult|ConvertTo-Json -Depth 20 -Compress
+ $world.Poll=[pscustomobject][ordered]@{Completed=$true;ExitCode=[int]1;StdoutText=$stdout;StdoutByteCount=[int]$stdout.Length;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}
+ Invoke-CcodSupervisorPollSlot $hostState $fixture.Fake.Adapters
+ $attemptKey='73|2030-02-03T03:03:00.0000000Z'
+ Assert-CcodEqual $null $hostState.Classification 'failure without a trusted package tuple cannot set a global compatibility verdict'
+ Assert-CcodTrue $hostState.AttemptKeys.Contains($attemptKey) 'nonretryable failure is bound to the exact ordinary identity'
+ Assert-CcodEqual 'StaticProbeFailed' $hostState.Reason 'nonretryable failure exposes one fixed reason'
+ Assert-CcodEqual $true $hostState.BlockAutomaticActions 'nonretryable failure blocks automatic mutation for that root'
+
+ $world.ProcessIds=@([int]$target.Pid);$world.Snapshots[[int]$target.Pid]=$target
+ $hostState.ForceReconcile=$true;$hostState.ObservationDirty=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 'KeepOrdinary' $hostState.LastDecision.Action 'same failed ordinary root remains fail-closed without terminating Supervisor'
+ Assert-CcodEqual 'AlreadyAttempted' $hostState.LastDecision.Reason 'same failed ordinary root is not probed in an unbounded loop'
+ Assert-CcodEqual $null $hostState.WorkerSlot 'same failed ordinary root starts no second worker'
+}
+
+Invoke-CcodTest 'malformed static failure frames remain fail-closed' {
+ $cases=@(
+ [pscustomobject]@{Name='wrong request';ExitCode=1;Mutate={param($Result)$Result.requestId=[guid]::NewGuid().ToString('D')}},
+ [pscustomobject]@{Name='invalid stage';ExitCode=1;Mutate={param($Result)$Result.error.stage='PostProbe'}},
+ [pscustomobject]@{Name='unbounded message';ExitCode=1;Mutate={param($Result)$Result.error.message='private target path'}},
+ [pscustomobject]@{Name='contradictory exit';ExitCode=0;Mutate={param($Result)}}
+ )
+ foreach($case in $cases){
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $target=New-CcodSupervisorTestSnapshot
+ $slot=Start-CcodSupervisorWorkerSlot $hostState $fixture.Fake.Adapters 'StaticProbe' 'StaticProbe' $target
+ $result=[pscustomobject][ordered]@{
+ schemaVersion=1;action='StaticProbe';ok=$false;requestId=$slot.Request.requestId;runtimeId=$slot.Request.runtimeId
+ targetIdentity=[pscustomobject][ordered]@{pid=$slot.Request.targetIdentity.pid;creationTimeUtc=$slot.Request.targetIdentity.creationTimeUtc}
+ probe=$null;error=[pscustomobject][ordered]@{code='CCOD_STATIC_TARGET_CHANGED';stage='TargetPostflight';message='The static probe worker failed safely.'}
+ }
+ & $case.Mutate $result
+ $world.WorkerResult=$result;$stdout=$result|ConvertTo-Json -Depth 20 -Compress
+ $world.Poll=[pscustomobject][ordered]@{Completed=$true;ExitCode=[int]$case.ExitCode;StdoutText=$stdout;StdoutByteCount=[int]$stdout.Length;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}
+ Invoke-CcodSupervisorPollSlot $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual $null $hostState.Classification "$($case.Name) cannot create a package verdict without trusted evidence"
+ Assert-CcodTrue $hostState.AttemptKeys.Contains('71|2030-02-03T03:01:00.0000000Z') "$($case.Name) is bound to the exact failed ordinary root"
+ Assert-CcodEqual 'WorkerFramingFailed' $hostState.Reason "$($case.Name) is reduced to the fixed framing failure"
+ Assert-CcodEqual $true $hostState.BlockAutomaticActions "$($case.Name) blocks automatic mutation"
+ }
+}
+
+Invoke-CcodTest 'invalid or empty completed static poll remains fail-closed for the same root' {
+ $cases=@(
+ [pscustomobject]@{Name='invalid poll';Poll=[pscustomobject][ordered]@{Completed=$true;StdoutText='';StdoutByteCount=[int]0;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}},
+ [pscustomobject]@{Name='empty completed poll';Poll=[pscustomobject][ordered]@{Completed=$true;ExitCode=[int]1;StdoutText='';StdoutByteCount=[int]0;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}}
+ )
+ $processId=73
+ foreach($case in $cases){
+ $processId++
+ $fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
+ $created=('2030-02-03T03:{0}:00.0000000Z'-f($processId-70).ToString('00'))
+ $target=New-CcodSupervisorTestSnapshot -ProcessId $processId -CreationTimeUtc $created
+ Start-CcodSupervisorWorkerSlot $hostState $fixture.Fake.Adapters 'StaticProbe' 'StaticProbe' $target|Out-Null
+ $world.Poll=$case.Poll
+ Invoke-CcodSupervisorPollSlot $hostState $fixture.Fake.Adapters
+ $attemptKey=('{0}|{1}'-f$processId,$created)
+ Assert-CcodEqual $null $hostState.Classification "$($case.Name) creates no untrusted package verdict"
+ Assert-CcodTrue $hostState.AttemptKeys.Contains($attemptKey) "$($case.Name) is bound to the exact failed ordinary root"
+ Assert-CcodEqual 'WorkerFramingFailed' $hostState.Reason "$($case.Name) has one fixed failure reason"
+ Assert-CcodEqual $true $hostState.BlockAutomaticActions "$($case.Name) blocks automatic mutation"
+ Assert-CcodEqual $null $hostState.WorkerSlot "$($case.Name) clears the completed worker slot"
+
+ $world.ProcessIds=@([int]$target.Pid);$world.Snapshots[[int]$target.Pid]=$target
+ $hostState.ForceReconcile=$true;$hostState.ObservationDirty=$true
+ $fixture.Fake.Adapters.ReadState={param($StateRoot,$SuppressionKey)$hostState.State}.GetNewClosure()
+ $fixture.Fake.Adapters.GetSupervisorDecision={param($Context)$world.Calls.Add('Decision');Get-CcodSupervisorDecision -Context $Context}.GetNewClosure()
+ Invoke-CcodSupervisorTick $hostState $fixture.Fake.Adapters
+ Assert-CcodEqual 'KeepOrdinary' $hostState.LastDecision.Action "$($case.Name) keeps the same root fail-closed"
+ Assert-CcodEqual 'AlreadyAttempted' $hostState.LastDecision.Reason "$($case.Name) starts no unbounded retry loop"
+ }
+}
+
Invoke-CcodTest 'fails closed when a static worker fabricates a ready candidate without sentinel proof' {
$fixture=New-CcodTickFixture;$world=$fixture.Fake.World;$hostState=$fixture.Host
$target=New-CcodSupervisorTestSnapshot
@@ -1338,7 +1740,8 @@ Invoke-CcodTest 'fails closed when a static worker fabricates a ready candidate
$stdout=$world.WorkerResult|ConvertTo-Json -Depth 20 -Compress
$world.Poll=[pscustomobject][ordered]@{Completed=$true;ExitCode=[int]0;StdoutText=$stdout;StdoutByteCount=[int]$stdout.Length;StdoutOverflow=$false;StderrByteCount=[int]0;StderrOverflow=$false}
Invoke-CcodSupervisorPollSlot $hostState $fixture.Fake.Adapters
- Assert-CcodEqual 'UnknownOrIncompatible' $hostState.Classification 'incomplete public proof cannot update the candidate classification'
+ Assert-CcodEqual $null $hostState.Classification 'incomplete public proof cannot update the candidate classification'
+ Assert-CcodTrue $hostState.AttemptKeys.Contains('71|2030-02-03T03:01:00.0000000Z') 'malformed success is bound to the exact failed ordinary root'
Assert-CcodEqual 'WorkerFramingFailed' $hostState.Reason 'malformed public proof is recorded as a framing failure'
Assert-CcodEqual $true $hostState.BlockAutomaticActions 'malformed public proof blocks automatic actions'
}
diff --git a/tests/persistence/TrayHostBuild.SelfTest.ps1 b/tests/persistence/TrayHostBuild.SelfTest.ps1
index fe95a6f..2e9153e 100644
--- a/tests/persistence/TrayHostBuild.SelfTest.ps1
+++ b/tests/persistence/TrayHostBuild.SelfTest.ps1
@@ -4,14 +4,19 @@ $ErrorActionPreference='Stop'
$repositoryRoot=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$lockPath=Join-Path $repositoryRoot 'build\trayhost-packages.lock.json'
-Invoke-CcodTest '2.5.9 source metadata binds the package and TrayHost assembly identities' {
+Invoke-CcodTest '2.5.21 source metadata binds the package and native assembly identities' {
$package=Get-Content -LiteralPath (Join-Path $repositoryRoot 'package.json') -Raw|ConvertFrom-Json
- Assert-CcodEqual '2.5.9' ([string]$package.version) 'package version is the 2.5.9 release version'
+ Assert-CcodEqual '2.5.21' ([string]$package.version) 'package version is the 2.5.21 release version'
$assemblyInfo=Get-Content -LiteralPath (Join-Path $repositoryRoot 'src\trayhost\AssemblyInfo.cs') -Raw
- Assert-CcodTrue ($assemblyInfo -match 'AssemblyVersion\("2\.5\.9\.0"\)') 'TrayHost assembly version is 2.5.9.0'
- Assert-CcodTrue ($assemblyInfo -match 'AssemblyFileVersion\("2\.5\.9\.0"\)') 'TrayHost file version is 2.5.9.0'
+ Assert-CcodTrue ($assemblyInfo -match 'AssemblyVersion\("2\.5\.21\.0"\)') 'TrayHost assembly version is 2.5.21.0'
+ Assert-CcodTrue ($assemblyInfo -match 'AssemblyFileVersion\("2\.5\.21\.0"\)') 'TrayHost file version is 2.5.21.0'
$manifest=Get-Content -LiteralPath (Join-Path $repositoryRoot 'src\trayhost\CodexRemote.TrayHost.manifest') -Raw
- Assert-CcodTrue ($manifest -match '
0, "real GetMessage receives the posted owner work");
+ Win32TrayPlatform.TranslateAndDispatch(message);
+ AssertTrue(observed == workMessage, "dispatch preserves the target HWND so posted presentation work reaches the owner WndProc");
+ }
+ finally
+ {
+ if (owner != IntPtr.Zero) { platform.DestroyOwner(owner); }
+ }
+ }
+
public static int Main(string[] args)
{
try
@@ -218,14 +281,18 @@ public static int Main(string[] args)
TestForegroundFailureFallsBackToNativeMenu();
TestOwnerShowFailureNeverTracks();
TestReentryAndPendingSnapshot();
+ TestStalePresentationIsNotReportedAsDisplayed();
+ TestMenuRevisionGateHoldsNewPresentationUntilTheDisplayedActionIsSelected();
TestSelectedCommandAndTaskbarRestore();
TestAboutCommandDefersProofToSupervisor();
TestVerifiedAboutUsesTheAcknowledgedSnapshotVersion();
+ TestActionFailureUsesTheAcknowledgedSnapshotStrings();
TestSimplifiedMenuAndExitConfirmation();
TestNoHimcFailureIsSafe();
TestShellRightClickNotificationMapping();
TestRealNativePInvokeSurface();
- Console.WriteLine("TrayHost native self-tests passed: 5");
+ TestPostedWorkMessageDispatchesToItsOwnerWindow();
+ Console.WriteLine("TrayHost native self-tests passed: 15");
return 0;
}
catch (Exception error)
diff --git a/tests/trayhost/TrayHostTransportSelfTest.cs b/tests/trayhost/TrayHostTransportSelfTest.cs
index ccd2531..63d0c7d 100644
--- a/tests/trayhost/TrayHostTransportSelfTest.cs
+++ b/tests/trayhost/TrayHostTransportSelfTest.cs
@@ -51,17 +51,17 @@ private static void TestBrokenPipeAndStderrCap()
private static void TestHostPendingAndReplayBound()
{
- HostTransport transport = new HostTransport();
+ int releasedPresentationSignals = 0;
+ HostTransport transport = new HostTransport(delegate { releasedPresentationSignals++; });
transport.SetMenuOpen(true);
AssertTrue(transport.TryAcceptPresentation(Snapshot(1)), "host accepts first presentation");
AssertTrue(transport.TryAcceptPresentation(Snapshot(2)), "host coalesces a newer presentation while menu is open");
PresentationSnapshot ignored;
AssertTrue(!transport.TryTakePresentation(out ignored), "menu-open presentation is not applied early");
transport.SetMenuOpen(false);
+ AssertTrue(releasedPresentationSignals == 1, "menu close re-signals pending presentation work consumed by the nested menu loop");
PresentationSnapshot newest;
AssertTrue(transport.TryTakePresentation(out newest) && newest.Revision == 2UL, "menu close applies newest snapshot only");
- TrayHostControl control;
- AssertTrue(transport.TryDequeueControl(out control) && control.Kind == TrayHostControlKind.PresentationAck && control.Revision == 2UL, "one ack is emitted for the applied revision");
Guid actionId = Guid.NewGuid();
AssertTrue(transport.TryRegisterAction(new TrayHostAction(actionId, TrayCommand.OpenLogs, 2UL)), "first action accepted");
AssertTrue(!transport.TryRegisterAction(new TrayHostAction(actionId, TrayCommand.OpenLogs, 2UL)), "replayed action id is rejected");
@@ -76,6 +76,18 @@ private static void TestHostPendingAndReplayBound()
transport.Dispose();
}
+ private static void TestDirectPresentationAcksDoNotAccumulateAnUnusedControlQueue()
+ {
+ HostTransport transport = new HostTransport();
+ for (ulong revision = 1UL; revision <= 40UL; revision++)
+ {
+ AssertTrue(transport.TryAcceptPresentation(Snapshot(revision)), "production presentation is accepted without a side-channel ACK queue");
+ PresentationSnapshot applied;
+ AssertTrue(transport.TryTakePresentation(out applied) && applied.Revision == revision, "production presentation remains available for the direct wire ACK path");
+ }
+ transport.Dispose();
+ }
+
private static void TestActionResultCorrelationAndControlPriority()
{
Guid actionId = Guid.NewGuid(); Guid transactionId = Guid.Parse("11111111-2222-3333-4444-555555555555");
@@ -121,6 +133,34 @@ private static void TestAcknowledgedAboutQueuesOneUiWorkItem()
host.Dispose();
}
+ private static void TestRejectedAndFailedActionsQueueUserFeedback()
+ {
+ HostTransport host = new HostTransport();
+ Guid rejectedId = Guid.NewGuid(); Guid failedId = Guid.NewGuid();
+ AssertTrue(host.TryRegisterAction(new TrayHostAction(rejectedId, TrayCommand.OpenLogs, 20UL)), "rejected action registers");
+ AssertTrue(host.TryRegisterAction(new TrayHostAction(failedId, TrayCommand.SetLanguageEnglish, 20UL)), "failed action registers");
+ AssertTrue(host.TryAcknowledgeAction(new TrayActionResult(rejectedId, 20UL, TrayActionResultStatus.Rejected, "CCOD_TRAY_ACTION_UNAVAILABLE", null)), "correlated rejected result is accepted");
+ AssertTrue(host.TryAcknowledgeAction(new TrayActionResult(failedId, 20UL, TrayActionResultStatus.Failed, "CCOD_TRAY_ACTION_FAILED", null)), "correlated failed result is accepted");
+ TrayActionResult first; TrayActionResult second; TrayActionResult none;
+ AssertTrue(host.TryTakeFailedAction(out first) && first.ActionId == rejectedId && first.Status == TrayActionResultStatus.Rejected, "rejected result queues user feedback");
+ AssertTrue(host.TryTakeFailedAction(out second) && second.ActionId == failedId && second.Status == TrayActionResultStatus.Failed, "failed result queues user feedback");
+ AssertTrue(!host.TryTakeFailedAction(out none), "each terminal failure queues feedback exactly once");
+ host.Dispose();
+ }
+
+ private static void TestUndisplayedActionFailureFeedbackIsBounded()
+ {
+ HostTransport host = new HostTransport();
+ for (int index = 0; index < 9; index++)
+ {
+ Guid actionId = Guid.NewGuid();
+ AssertTrue(host.TryRegisterAction(new TrayHostAction(actionId, TrayCommand.OpenLogs, 21UL)), "terminal failure releases pending action capacity");
+ bool accepted = host.TryAcknowledgeAction(new TrayActionResult(actionId, 21UL, TrayActionResultStatus.Failed, "CCOD_TRAY_ACTION_FAILED", null));
+ AssertTrue(index < 8 ? accepted : !accepted, "undisplayed action failure feedback is bounded to the pending-action capacity");
+ }
+ host.Dispose();
+ }
+
public static int Main(string[] args)
{
try
@@ -128,9 +168,12 @@ public static int Main(string[] args)
TestParentLatestAndReservedControl();
TestBrokenPipeAndStderrCap();
TestHostPendingAndReplayBound();
+ TestDirectPresentationAcksDoNotAccumulateAnUnusedControlQueue();
TestActionResultCorrelationAndControlPriority();
TestAcknowledgedAboutQueuesOneUiWorkItem();
- Console.WriteLine("TrayHost transport self-tests passed: 5");
+ TestRejectedAndFailedActionsQueueUserFeedback();
+ TestUndisplayedActionFailureFeedbackIsBounded();
+ Console.WriteLine("TrayHost transport self-tests passed: 8");
return 0;
}
catch (Exception error)