From c1bbf200c96988e89912be6654061d5bc2b22c99 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Wed, 28 Oct 2026 09:50:32 -0700 Subject: [PATCH 1/4] Add canonical focused UI capture mode Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/Invoke-UiCapture.ps1 | 142 +++++++++-- Tools/windows/Invoke-UiCaptureWorker.ps1 | 143 +++++++++-- Tools/windows/Prepare-FocusedUiCapture.ps1 | 197 +++++++++++++++ Tools/windows/Tests/UiCaptureDriver.Tests.ps1 | 235 +++++++++++++++++- Tools/windows/UI-CAPTURE-SCENARIOS.md | 46 +++- Tools/windows/UiCaptureDriver.psm1 | 222 ++++++++++++++++- 6 files changed, 928 insertions(+), 57 deletions(-) create mode 100644 Tools/windows/Prepare-FocusedUiCapture.ps1 diff --git a/Tools/windows/Invoke-UiCapture.ps1 b/Tools/windows/Invoke-UiCapture.ps1 index eb21fc7c..8071c462 100644 --- a/Tools/windows/Invoke-UiCapture.ps1 +++ b/Tools/windows/Invoke-UiCapture.ps1 @@ -4,12 +4,14 @@ param( [Parameter(Mandatory)][string]$JobPath, [Parameter()][string]$JobSha256, [Parameter()][ValidatePattern('^[A-Fa-f0-9]{64}$')][string]$FixtureMappingSha256, - [ValidateSet('LIVE_CAPTURE', 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL', 'LIVE_TRANSITION_REHEARSAL')] + [ValidateSet('LIVE_CAPTURE','FOCUSED_LIVE_CAPTURE','NO_DESKTOP_REAL_LAUNCH_REHEARSAL','LIVE_TRANSITION_REHEARSAL')] [string]$ExecutionMode = 'LIVE_CAPTURE', [Parameter()][string]$LeaseReceiptPath, [Parameter()][string]$LeaseReceiptSha256, [Parameter()][string]$RehearsalAuthorizationPath, [Parameter()][string]$RehearsalAuthorizationSha256, + [Parameter()][string]$FocusedAuthorizationPath, + [Parameter()][string]$FocusedAuthorizationSha256, [Parameter()][string]$PreflightReceiptPath, [Parameter()][string]$PreflightReceiptSha256, [switch]$TimeoutProbe @@ -23,6 +25,8 @@ $modulePath = Join-Path $PSScriptRoot 'UiCaptureDriver.psm1' $workerPath = Join-Path $PSScriptRoot 'Invoke-UiCaptureWorker.ps1' $rehearsal = $ExecutionMode -ceq 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL' $transitionRehearsal = $ExecutionMode -ceq 'LIVE_TRANSITION_REHEARSAL' +$focusedCapture = $ExecutionMode -ceq 'FOCUSED_LIVE_CAPTURE' +$matchedCapturePlan = $null $fixtureMappingHashExpected = if ($FixtureMappingSha256) { $FixtureMappingSha256.ToUpperInvariant() } else { $null } $jobIdentity = $null @@ -483,6 +487,58 @@ function Assert-CapPreflightReport { function Assert-CapLease { if (Get-Command Write-CapTrace -ErrorAction SilentlyContinue) { Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters } + if ($focusedCapture) { + if (-not $FocusedAuthorizationPath -or -not $FocusedAuthorizationSha256) { + throw 'Focused live capture requires an explicit exact-source authorization.' + } + $leasePath=Assert-CapSupervisorPath $FocusedAuthorizationPath + if (-not (Test-Path -LiteralPath $leasePath -PathType Leaf)) { + throw "Focused authorization is missing: $leasePath" + } + $leaseHash=(Get-FileHash -Algorithm SHA256 -LiteralPath $leasePath).Hash + if ($leaseHash -ne $FocusedAuthorizationSha256.ToUpperInvariant()) { + throw 'Focused authorization hash mismatch.' + } + $lease=Read-CapBoundJson -Root $root -Reference ([pscustomobject]@{path=$leasePath;sha256=$leaseHash}) + Assert-CapDataRecord $lease @{schemaVersion=[long];authorizationId=[string];status=[string] + authorizedAction=[string];evidenceClassification=[string];officialParityEvidence=[bool] + desktopAuthorized=[bool];captureEvidenceAuthorized=[bool];pngAuthorized=[bool] + lane=[string];head=[string];sourceTree=[string];jobManifestSha256=[string] + moduleSha256=[string];workerSha256=[string];supervisorSha256=[string] + preflightReceiptSha256=[string];fixtureMappingSha256=[string] + beforeBinarySha256=[string];afterBinarySha256=[string];sceneNames=[array];phases=[array] + grantedUtc=[string];expiresUtc=[string];maxMinutes=[long]} 'Focused authorization' + $plan=Get-CapMatchedCapturePlan -Job $job + if ($lease.schemaVersion -ne 1 -or $lease.status -cne 'AUTHORIZED' -or + $lease.authorizedAction -cne 'focused-matched-capture' -or + $lease.evidenceClassification -cne $plan.evidenceClassification -or + $lease.officialParityEvidence -cne $false -or -not $lease.desktopAuthorized -or + -not $lease.captureEvidenceAuthorized -or -not $lease.pngAuthorized -or + $lease.lane -cne $job.lane -or $lease.head -cne $job.head -or + $lease.sourceTree -cne $job.captureTree -or $lease.jobManifestSha256 -ne $jobHash -or + $lease.moduleSha256 -ne $moduleHash -or $lease.workerSha256 -ne $workerHash -or + $lease.supervisorSha256 -ne $supervisorHash -or + $lease.preflightReceiptSha256 -ne $preflightHash -or + $lease.fixtureMappingSha256 -ne $fixtureMappingHashExpected -or + ($lease.sceneNames -join ',') -cne ($plan.sceneNames -join ',') -or + ($lease.phases -join ',') -cne ($plan.phases -join ',')) { + throw 'Focused authorization does not bind the exact current job, controls, preflight and scene plan.' + } + $null=[guid]::Parse([string]$lease.authorizationId) + $script:authorityId=$lease.authorizationId + $script:authorityHash=$leaseHash + $script:authorityPath=$leasePath + $leaseWindow=Get-CapLeaseWindow -GrantedUtc $lease.grantedUtc -ExpiresUtc $lease.expiresUtc ` + -MaximumMinutes ([double]$lease.maxMinutes) -NowUtc ([DateTime]::UtcNow) + $script:leaseExpiresUtc=$leaseWindow.ExpiresUtc + $script:deadlineUtc=$leaseWindow.WorkerDeadlineUtc + if ($script:deadlineUtc -le $leaseWindow.NowUtc.AddMinutes(1)) { + throw 'Focused authorization has insufficient remaining time after the reserved 90-second cleanup margin.' + } + $script:lease=$lease + $script:leaseHash=$leaseHash + return $leasePath + } if ($transitionRehearsal) { if (-not $RehearsalAuthorizationPath -or -not $RehearsalAuthorizationSha256) { throw 'Live transition rehearsal requires an explicit non-capture rehearsal authorization.' @@ -842,10 +898,15 @@ function Assert-CapNoTrackedSurvivors { function Assert-CapMatchedWorkerManifest { Assert-CapMatchedManifest -Manifest $manifest -Job $job + $plan=Get-CapMatchedCapturePlan -Job $job if ($manifest.head -cne $job.head -or $manifest.base -cne $job.base -or $manifest.moduleSha256 -ne $moduleHash -or $manifest.workerSha256 -ne $workerHash -or $manifest.supervisorSha256 -ne $supervisorHash -or $manifest.jobManifestSha256 -ne $jobHash -or - $manifest.leaseReceiptSha256 -ne $leaseHash -or $manifest.preflightReceiptSha256 -ne $preflightHash -or + $(if ($focusedCapture) { + $manifest.focusedAuthorizationSha256 -ne $leaseHash + } else { + $manifest.leaseReceiptSha256 -ne $leaseHash + }) -or $manifest.preflightReceiptSha256 -ne $preflightHash -or $manifest.fixtureSourceSha256 -ne $job.fixture.sha256 -or $manifest.fixtureSha256 -ne $fixtureHashExpected -or $manifest.fixtureMappingSha256 -ne $fixtureMappingHashExpected -or $manifest.failures.Count -ne 0) { throw 'Matched manifest provenance differs from its lease-bound job.' @@ -854,7 +915,9 @@ function Assert-CapMatchedWorkerManifest { if ($manifest.$field -isnot [array] -or $manifest.$field.Count -ne 0) {throw "Matched manifest has unproved $field."} } $isDpi=Test-CapMatchedDpiAdapter - if ($isDpi -and ($manifest.evidenceClassification -cne 'C8R_RDP_DPI192_QUALIFIED' -or + if ($isDpi -and ($manifest.evidenceClassification -cne $plan.evidenceClassification -or + ($null -ne $plan.officialParityEvidence -and + $manifest.officialParityEvidence -cne $plan.officialParityEvidence) -or $manifest.geometrySchemaVersion -ne 3 -or ($manifest.logicalClientWH -join ',') -cne ($job.captureContract.logicalClientWH -join ',') -or $manifest.expectedWindowDpi -ne $job.captureContract.expectedWindowDpi -or @@ -873,7 +936,8 @@ function Assert-CapMatchedWorkerManifest { $image.height -ne ($image.outerBounds[3]-$image.outerBounds[1])) {throw 'Matched image geometry, state or provenance differs.'} if ($isDpi) { $contract=$job.captureContract - $null=Assert-CapMatchedDpiImage -Image $image -Contract $contract + $null=Assert-CapMatchedDpiImage -Image $image -Contract $contract ` + -ExpectedEvidenceClassification $plan.evidenceClassification } elseif (($image.clientBounds -join ',') -cne '0,0,1440,900' -or $image.dpi -ne 96) { throw 'Matched V1 image geometry differs.' } @@ -882,9 +946,12 @@ function Assert-CapMatchedWorkerManifest { $path=Assert-CapSupervisorPath $image.path if ((Get-FileHash $path -Algorithm SHA256).Hash -ne $image.sha256) {throw 'Matched image bytes changed.'} } - $expectedSeeds=@(foreach ($phase in @('before','after')) {foreach ($scene in (Get-CapMatchedSceneNames)[6..16]) {"$phase-$scene"}}) + $expectedSeeds=@(foreach ($phase in $plan.phases) { + foreach ($scene in @($plan.sceneNames | Where-Object {$_ -cmatch '^s-tabs-'})) {"$phase-$scene"} + }) $actualSeeds=@($manifest.zmxSeeds | ForEach-Object phase) - if ($actualSeeds.Count -ne 22 -or @($actualSeeds | Sort-Object -Unique).Count -ne 22 -or + if ($actualSeeds.Count -ne $expectedSeeds.Count -or + @($actualSeeds | Sort-Object -Unique).Count -ne $expectedSeeds.Count -or (($actualSeeds | Sort-Object) -join ',') -cne (($expectedSeeds | Sort-Object) -join ',')) {throw 'Matched seed inventory differs.'} foreach ($seed in $manifest.zmxSeeds) { if ($seed.exitCode -ne 0 -or -not $seed.daemonReady -or $seed.cmdCount -lt 1 -or @@ -1138,7 +1205,8 @@ try { $outputDirectory = Assert-CapSupervisorPath $requestedOutputDirectory if (Test-Path -LiteralPath $outputDirectory) { throw "Refusing to replace a prior capture output: $outputDirectory" } $null = New-Item -ItemType Directory -Path $outputDirectory - $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} else {'lease-release.json'}) + $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} + elseif ($focusedCapture) {'focused-release.json'} else {'lease-release.json'}) $safeOutputInitialized = $true $moduleHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $modulePath).Hash $workerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $workerPath).Hash @@ -1151,7 +1219,9 @@ try { return } if (($transitionRehearsal -and (-not $RehearsalAuthorizationPath -or -not $RehearsalAuthorizationSha256)) -or - (-not $transitionRehearsal -and (-not $LeaseReceiptPath -or -not $LeaseReceiptSha256)) -or + ($focusedCapture -and (-not $FocusedAuthorizationPath -or -not $FocusedAuthorizationSha256)) -or + (-not $transitionRehearsal -and -not $focusedCapture -and + (-not $LeaseReceiptPath -or -not $LeaseReceiptSha256)) -or (-not $rehearsal -and (-not $PreflightReceiptPath -or -not $PreflightReceiptSha256))) { throw 'No exact desktop authorization and accepted preflight receipt were supplied.' } @@ -1171,7 +1241,16 @@ try { } $fixtureMappingHashExpected = $job.fixture.mappingSha256 } - if ($rehearsal) { + if ($focusedCapture) { + if ($job.executionMode -cne $ExecutionMode -or $job.desktopAuthorized -cne $true -or + $job.evidenceClassification -cne 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2') { + throw 'Expected an explicitly authorized focused matched capture job.' + } + $preflightHash=(Get-FileHash -Algorithm SHA256 -LiteralPath $preflightPath).Hash + $preflight=Read-CapBoundJson -Root $root -Reference ([pscustomobject]@{ + path=$preflightPath;sha256=$preflightHash + }) + } elseif ($rehearsal) { if ($job.executionMode -cne $ExecutionMode -or $job.desktopAuthorized -ne $false -or $job.rehearsalTarget.arguments -isnot [array] -or $job.rehearsalTarget.behavior -notin @( @@ -1197,7 +1276,9 @@ try { $preflight = Read-CapBoundJson -Root $root -Reference ([pscustomobject]@{ path = $preflightPath; sha256 = $preflightHash }) } $null = Assert-CapLease - if ($transitionRehearsal) { + if ($focusedCapture) { + $jobIdentity=Assert-CapFocusedJobIdentity -Root $root -Job $job -Authorization $lease -JobSha256 $jobHash + } elseif ($transitionRehearsal) { $jobIdentity = Assert-CapTransitionJobIdentity } elseif (-not $rehearsal -or $null -ne $job.PSObject.Properties['admission']) { $jobIdentity = Assert-CapJobIdentity -Root $root -Job $job -Lease $lease -JobSha256 $jobHash -OfflineTestData:$rehearsal @@ -1216,6 +1297,7 @@ try { $sourceRoot = Assert-CapGreenJob $null = Assert-CapPreflightReport } + if (Test-CapMatchedAdapter) {$matchedCapturePlan=Get-CapMatchedCapturePlan -Job $job} $exports = @(Get-Command -Module UiCaptureDriver | Sort-Object Name) if ($exports.Count -lt 30) { throw "Capture driver exports unavailable: count=$($exports.Count)." } if (-not $rehearsal) { @@ -1263,7 +1345,8 @@ try { $processJournalPath = Join-Path $runDirectory 'process-journal' $manifestPath = Join-Path $outputDirectory 'manifest.json' $workerLogPath = Join-Path $outputDirectory 'worker.log' - $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} else {'lease-release.json'}) + $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} + elseif ($focusedCapture) {'focused-release.json'} else {'lease-release.json'}) foreach ($path in @($runDirectory)) { if (Test-Path -LiteralPath $path) { throw "Refusing to reuse prior capture run path: $path" } } @@ -1280,11 +1363,13 @@ try { authorityId = $authorityId authorityPath = $authorityPath authoritySha256 = $authorityHash - leaseId = if ($transitionRehearsal) { $null } else { $lease.leaseId } - leaseReceiptPath = if ($transitionRehearsal) { $null } else { $LeaseReceiptPath } - leaseReceiptSha256 = if ($transitionRehearsal) { $null } else { $leaseHash } + leaseId = if ($transitionRehearsal -or $focusedCapture) { $null } else { $lease.leaseId } + leaseReceiptPath = if ($transitionRehearsal -or $focusedCapture) { $null } else { $LeaseReceiptPath } + leaseReceiptSha256 = if ($transitionRehearsal -or $focusedCapture) { $null } else { $leaseHash } rehearsalAuthorizationPath = if ($transitionRehearsal) { $RehearsalAuthorizationPath } else { $null } rehearsalAuthorizationSha256 = if ($transitionRehearsal) { $authorityHash } else { $null } + focusedAuthorizationPath = if ($focusedCapture) { $FocusedAuthorizationPath } else { $null } + focusedAuthorizationSha256 = if ($focusedCapture) { $authorityHash } else { $null } jobPath = $JobPath jobSha256 = $jobHash modulePath = $modulePath @@ -1312,7 +1397,19 @@ try { } Write-CapJsonAtomic -Path $authorizationPath -Value $auth $authorizationHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $authorizationPath).Hash - $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} else {'lease-release.json'}) + if ($focusedCapture) { + $null=Assert-CapFocusedPrelaunchPaths -Root $root -Paths ([pscustomobject]@{ + outputDirectory=$outputDirectory + runDirectory=$runDirectory + processJournalPath=$processJournalPath + authorizationPath=$authorizationPath + jobPath=$JobPath + preflightPath=$preflightPath + authorityPath=$authorityPath + }) + } + $releaseReceiptPath = Join-Path $outputDirectory $(if ($transitionRehearsal) {'rehearsal-release.json'} + elseif ($focusedCapture) {'focused-release.json'} else {'lease-release.json'}) Write-CapSupervisorLog "AUTHORIZATION_ACCEPTED id=$authorityId mode=$ExecutionMode head=$($job.head) remaining=$([Math]::Round(($leaseExpiresUtc - [DateTime]::UtcNow).TotalSeconds, 1))s reserve=90s" Write-CapSupervisorLog "STARTING_WORKER path=$workerPath sha256=$workerHash authorization=$authorizationHash" @@ -1501,12 +1598,16 @@ try { else { 'See worker manifest' } evidenceClassification = if ($transitionRehearsal) { 'LIVE_TRANSITION_REHEARSAL_NOT_CAPTURE_EVIDENCE' + } elseif ($focusedCapture) { + 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' } else { $null } + officialParityEvidence = if ($focusedCapture) {$false} else {$null} authorityId = $authorityId authoritySha256 = $authorityHash - leaseId = if ($transitionRehearsal) { $null } elseif ($lease) { $lease.leaseId } else { $null } - leaseReceiptSha256 = if ($transitionRehearsal) { $null } else { $leaseHash } + leaseId = if ($transitionRehearsal -or $focusedCapture) { $null } elseif ($lease) { $lease.leaseId } else { $null } + leaseReceiptSha256 = if ($transitionRehearsal -or $focusedCapture) { $null } else { $leaseHash } rehearsalAuthorizationSha256 = if ($transitionRehearsal) { $authorityHash } else { $null } + focusedAuthorizationSha256 = if ($focusedCapture) { $authorityHash } else { $null } jobSha256 = $jobHash head = if ($job) { $job.head } else { $null } moduleSha256 = $moduleHash @@ -1535,6 +1636,10 @@ try { containmentJob = $workerJobReceipt journalPublishedInventory = if ($journalProof) { ,@($journalProof.Inventory) } else { $null } captures = if ($manifest) { $manifest.images.Count } else { 0 } + focusedReleaseStatus = if ($focusedCapture) { + if ($cleanupProven -and -not $survivors.Count) {'FOCUSED_RELEASE_ZERO_SURVIVORS'} + else {'FOCUSED_RELEASE_CLEANUP_UNPROVEN'} + } else {$null} sourceHashesBeforeImport = [ordered]@{ module = $moduleHash; worker = $workerHash; supervisor = $supervisorHash } } Write-CapJsonAtomic -Path $releaseReceiptPath -Value $release -FinalRelease @@ -1553,4 +1658,5 @@ if ($cleanupFailures.Count -or $releaseFailure -or -not $releaseWritten) { } if ($primaryFailure) { throw $primaryFailure } Write-Output ('{0:o} {1}_COMPLETE output={2} release={3}' -f [DateTime]::UtcNow, - $(if ($transitionRehearsal) {'LIVE_TRANSITION_REHEARSAL'} else {'CAPTURE'}), $outputDirectory, $releaseReceiptPath) + $(if ($transitionRehearsal) {'LIVE_TRANSITION_REHEARSAL'} + elseif ($focusedCapture) {'FOCUSED_CAPTURE'} else {'CAPTURE'}), $outputDirectory, $releaseReceiptPath) diff --git a/Tools/windows/Invoke-UiCaptureWorker.ps1 b/Tools/windows/Invoke-UiCaptureWorker.ps1 index effe1d07..137df5d8 100644 --- a/Tools/windows/Invoke-UiCaptureWorker.ps1 +++ b/Tools/windows/Invoke-UiCaptureWorker.ps1 @@ -12,6 +12,8 @@ $root = [IO.Path]::GetFullPath($OwnedRoot).TrimEnd('\') $modulePath = Join-Path $PSScriptRoot 'UiCaptureDriver.psm1' $rehearsal = $false $transitionRehearsal = $false +$focusedCapture = $false +$matchedCapturePlan = $null $rehearsalLaunches = [Collections.Generic.List[object]]::new() $rehearsalActions = [Collections.Generic.List[object]]::new() $peerScript = Join-Path $root 'inputs\offline-peer-source.ps1' @@ -258,7 +260,27 @@ function Assert-CapAcceptedInputs { Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters Assert-CapSceneContract -Job $job - if ($transitionRehearsal) { + if ($focusedCapture) { + $plan=Get-CapMatchedCapturePlan -Job $job + if ($lease.status -cne 'AUTHORIZED' -or + $lease.authorizedAction -cne 'focused-matched-capture' -or + $lease.evidenceClassification -cne $plan.evidenceClassification -or + -not $lease.desktopAuthorized -or -not $lease.captureEvidenceAuthorized -or + -not $lease.pngAuthorized -or $lease.officialParityEvidence -cne $false -or + ($lease.sceneNames -join ',') -cne ($plan.sceneNames -join ',') -or + ($lease.phases -join ',') -cne ($plan.phases -join ',')) { + throw 'Focused matched capture lacks exact non-parity desktop and PNG authority.' + } + if ($lease.authorizationId -ne (Get-CapWorkerAuthorityId) -or + $lease.head -ne $job.head -or $lease.sourceTree -ne $job.captureTree -or + $lease.jobManifestSha256 -ne $jobHash -or $lease.moduleSha256 -ne $moduleHash -or + $lease.workerSha256 -ne $workerHash -or + $lease.supervisorSha256 -ne $auth.supervisorSha256 -or + $lease.preflightReceiptSha256 -ne $auth.preflightReceiptSha256 -or + $lease.fixtureMappingSha256 -ne $auth.fixtureMappingSha256) { + throw 'Focused authorization hashes do not bind the exact current job and controls.' + } + } elseif ($transitionRehearsal) { if (-not $job.authority.liveTransitionRehearsalApproved -or $job.authority.captureEvidenceAuthorized -or $job.authority.pngAuthorized -or $lease.status -cne 'AUTHORIZED' -or $lease.authorizedAction -cne 'live-transition-rehearsal' -or @@ -881,7 +903,10 @@ function Save-CapSceneImage { if ($isSDpi) { $geometry=$AppRun.Placement $record | Add-Member -NotePropertyMembers @{ - evidenceClassification='C8R_RDP_DPI192_QUALIFIED' + evidenceClassification=$(if ($focusedCapture) { + $matchedCapturePlan.evidenceClassification + } else { 'C8R_RDP_DPI192_QUALIFIED' }) + officialParityEvidence=$(if ($focusedCapture) {$false} else {$true}) geometrySchemaVersion=3L geometryDerivation=$geometry.GeometryDerivation logicalClientWH=@($geometry.LogicalClientWH) @@ -918,15 +943,20 @@ function New-CapManifest { status = $Status executionMode = if ($mockSceneRun) { 'OFFLINE_MOCK_ONLY' } elseif ($rehearsal) { 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL' } - elseif ($transitionRehearsal) { 'LIVE_TRANSITION_REHEARSAL' } else { 'LIVE_CAPTURE' } + elseif ($transitionRehearsal) { 'LIVE_TRANSITION_REHEARSAL' } + elseif ($focusedCapture) { 'FOCUSED_LIVE_CAPTURE' } else { 'LIVE_CAPTURE' } evidenceClassification = if ($transitionRehearsal) { 'LIVE_TRANSITION_REHEARSAL_NOT_CAPTURE_EVIDENCE' + } elseif ($focusedCapture) { + $matchedCapturePlan.evidenceClassification } elseif (Test-CapMatchedDpiAdapter) { 'C8R_RDP_DPI192_QUALIFIED' } else { $null } + officialParityEvidence = if ($focusedCapture) {$false} else {$null} authorityId = Get-CapWorkerAuthorityId authoritySha256 = if ($auth.PSObject.Properties['authoritySha256']) {$auth.authoritySha256} else {$leaseHash} - leaseId = if ($transitionRehearsal) { $null } else { $auth.leaseId } - leaseReceiptSha256 = if ($transitionRehearsal) { $null } else { $leaseHash } + leaseId = if ($transitionRehearsal -or $focusedCapture) { $null } else { $auth.leaseId } + leaseReceiptSha256 = if ($transitionRehearsal -or $focusedCapture) { $null } else { $leaseHash } rehearsalAuthorizationSha256 = if ($transitionRehearsal) { $authorityHash } else { $null } + focusedAuthorizationSha256 = if ($focusedCapture) {$authorityHash} else {$null} jobManifestSha256 = $jobHash moduleSha256 = $moduleHash workerSha256 = $workerHash @@ -970,11 +1000,13 @@ function New-CapManifest { @{name='saved-Agent-Split-role-labels';status='NotExecuted';reason='Production listing prunes absent panes; only two chat seeds authorized'}) } if (Test-CapMatchedAdapter) { - $result.sceneAdapter=$job.sceneAdapter;$result.lane='S';$result.requiredSceneNames=@(Get-CapMatchedSceneNames) + $result.sceneAdapter=$job.sceneAdapter;$result.lane='S' + $result.requiredSceneNames=@($matchedCapturePlan.sceneNames) $result.g11Qualification='ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED' $result.backendTurns=0;$result.realRestarts=@($realRestarts) if ((Test-CapMatchedDpiAdapter) -and -not $transitionRehearsal) { - $result.evidenceClassification='C8R_RDP_DPI192_QUALIFIED' + $result.evidenceClassification=$matchedCapturePlan.evidenceClassification + $result.officialParityEvidence=$matchedCapturePlan.officialParityEvidence $result.geometrySchemaVersion=3L $result.logicalClientWH=@($job.captureContract.logicalClientWH) $result.expectedWindowDpi=[long]$job.captureContract.expectedWindowDpi @@ -1742,6 +1774,35 @@ function Read-CapMatchedRows { }) } +function Read-CapMatchedChromeControls { + param($AppRun,[long]$ExpectedDpi,[long]$DpiBase) + $null=Assert-CapRootProcess -RootHandle $AppRun.Window -ProcessIdentity $AppRun.Run.Identity + $graph=Find-CapAutomationElement -RootHandle $AppRun.Window -ProcessIdentity $AppRun.Run.Identity ` + -ExpectedProcessId $AppRun.Run.Identity.Pid -Name 'Graph' -AutomationId 'graph' + $rows=@(Get-CapDirectChildren -Parent $graph | Where-Object { + $_.Current.AutomationId -cmatch '^workspace-(?:split-right|split-down|new-tab)-' + } | ForEach-Object { + $current=$_.Current + $parent=[Windows.Automation.TreeWalker]::ControlViewWalker.GetParent($_) + $invoke=$null + $invokable=$_.TryGetCurrentPattern([Windows.Automation.InvokePattern]::Pattern,[ref]$invoke) + [pscustomobject]@{ + AutomationId=[string]$current.AutomationId + Name=[string]$current.Name + ProcessId=[long]$current.ProcessId + ParentAutomationId=[string]$parent.Current.AutomationId + ControlType=[string]$current.ControlType.ProgrammaticName.Replace('ControlType.','') + Invokable=[bool]$invokable + IsEnabled=[bool]$current.IsEnabled + IsOffscreen=[bool]$current.IsOffscreen + Bounds=@($current.BoundingRectangle.Left,$current.BoundingRectangle.Top, + $current.BoundingRectangle.Right,$current.BoundingRectangle.Bottom) + } + }) + return @(Get-CapMatchedChromeControlObservation -Rows $rows ` + -ExpectedProcessId $AppRun.Run.Identity.Pid -ExpectedDpi $ExpectedDpi -DpiBase $DpiBase) +} + function Invoke-CapMatchedTabSelection { param( [Parameter(Mandatory)]$AppRun, @@ -2034,14 +2095,23 @@ function Invoke-CapMatchedRestartCapture { } function Invoke-CapMatchedScenes { - param([string]$Phase) + param( + [Parameter(Mandatory)][string]$Phase, + [Parameter()][string[]]$SceneNames = @((Get-CapMatchedSceneNames)[0..16]), + [switch]$SkipRestart + ) + $allowed=@((Get-CapMatchedSceneNames)[0..16]) + if ($SceneNames.Count -lt 1 -or @($SceneNames | Sort-Object -Unique).Count -ne $SceneNames.Count -or + @($SceneNames | Where-Object {$_ -cnotin $allowed}).Count -ne 0) { + throw 'Matched scene execution requested a missing, duplicate or foreign scene.' + } $fixture=Get-Content -LiteralPath $fixturePath -Raw | ConvertFrom-Json -DateKind String $null=Assert-CapMatchedFixture -Fixture $fixture -ProjectRoot (Join-Path $root 'fixtures\projects') $expectedDpiProperty=$job.captureContract.PSObject.Properties['expectedWindowDpi'] $dpiBaseProperty=$job.captureContract.PSObject.Properties['dpiBase'] $tabExpectedDpi=if ($expectedDpiProperty) {[long]$expectedDpiProperty.Value} else {[long]$job.captureContract.dpi} $tabDpiBase=if ($dpiBaseProperty) {[long]$dpiBaseProperty.Value} else {96L} - foreach ($scene in @((Get-CapMatchedSceneNames)[0..16])) { + foreach ($scene in $SceneNames) { Assert-CapWorkerDeadline "matched scene $scene" if ([DateTime]::UtcNow -ge $leaseDeadlineUtc.AddSeconds(-90)) {throw 'Matched scenes reached the required cleanup reserve.'} $roots=Initialize-CapPhaseRoot -PhaseRoot (Join-Path $runDirectory "phases\$Phase\$scene") @@ -2222,6 +2292,9 @@ function Invoke-CapMatchedScenes { -TotalTabs $total -SelectedTab $selected -AgentTitle $title ` -SourceCommit $job.binaries.$Phase.source ` -ExpectedDpi $tabExpectedDpi -DpiBase $tabDpiBase + $chromeControls=if ($focusedCapture) { + @(Read-CapMatchedChromeControls -AppRun $app -ExpectedDpi $tabExpectedDpi -DpiBase $tabDpiBase) + } else {@()} if ($scene -ceq 's-tabs-hover-close') { $tab=@($tabs.visible | Where-Object index -EQ 1)[0] Set-CapMatchedTabHover -AppRun $app -Bounds $tab.bounds @@ -2231,6 +2304,7 @@ function Invoke-CapMatchedScenes { if (@($panes | Where-Object focused).Count -ne 1) {throw 'Matched selected pane did not acquire the reported UIA focus.'} $app.SObservation=[pscustomobject]@{readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY' tabs=$tabs;panes=$panes;nodeId=$node.id;nodeState=$node.state + chromeControls=$chromeControls tabSelection=$tabSelection hover=if ($scene -ceq 's-tabs-hover-close') {'POSTED_WM_MOUSEMOVE_NOT_PHYSICAL_POINTER'} else {'NOT_REQUESTED'}} Save-CapSceneImage -AppRun $app -SceneName $scene -ZoomEvidence $null -Phase $Phase @@ -2242,7 +2316,7 @@ function Invoke-CapMatchedScenes { Close-CapPeer -PeerRun $peer -Label $pipe } } - Invoke-CapMatchedRestartCapture -Phase $Phase -Fixture $fixture + if (-not $SkipRestart) {Invoke-CapMatchedRestartCapture -Phase $Phase -Fixture $fixture} } function Set-CapMatchedTabHover { @@ -2297,11 +2371,13 @@ try { } $auth = Get-Content -LiteralPath $AuthorizationPath -Raw | ConvertFrom-Json -DateKind String if ($auth.ownedRoot -ine $root -or - $auth.executionMode -notin @('LIVE_CAPTURE', 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL', 'LIVE_TRANSITION_REHEARSAL')) { + $auth.executionMode -notin @('LIVE_CAPTURE','FOCUSED_LIVE_CAPTURE', + 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL','LIVE_TRANSITION_REHEARSAL')) { throw 'Worker root or execution mode differs from supervisor authorization.' } $rehearsal = $auth.executionMode -ceq 'NO_DESKTOP_REAL_LAUNCH_REHEARSAL' $transitionRehearsal = $auth.executionMode -ceq 'LIVE_TRANSITION_REHEARSAL' + $focusedCapture = $auth.executionMode -ceq 'FOCUSED_LIVE_CAPTURE' # Hash the exact source bytes before importing or compiling the native declarations. $moduleHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $modulePath).Hash $workerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $PSCommandPath).Hash @@ -2351,7 +2427,7 @@ try { $lease.fixtureMappingSha256 -ne $auth.fixtureMappingSha256) { throw 'Worker lease does not bind its exact owned-root fixture mapping.' } - $authorityId = if ($transitionRehearsal) {$lease.authorizationId} else {$lease.leaseId} + $authorityId = if ($transitionRehearsal -or $focusedCapture) {$lease.authorizationId} else {$lease.leaseId} if ($authorityId -ne (Get-CapWorkerAuthorityId) -or $lease.jobManifestSha256 -ne $jobHash -or $lease.moduleSha256 -ne $moduleHash -or $lease.workerSha256 -ne $workerHash -or $lease.supervisorSha256 -ne $supervisorHash) { @@ -2384,13 +2460,32 @@ try { $journalState = New-CapJournalState -Root $root -Directory $journalPath ` -Binding $journalBinding -DeadlineUtc $leaseExpiresUtc Write-CapWorkerJournalSnapshot -Kind 'Bootstrap' - if (-not $transitionRehearsal -and (-not $rehearsal -or $null -ne $job.PSObject.Properties['admission'])) { + if ($focusedCapture) { + $identity=Assert-CapFocusedJobIdentity -Root $root -Job $job -Authorization $lease -JobSha256 $jobHash + if (($identity | ConvertTo-Json -Depth 15 -Compress) -cne + ($auth.jobIdentity | ConvertTo-Json -Depth 15 -Compress)) { + throw 'Worker recomputed focused job identity differs from supervisor authorization.' + } + } elseif (-not $transitionRehearsal -and (-not $rehearsal -or $null -ne $job.PSObject.Properties['admission'])) { $identity = Assert-CapJobIdentity -Root $root -Job $job -Lease $lease -JobSha256 $jobHash -OfflineTestData:$rehearsal if (($identity | ConvertTo-Json -Depth 15 -Compress) -cne ($auth.jobIdentity | ConvertTo-Json -Depth 15 -Compress)) { throw 'Worker recomputed job identity differs from supervisor authorization.' } } - if ($rehearsal) { + if ($focusedCapture) { + if ($job.executionMode -cne 'FOCUSED_LIVE_CAPTURE') { + throw 'Focused job execution mode differs from its authorization.' + } + Assert-CapAcceptedInputs + $zmxHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $root 'runtime\zmx.exe')).Hash + $beforeHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $root 'inputs\graphcode-before.exe')).Hash + $afterHash = (Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $root 'inputs\graphcode-after.exe')).Hash + if ($zmxHash -ne $job.binaries.offlineZmx.sha256 -or + $beforeHash -ne $job.binaries.before.sha256 -or $afterHash -ne $job.binaries.after.sha256) { + throw 'Focused executable hashes differ from the frozen current-source job.' + } + $fixtureHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $fixturePath).Hash + } elseif ($rehearsal) { if ($lease.status -cne 'REHEARSAL_ONLY' -or $lease.desktopAuthorized -ne $false -or $lease.authorizedAction -cne 'no-desktop-real-launch-rehearsal' -or $job.executionMode -cne $auth.executionMode -or $job.desktopAuthorized -ne $false) { @@ -2437,6 +2532,9 @@ try { } $fixtureHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $fixturePath).Hash } + if (Test-CapMatchedAdapter) { + $matchedCapturePlan=Get-CapMatchedCapturePlan -Job $job + } if (-not (Test-Path -LiteralPath $outputDirectory -PathType Container)) { throw "Supervisor did not pre-create the authorized output directory: $outputDirectory" } @@ -2457,15 +2555,24 @@ try { $matchedFixture=Get-Content -LiteralPath $fixturePath -Raw | ConvertFrom-Json -DateKind String Initialize-CapMatchedWorktrees -Fixture $matchedFixture } - foreach ($phase in @('before', 'after')) { + $capturePhases=if (Test-CapMatchedAdapter) {@($matchedCapturePlan.phases)} else {@('before','after')} + foreach ($phase in $capturePhases) { Assert-CapWorkerDeadline "starting phase $phase" if ($transitionRehearsal -and $phase -ceq 'after') { Save-CapPhaseTransition } - if (Test-CapMatchedAdapter) {Invoke-CapMatchedScenes -Phase $phase} + if (Test-CapMatchedAdapter) { + Invoke-CapMatchedScenes -Phase $phase -SceneNames $matchedCapturePlan.sceneNames ` + -SkipRestart:(-not $matchedCapturePlan.includeRestart) + } elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {Invoke-CapDScenes -Phase $phase} else {Invoke-CapNormalScenes -Phase $phase;Invoke-CapActionsScene -Phase $phase} } - $expectedImages=if ($transitionRehearsal) {0} elseif (Test-CapMatchedAdapter) {36} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {20} else {10} - $expectedSeeds=if (Test-CapMatchedAdapter) {22} elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {4} else {2} + $expectedImages=if ($transitionRehearsal) {0} elseif (Test-CapMatchedAdapter) { + $matchedCapturePlan.phases.Count*($matchedCapturePlan.sceneNames.Count+ + $(if ($matchedCapturePlan.includeRestart) {1} else {0})) + } elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {20} else {10} + $expectedSeeds=if (Test-CapMatchedAdapter) { + $matchedCapturePlan.phases.Count*@($matchedCapturePlan.sceneNames | Where-Object {$_ -cmatch '^s-tabs-'}).Count + } elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {4} else {2} if ($images.Count -ne $expectedImages) { throw "Expected $expectedImages images across before/after; captured $($images.Count)." } if ($transitionRehearsal) { Assert-CapNoPngArtifacts diff --git a/Tools/windows/Prepare-FocusedUiCapture.ps1 b/Tools/windows/Prepare-FocusedUiCapture.ps1 new file mode 100644 index 00000000..3fcf635a --- /dev/null +++ b/Tools/windows/Prepare-FocusedUiCapture.ps1 @@ -0,0 +1,197 @@ +param( + [Parameter(Mandatory)][string]$Repository, + [Parameter(Mandatory)][string]$OwnedRoot, + [Parameter(Mandatory)][string]$GraphCodeBinary, + [Parameter(Mandatory)][string]$ZmxBinary, + [Parameter(Mandatory)][string]$DaemonRuntimeRoot, + [Parameter(Mandatory)][string]$BackendFixtureBinary, + [Parameter(Mandatory)][string]$OfflinePeerSource, + [Parameter(Mandatory)][string]$PreparedFixtureRoot, + [Parameter(Mandatory)][string]$PreflightReport, + [ValidateRange(3,30)][long]$AuthorizationMinutes = 20 +) +$ErrorActionPreference='Stop' +$root=[IO.Path]::GetFullPath($OwnedRoot).TrimEnd('\') +$repositoryRoot=[IO.Path]::GetFullPath($Repository).TrimEnd('\') +if (Test-Path -LiteralPath $root) {throw "Focused capture root already exists: $root"} +if ($root.Length -gt 5 -or $root -eq [IO.Path]::GetPathRoot($root).TrimEnd('\')) { + throw 'Focused capture requires a fresh owned root of at most five characters.' +} +$head=(& git -C $repositoryRoot rev-parse HEAD).Trim() +$tree=(& git -C $repositoryRoot rev-parse 'HEAD^{tree}').Trim() +if ($head -cnotmatch '^[a-f0-9]{40}$' -or $tree -cnotmatch '^[a-f0-9]{40}$' -or + @(& git -C $repositoryRoot status --porcelain --untracked-files=all).Count -ne 0) { + throw 'Focused capture preparation requires a clean exact Git head and tree.' +} + +$modulePath=Join-Path $repositoryRoot 'Tools\windows\UiCaptureDriver.psm1' +$workerPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCaptureWorker.ps1' +$supervisorPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCapture.ps1' +foreach ($path in @($modulePath,$workerPath,$supervisorPath,$GraphCodeBinary,$ZmxBinary, + (Join-Path $DaemonRuntimeRoot 'graphcoded.exe'),$BackendFixtureBinary,$OfflinePeerSource, + (Join-Path $PreparedFixtureRoot 'projects'),$PreflightReport)) { + if (-not (Test-Path -LiteralPath $path)) {throw "Focused capture input is missing: $path"} +} +Import-Module $modulePath -Force -WarningAction SilentlyContinue + +$inputs=Join-Path $root 'inputs' +$runtime=Join-Path $root 'runtime' +$fixtures=Join-Path $root 'fixtures' +$reports=Join-Path $root 'reports' +$null=New-Item -ItemType Directory -Path $inputs,$runtime,$fixtures,$reports +$null=Copy-Item -LiteralPath $GraphCodeBinary -Destination (Join-Path $inputs 'graphcode-before.exe') +$null=Copy-Item -LiteralPath $GraphCodeBinary -Destination (Join-Path $inputs 'graphcode-after.exe') +$null=Copy-Item -LiteralPath $ZmxBinary -Destination (Join-Path $runtime 'zmx.exe') +$null=Copy-Item -LiteralPath $BackendFixtureBinary -Destination (Join-Path $runtime 'copilot.exe') +$null=Copy-Item -LiteralPath $OfflinePeerSource -Destination (Join-Path $inputs 'offline-peer-source.ps1') +$null=Copy-Item -LiteralPath (Join-Path $PreparedFixtureRoot 'projects') -Destination $fixtures -Recurse +foreach ($file in Get-ChildItem -LiteralPath $DaemonRuntimeRoot -File) { + if ($file.Name -cin @('zmx.exe','copilot.exe')) {continue} + $null=Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $runtime $file.Name) +} +$fixtureSource=Join-Path $repositoryRoot 'Tools\windows\fixtures\matched-v12.json' +$copiedFixtureSource=Join-Path $inputs 'fixture-source.json' +$null=Copy-Item -LiteralPath $fixtureSource -Destination $copiedFixtureSource +$fixturePath=Join-Path $fixtures 'fixture.json' +$destinationProjectRoot=Join-Path $fixtures 'projects' +$fixtureText=[IO.File]::ReadAllText($copiedFixtureSource).Replace( + 'C:\\fixture',(($destinationProjectRoot | ConvertTo-Json -Compress).Trim('"'))) +[IO.File]::WriteAllText($fixturePath,$fixtureText,[Text.UTF8Encoding]::new($false)) +$backendSourcePath=Join-Path $inputs 'offline-backend.c' +[IO.File]::WriteAllText($backendSourcePath,(Get-CapMatchedBackendSource),[Text.UTF8Encoding]::new($false)) +$preflightPath=Join-Path $reports 'preflight-185.json' +$null=Copy-Item -LiteralPath $PreflightReport -Destination $preflightPath + +$fixtureSourceHash=(Get-FileHash -LiteralPath $copiedFixtureSource -Algorithm SHA256).Hash +$fixtureHash=(Get-FileHash -LiteralPath $fixturePath -Algorithm SHA256).Hash +$mappingPath=Join-Path $reports 'fixture-mapping.json' +$mapping=[ordered]@{ + sourcePath='C:\fixture\fixture.json' + sourceSha256=$fixtureSourceHash + copiedSourcePath=$copiedFixtureSource + rewrittenFixturePath=$fixturePath + rewrittenFixtureSha256=$fixtureHash + pathPrefixMap=[ordered]@{from='C:\fixture';to=$destinationProjectRoot} + graphs=1L + topLevelNodes=8L + pathsRewritten=$true +} +[IO.File]::WriteAllText($mappingPath,($mapping | ConvertTo-Json -Depth 10),[Text.UTF8Encoding]::new($false)) +$mappingHash=(Get-FileHash -LiteralPath $mappingPath -Algorithm SHA256).Hash +$binaryHash=(Get-FileHash -LiteralPath (Join-Path $inputs 'graphcode-after.exe') -Algorithm SHA256).Hash +$zmxHash=(Get-FileHash -LiteralPath (Join-Path $runtime 'zmx.exe') -Algorithm SHA256).Hash +$daemonHash=(Get-FileHash -LiteralPath (Join-Path $runtime 'graphcoded.exe') -Algorithm SHA256).Hash +$backendHash=(Get-FileHash -LiteralPath (Join-Path $runtime 'copilot.exe') -Algorithm SHA256).Hash +$backendSourceHash=(Get-FileHash -LiteralPath $backendSourcePath -Algorithm SHA256).Hash +$peerHash=(Get-FileHash -LiteralPath (Join-Path $inputs 'offline-peer-source.ps1') -Algorithm SHA256).Hash + +$scenes=@(Get-CapMatchedSceneNames | ForEach-Object { + [ordered]@{name=$_;naturalWidthBeforeAfter=@(250L,250L)} +}) +$identity=[ordered]@{ + sourceContract='GRAPHCODE_MAIN_WINDOW_V1';sourceCommit=$head;expectedWindowDpi=192L + requestedOuterWH=@(1280L,820L);expectedInitialClientWH=@(1254L,710L) + expectedInitialOuterWH=@(1280L,820L);expectedPhysicalClientWH=@(2880L,1800L) + expectedAdjustedOuterWH=@(2906L,1910L);windowStyleForAdjustment=47120384L + windowExStyleForAdjustment=0L;expectedRuntimeWindowStyle=382664704L + expectedRuntimeWindowExStyle=256L;hasMenu=$true + geometryDerivation='SOURCE_CREATEWINDOWEX_PLUS_WIN32_NORMALIZATION' +} +$job=[ordered]@{ + executionMode='FOCUSED_LIVE_CAPTURE' + desktopAuthorized=$true + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + lane='S' + sceneAdapter='S_MATCHED_V12_DPI_V1' + head=$head + base=$head + captureTree=$tree + sourceRepository=$repositoryRoot + authority=[ordered]@{centralCaptureLeaseGrantedByThisArtifact=$false;sWorkerMayLaunchGui=$false} + focusedCapture=[ordered]@{ + schemaVersion=1L + phases=@('after') + sceneNames=@(Get-CapMatchedFocusedSceneNames) + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + officialParityEvidence=$false + } + captureContract=[ordered]@{ + geometrySchemaVersion=3L;phases=@('before','after');sceneCountPerPhase=18L + placement='MEASURED_FIT_WORKAREA';logicalClientWH=@(1440L,900L) + expectedWindowDpi=192L;dpiBase=96L;expectedPhysicalClientWH=@(2880L,1800L) + minimumPhysicalWorkAreaWH=@(2906L,1910L) + windowIdentities=[ordered]@{before=$identity;after=$identity} + geometryDerivation='AdjustWindowRectExForDpi';canvasZoomPercentLogical=100L + rawCaptureResamplingAllowed=$false;cleanupReserveSeconds=90L;estimatedSeconds=900L + } + scenes=$scenes + fixture=[ordered]@{ + sha256=$fixtureSourceHash;mappingSha256=$mappingHash;originalRoot='C:\fixture' + macSource='2ccf303a04f995184aae1d4b738a9eca0f71b79e' + offlinePeerReference=[ordered]@{sha256=$peerHash} + } + binaries=[ordered]@{ + before=[ordered]@{path=(Join-Path $inputs 'graphcode-before.exe');sha256=$binaryHash + source=$head;binaryBuiltAtSource=$head} + after=[ordered]@{path=(Join-Path $inputs 'graphcode-after.exe');sha256=$binaryHash + source=$head;binaryBuiltAtSource=$head} + offlineZmx=[ordered]@{ + sha256=$zmxHash;providerCommit='785b3fd15dcafd1882b495c831a10f98c201b908' + seedArgumentOrder=@('run','graphcode-11111111-1111-4111-8111-000000000001', + '-d','cmd.exe','/Q','/K','echo Offline card fixture - no backend') + } + daemon=[ordered]@{path=(Join-Path $runtime 'graphcoded.exe');sha256=$daemonHash + source=$head;binaryBuiltAtSource=$head} + backendFixture=[ordered]@{path=(Join-Path $runtime 'copilot.exe');sha256=$backendHash + sourceSha256=$backendSourceHash;networkAllowed=$false} + } +} +$jobPath=Join-Path $root 'job.json' +[IO.File]::WriteAllText($jobPath,($job | ConvertTo-Json -Depth 40),[Text.UTF8Encoding]::new($false)) +$jobHash=(Get-FileHash -LiteralPath $jobPath -Algorithm SHA256).Hash +$now=[DateTimeOffset]::UtcNow +$authorization=[ordered]@{ + schemaVersion=1L + authorizationId=[guid]::NewGuid().ToString() + status='AUTHORIZED' + authorizedAction='focused-matched-capture' + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + officialParityEvidence=$false + desktopAuthorized=$true + captureEvidenceAuthorized=$true + pngAuthorized=$true + lane='S' + head=$head + sourceTree=$tree + jobManifestSha256=$jobHash + moduleSha256=(Get-FileHash -LiteralPath $modulePath -Algorithm SHA256).Hash + workerSha256=(Get-FileHash -LiteralPath $workerPath -Algorithm SHA256).Hash + supervisorSha256=(Get-FileHash -LiteralPath $supervisorPath -Algorithm SHA256).Hash + preflightReceiptSha256=(Get-FileHash -LiteralPath $preflightPath -Algorithm SHA256).Hash + fixtureMappingSha256=$mappingHash + beforeBinarySha256=$binaryHash + afterBinarySha256=$binaryHash + sceneNames=@(Get-CapMatchedFocusedSceneNames) + phases=@('after') + grantedUtc=$now.AddSeconds(-1).ToString('o') + expiresUtc=$now.AddMinutes($AuthorizationMinutes).ToString('o') + maxMinutes=$AuthorizationMinutes +} +$authorizationPath=Join-Path $root 'focused-authorization.json' +[IO.File]::WriteAllText($authorizationPath,($authorization | ConvertTo-Json -Depth 20),[Text.UTF8Encoding]::new($false)) +$result=[ordered]@{ + ownedRoot=$root + jobPath=$jobPath + jobSha256=$jobHash + fixtureMappingSha256=$mappingHash + preflightReceiptPath=$preflightPath + preflightReceiptSha256=$authorization.preflightReceiptSha256 + focusedAuthorizationPath=$authorizationPath + focusedAuthorizationSha256=(Get-FileHash -LiteralPath $authorizationPath -Algorithm SHA256).Hash + outputDirectory=Join-Path $root 'outputs' + sourceHead=$head + sourceTree=$tree + binarySha256=$binaryHash + zmxSha256=$zmxHash +} +$result | ConvertTo-Json -Depth 10 diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index 01223086..9d136060 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -2956,6 +2956,52 @@ function Invoke-CapMatchedContracts { $job=New-CapMatchedTestJob Assert-CapSceneContract -Job $job Assert-CapTest ($job.scenes.Count -eq 18) 'Expected eighteen bounded matched scenes.' + $focused=New-CapMatchedDpiTestJob + $focused | Add-Member -NotePropertyName focusedCapture -NotePropertyValue ([pscustomobject]@{ + schemaVersion=1L + phases=@('after') + sceneNames=@( + 's-tabs-agent-unknown','s-tabs-backend-idle','s-tabs-goal-needs-you','s-tabs-loop-running', + 's-tabs-two-agent-selected','s-tabs-two-shell-selected','s-tabs-split-right-focused', + 's-tabs-many-first','s-tabs-overflow-last','s-tabs-hover-close','s-tabs-last-guard' + ) + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + officialParityEvidence=$false + }) + $focused | Add-Member -NotePropertyName head -NotePropertyValue $focused.binaries.after.source + $focused.binaries.before.source=$focused.head + $focused.binaries.before.binaryBuiltAtSource=$focused.head + $focused.captureContract.windowIdentities.before=$focused.captureContract.windowIdentities.after | + ConvertTo-Json -Depth 10 | ConvertFrom-Json -DateKind String + Assert-CapSceneContract -Job $focused + $plan=Get-CapMatchedCapturePlan -Job $focused + Assert-CapTest ($plan.phases.Count -eq 1 -and $plan.phases[0] -ceq 'after' -and + $plan.sceneNames.Count -eq 11 -and $plan.includeRestart -eq $false -and + $plan.evidenceClassification -ceq 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2') ` + 'Focused matched plan did not preserve the exact after-only F2 inventory.' + $supervisorSource=[IO.File]::ReadAllText($supervisorPath) + $workerSource=[IO.File]::ReadAllText($workerPath) + $prepareSource=[IO.File]::ReadAllText((Join-Path $driverDirectory 'Prepare-FocusedUiCapture.ps1')) + Assert-CapTest ($supervisorSource.Contains("'FOCUSED_LIVE_CAPTURE'") -and + $supervisorSource.Contains('FocusedAuthorizationPath') -and + $supervisorSource.Contains('Assert-CapFocusedPrelaunchPaths') -and + $workerSource.Contains('function Read-CapMatchedChromeControls') -and + $workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and + $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'")) ` + 'Focused supervisor, worker chrome reader or checked-in preparation entry point is not wired.' + $focusedMutations=@('missing','extra','wrong-scene','wrong-phase','wrong-classification','parity-overclaim') + foreach ($mutation in $focusedMutations) { + $changed=$focused | ConvertTo-Json -Depth 30 | ConvertFrom-Json -DateKind String + switch ($mutation) { + 'missing' {$changed.focusedCapture.sceneNames=@($changed.focusedCapture.sceneNames[1..10])} + 'extra' {$changed.focusedCapture.sceneNames+= 's-restart-confirmed'} + 'wrong-scene' {$changed.focusedCapture.sceneNames[0]='s-main-cycle-selected'} + 'wrong-phase' {$changed.focusedCapture.phases=@('before')} + 'wrong-classification' {$changed.focusedCapture.evidenceClassification='C8R_RDP_DPI192_QUALIFIED'} + 'parity-overclaim' {$changed.focusedCapture.officialParityEvidence=$true} + } + $null=Assert-CapRejects {Get-CapMatchedCapturePlan -Job $changed} 'Focused matched capture' + } foreach ($seconds in @(810,1710)) { $job.captureContract.estimatedSeconds=[long]$seconds Assert-CapSceneContract -Job $job @@ -2968,11 +3014,14 @@ function Invoke-CapMatchedContracts { $changed.scenes=@($changed.scenes | Where-Object name -CNE $name) $null=Assert-CapRejects {Assert-CapSceneContract -Job $changed} 'Matched scene' } - return @{positive=3;negative=($names.Count+1);budgetAcceptedSeconds=@(810,1710);budgetRefusedSeconds=1711 - names=$names;routing=(Invoke-CapMatchedSceneRoutingContract)} + return @{positive=4;negative=($names.Count+1+$focusedMutations.Count) + budgetAcceptedSeconds=@(810,1710);budgetRefusedSeconds=1711 + names=$names;focused=$plan;focusedMutations=$focusedMutations + prelaunchPaths=(Invoke-CapFocusedPrelaunchPathContract) + routing=(Invoke-CapMatchedSceneRoutingContract)} } Invoke-CapTest 'matched tab observations preserve exact owned IDs parents selection and close bounds' { - Invoke-CapMatchedTabContracts + return @{tabs=(Invoke-CapMatchedTabContracts);chrome=(Invoke-CapMatchedChromeControlContracts)} } Invoke-CapTest 'real restart transcript requires daemon confirmation resume and new owned attach' { $evidence=New-CapRestartTestEvidence @@ -3469,6 +3518,54 @@ function Invoke-CapMatchedTabContracts { evidence='SYNTHETIC_UIA_RECORDS_NOT_NATIVE_OBSERVATION'} } +function Invoke-CapMatchedChromeControlContracts { + $specs=@( + @{identity='workspace-split-right:control';name='Split Right';left=100}, + @{identity='workspace-split-down:control';name='Split Down';left=152}, + @{identity='workspace-new-tab:control';name='New Tab';left=204} + ) + $rows=@($specs | ForEach-Object { + [pscustomobject]@{ + AutomationId=(Get-CapMatchedAutomationId $_.identity) + Name=$_.name + ProcessId=4242L + ParentAutomationId='graph' + ControlType='Button' + Invokable=$true + IsEnabled=$true + IsOffscreen=$false + Bounds=@([long]$_.left,100L,([long]$_.left+48L),144L) + } + }) + $proof=@(Get-CapMatchedChromeControlObservation -Rows $rows -ExpectedProcessId 4242 ` + -ExpectedDpi 192 -DpiBase 96) + Assert-CapTest ($proof.Count -eq 3 -and ($proof.name -join ',') -ceq + 'Split Right,Split Down,New Tab' -and + @($proof | Where-Object {$_.physicalWidth -ne 48 -or $_.physicalHeight -ne 44 -or + $_.logicalWidth -ne 24 -or $_.logicalHeight -ne 22}).Count -eq 0) ` + 'Focused chrome controls lost exact stable IDs, order or DPI192 geometry.' + $mutations=@('missing','duplicate','foreign','wrong-bounds','wrong-owner','wrong-type','not-invokable') + foreach ($mutation in $mutations) { + $changed=$rows | ConvertTo-Json -Depth 10 | ConvertFrom-Json -DateKind String + switch ($mutation) { + 'missing' {$changed=@($changed[0..1])} + 'duplicate' {$changed+= $changed[0]} + 'foreign' {$changed[0].AutomationId=Get-CapMatchedAutomationId 'workspace-split-right:foreign'} + 'wrong-bounds' {$changed[0].Bounds[2]=[long]$changed[0].Bounds[2]-1} + 'wrong-owner' {$changed[0].ProcessId=7777L} + 'wrong-type' {$changed[0].ControlType='Text'} + 'not-invokable' {$changed[0].Invokable=$false} + } + $null=Assert-CapRejects { + Get-CapMatchedChromeControlObservation -Rows $changed -ExpectedProcessId 4242 ` + -ExpectedDpi 192 -DpiBase 96 + } 'Matched chrome control' + } + return @{positive=1;negative=$mutations.Count;mutations=$mutations + stableIds=@($proof.automationId);expectedPhysicalWH=@(48,44);expectedLogicalWH=@(24,22) + evidence='SYNTHETIC_UIA_RECORDS_NOT_NATIVE_OBSERVATION'} +} + function Invoke-CapSeedScratchContract { $testAst = [Management.Automation.Language.Parser]::ParseFile( (Join-Path $PSScriptRoot 'UiCaptureDriver.Tests.ps1'),[ref]$null,[ref]$null) @@ -4015,8 +4112,10 @@ function Invoke-CapMatchedSceneRoutingContract { $leaseDeadlineUtc=[DateTime]::UtcNow.AddMinutes(5) $captured=[Collections.Generic.List[object]]::new() $seeded=[Collections.Generic.List[string]]::new() + $focusedCapture=$false $model=[pscustomobject]@{total=1;selected=0;node=$null;removed=$false;restart=0;hover=0;freezeVisibility=$false wireArmed=$false;marked=$false + deadlineFailure=$false;captureFailure=$false;appCleanup=0;seedCleanup=0;peerCleanup=0;chromeReads=0 selections=[Collections.Generic.List[string]]::new()} function Get-MockVisibleIndexes { if ($model.freezeVisibility) {return @(4..11)} @@ -4024,7 +4123,7 @@ function Invoke-CapMatchedSceneRoutingContract { $first=if ($model.selected -ge $count) {$model.selected-$count+1} else {0} return @($first..($first+$count-1)) } - function Assert-CapWorkerDeadline {param($Label)} + function Assert-CapWorkerDeadline {param($Label) if ($model.deadlineFailure) {throw 'Focused deadline injected.'}} function Initialize-CapPhaseRoot {param($PhaseRoot) [pscustomobject]@{Root=$PhaseRoot}} function Start-CapOfflinePeer {param($Roots,$PipeName,$Phase) [pscustomobject]@{mode='SIMULATED_PEER'}} function Start-CapOfflineSeed {param($Roots,$Phase,$SessionName) $seeded.Add($SessionName);[pscustomobject]@{mode='SIMULATED_SEED'}} @@ -4125,23 +4224,68 @@ function Invoke-CapMatchedSceneRoutingContract { param($AppRun,$ExpectedCount,$FocusedIndex) @(foreach ($i in 0..($ExpectedCount-1)) {[pscustomobject]@{focused=($i -eq $FocusedIndex);index=$i;mode='SIMULATED_PANE'}}) } + function Read-CapMatchedChromeControls { + param($AppRun,$ExpectedDpi,$DpiBase) + $model.chromeReads++ + @('Split Right','Split Down','New Tab' | ForEach-Object { + [pscustomobject]@{name=$_;physicalWidth=48L;physicalHeight=44L + logicalWidth=24;logicalHeight=22;controlType='Button';invokable=$true} + }) + } function Set-CapMatchedTabHover {param($AppRun,$Bounds) $model.hover++} function Measure-CapSceneZoom {param($AppRun,$Scene,$NaturalWidth) [pscustomobject]@{ZoomPercent=100;mode='SIMULATED_ZOOM'}} function Save-CapSceneImage { param($AppRun,$SceneName,$ZoomEvidence,$Phase) + if ($model.captureFailure -and $SceneName -ceq 's-tabs-agent-unknown') { + throw 'Focused image failure injected.' + } Assert-CapTest ($AppRun.SObservation.readSucceeded -and $AppRun.SObservation.evidenceMode -ceq 'OWNED_UIA_READ_NOT_VISUAL_PARITY') 'Actual executor skipped its state predicate before capture.' $captured.Add([pscustomobject]@{scene=$SceneName;phase=$Phase;observation=$AppRun.SObservation;image='NOT_CAPTURED_COMPONENT_ONLY'}) } - function Close-CapGraphCodeApp {param($AppRun,[switch]$NormalExit)} - function Close-CapOfflineSeed {param($SeedRun)} - function Close-CapPeer {param($PeerRun,$Label)} + function Close-CapGraphCodeApp {param($AppRun,[switch]$NormalExit) $model.appCleanup++} + function Close-CapOfflineSeed {param($SeedRun) $model.seedCleanup++} + function Close-CapPeer {param($PeerRun,$Label) $model.peerCleanup++} function Invoke-CapMatchedRestartCapture {param($Phase,$Fixture) $model.restart++} Invoke-CapMatchedScenes -Phase 'after' Assert-CapTest ($captured.Count -eq 17 -and $seeded.Count -eq 11 -and $model.restart -eq 1 -and $model.hover -eq 1 -and ($captured.scene -join ',') -ceq (((Get-CapMatchedSceneNames)[0..16]) -join ',')) 'Actual executor lost a scene, seed, Restart dispatch or hover.' + $captured.Clear();$seeded.Clear();$model.restart=0;$model.hover=0;$model.selections.Clear() + $focusedScenes=@( + 's-tabs-agent-unknown','s-tabs-backend-idle','s-tabs-goal-needs-you','s-tabs-loop-running', + 's-tabs-two-agent-selected','s-tabs-two-shell-selected','s-tabs-split-right-focused', + 's-tabs-many-first','s-tabs-overflow-last','s-tabs-hover-close','s-tabs-last-guard' + ) + $scene='s-tabs-agent-unknown' + $legacyRows=@(Read-CapMatchedRows -AppRun ([pscustomobject]@{ + Run=[pscustomobject]@{Identity=[pscustomobject]@{Pid=4242}} + })) + $legacyControls=@($legacyRows | Where-Object {$_.Name -cin @('Split Right','Split Down','New Tab')}) + Assert-CapTest ($legacyControls.Count -eq 0) ` + 'Legacy wrapper mismatch no longer reproduces: matched rows unexpectedly returned chrome controls.' + $focusedCapture=$true + Invoke-CapMatchedScenes -Phase 'after' -SceneNames $focusedScenes -SkipRestart + Assert-CapTest ($captured.Count -eq 11 -and $seeded.Count -eq 11 -and $model.restart -eq 0 -and + $model.chromeReads -eq 11 -and + @($captured | Where-Object {$_.observation.chromeControls.Count -ne 3}).Count -eq 0 -and + ($captured.scene -join ',') -ceq ($focusedScenes -join ',')) ` + 'Focused executor did not reuse the canonical scene setup and assertions for all eleven F2 scenes.' + $focusedSceneCount=$captured.Count;$focusedSeedCount=$seeded.Count + $focusedRestartCount=$model.restart;$focusedHoverCount=$model.hover $many=@($captured | Where-Object scene -CEQ 's-tabs-many-first')[0] $manySelections=@($model.selections | Where-Object {$_ -cmatch '^s-tabs-many-first:'}) + $captured.Clear();$seeded.Clear();$model.appCleanup=0;$model.seedCleanup=0;$model.peerCleanup=0 + $model.captureFailure=$true + $cleanupFailure=Assert-CapRejects { + Invoke-CapMatchedScenes -Phase 'after' -SceneNames $focusedScenes -SkipRestart + } 'Focused image failure injected' + Assert-CapTest ($model.appCleanup -ge 1 -and $model.seedCleanup -eq 1 -and $model.peerCleanup -eq 1) ` + 'Focused scene failure did not run canonical app, seed and peer cleanup.' + $model.captureFailure=$false;$model.deadlineFailure=$true + $deadlineFailure=Assert-CapRejects { + Invoke-CapMatchedScenes -Phase 'after' -SceneNames $focusedScenes -SkipRestart + } 'Focused deadline injected' + $model.deadlineFailure=$false Assert-CapTest (($many.observation.tabs.visible.index -join ',') -ceq '0,1,2,3,4,5,6,7' -and ($manySelections -join ',') -ceq 's-tabs-many-first:4,s-tabs-many-first:0') ` 'Many-first did not bridge through the exact lowest visible tab and reacquire the requested first tab.' @@ -4154,12 +4298,54 @@ function Invoke-CapMatchedSceneRoutingContract { }) -TotalTabs 12 -SelectedTab 11 -TargetIndex 0 -AgentTitle 'Copilot CLI' ` -SourceCommit $job.binaries.after.source -ExpectedDpi 96 -DpiBase 96 } 'remained genuinely unavailable after exact overflow bridge 4' - return @{scenes=$captured.Count;seedDispatches=$seeded.Count;realRestartDispatches=$model.restart;hoverDispatches=$model.hover + return @{scenes=$focusedSceneCount;seedDispatches=$focusedSeedCount + realRestartDispatches=$focusedRestartCount;hoverDispatches=$focusedHoverCount + legacyExpectedControls=3;legacyObservedControls=$legacyControls.Count + cleanupFailure=$cleanupFailure;deadlineFailure=$deadlineFailure manyFirstSelections=$manySelections;unavailableFailure=$unavailable evidence='ACTUAL_WORKER_FUNCTION_SIMULATED_DEPENDENCIES_NO_GUI_NO_PROVIDER_NO_IMAGES'} } $selectionFunction.Extent.Text $function.Extent.Text } +function Invoke-CapFocusedPrelaunchPathContract { + $caseRoot=Join-Path $scratch 'focused-prelaunch-paths' + $output=Join-Path $caseRoot 'outputs' + $run=Join-Path $caseRoot 'runs\one' + $journal=Join-Path $run 'process-journal' + $null=New-Item -ItemType Directory -Path $output,$journal -Force + $files=[ordered]@{ + authorizationPath=Join-Path $run 'supervisor-authorization.json' + jobPath=Join-Path $caseRoot 'job.json' + preflightPath=Join-Path $caseRoot 'preflight.json' + authorityPath=Join-Path $caseRoot 'focused-authorization.json' + } + foreach ($path in $files.Values) {[IO.File]::WriteAllText($path,'{}',[Text.UTF8Encoding]::new($false))} + $proof=& { + $outputDirectory=$output + $runDirectory=$run + . $workerPath -OwnedRoot $caseRoot -DefinitionsOnly + Assert-CapTest ($null -eq $outputDirectory -and $null -eq $runDirectory) ` + 'Legacy pre-import path ordering no longer reproduces its reset-to-null failure.' + $paths=[pscustomobject]@{ + outputDirectory=$output;runDirectory=$run;processJournalPath=$journal + authorizationPath=$files.authorizationPath;jobPath=$files.jobPath + preflightPath=$files.preflightPath;authorityPath=$files.authorityPath + } + Assert-CapFocusedPrelaunchPaths -Root $caseRoot -Paths $paths + } + foreach ($mutation in @('null','outside','missing')) { + $changed=$proof | ConvertTo-Json -Depth 10 | ConvertFrom-Json -DateKind String + switch ($mutation) { + 'null' {$changed.jobPath=''} + 'outside' {$changed.authorityPath=Join-Path (Split-Path -Parent $caseRoot) 'foreign.json'} + 'missing' {$changed.preflightPath=Join-Path $caseRoot 'missing.json'} + } + $null=Assert-CapRejects {Assert-CapFocusedPrelaunchPaths -Root $caseRoot -Paths $changed} 'Focused prelaunch' + } + return @{validated=@($proof.PSObject.Properties.Name);mutations=@('null','outside','missing') + initializationOrder='WORKER_DEFINITIONS_FIRST_PATH_BINDING_SECOND'} +} + function Invoke-CapMatchedRestartRouteContract { $ast=[Management.Automation.Language.Parser]::ParseFile($workerPath,[ref]$null,[ref]$null) $function=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and @@ -4401,7 +4587,38 @@ function Invoke-CapMatchedManifestContract { } $null=Assert-CapRejects {Assert-CapMatchedManifest -Manifest $m -Job $job} $(if ($case -ceq 'ack-only') {'Restart evidence'} else {'Matched manifest'}) } - return @{positive=1;negative=6;images=36;realRestarts=2;evidence='SIMULATED_MANIFEST_NOT_CAPTURE'} + $focusedJob=New-CapMatchedDpiTestJob + $focusedJob | Add-Member -NotePropertyName focusedCapture -NotePropertyValue ([pscustomobject]@{ + schemaVersion=1L;phases=@('after');sceneNames=@(Get-CapMatchedFocusedSceneNames) + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2';officialParityEvidence=$false + }) + $focused=@{ + sceneAdapter='S_MATCHED_V12_DPI_V1';lane='S';status='COMPLETE' + executionMode='FOCUSED_LIVE_CAPTURE' + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + officialParityEvidence=$false + backendTurns=0;g11Qualification='ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED' + images=@(foreach ($scene in Get-CapMatchedFocusedSceneNames) { + @{phase='after';scene=$scene;captureMode='PRINTWINDOW';observedState=@{ + readSucceeded=$true;evidenceMode='OWNED_UIA_READ_NOT_VISUAL_PARITY'}} + }) + realRestarts=@() + } | ConvertTo-Json -Depth 35 | ConvertFrom-Json -DateKind String + Assert-CapMatchedManifest -Manifest $focused -Job $focusedJob + $focusedBase=$focused | ConvertTo-Json -Depth 35 + foreach ($case in @('missing-focused','extra-focused','wrong-classification','parity-overclaim','unexpected-restart')) { + $changed=$focusedBase | ConvertFrom-Json -DateKind String + switch ($case) { + 'missing-focused' {$changed.images=@($changed.images[1..10])} + 'extra-focused' {$changed.images+= $changed.images[0]} + 'wrong-classification' {$changed.evidenceClassification='C8R_RDP_DPI192_QUALIFIED'} + 'parity-overclaim' {$changed.officialParityEvidence=$true} + 'unexpected-restart' {$changed.realRestarts=@(@{phase='after';evidence=(New-CapRestartTestEvidence)})} + } + $null=Assert-CapRejects {Assert-CapMatchedManifest -Manifest $changed -Job $focusedJob} 'Matched manifest' + } + return @{positive=2;negative=11;images=36;realRestarts=2;focusedImages=11;focusedRestarts=0 + evidence='SIMULATED_MANIFEST_NOT_CAPTURE'} } function Invoke-CapDContracts { diff --git a/Tools/windows/UI-CAPTURE-SCENARIOS.md b/Tools/windows/UI-CAPTURE-SCENARIOS.md index bb20408e..8063d8d8 100644 --- a/Tools/windows/UI-CAPTURE-SCENARIOS.md +++ b/Tools/windows/UI-CAPTURE-SCENARIOS.md @@ -27,7 +27,8 @@ including all eight S groups and the unchanged fourteen-subcase G group once. The historical standalone Lane S totals below describe its own delta, not the current combined-tree qualification. A later C8 preparation must re-pin merged tooling and rerun its exact declared qualification count before preflight and -a separately granted capture window. No subset or skipped-scene support is added. +a separately granted capture window. The focused mode below is a distinct, +non-C8R current-source capture and cannot satisfy that qualification. ## Scene-bound seed receipts @@ -204,6 +205,49 @@ not physical input, local-console compositor equivalence, native local glyph or antialiasing fidelity, focus outside the observed RDP session, or automatic row validation. Unmatched scenes remain logical-geometry-only or unassessable. +### Focused current-source matched capture + +`FOCUSED_LIVE_CAPTURE` runs an exact validated subset through the same +`Invoke-CapMatchedScenes` implementation used by the full matched adapter. It +does not copy scene setup, UIA assertions, provider ownership, image capture, or +cleanup into a wrapper. The only admitted plan is the after/current-source F2 +terminal-chrome set: + +`agent-unknown`, `backend-idle`, `goal-needs-you`, `loop-running`, +`two-agent-selected`, `two-shell-selected`, `split-right-focused`, +`many-first`, `overflow-last`, `hover-close`, and `last-guard`. + +The mode is permanently classified +`F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2` with +`officialParityEvidence=false`. Missing, extra, duplicate, reordered, foreign, +or pre-restart scenes refuse. It still requires the exact 185-case real-zmx +preflight, a clean current source head/tree, hash-bound current binaries, PMv2 +physical admission, raw unresampled geometry, individual image privacy review, +the numbered process journal, and zero-survivor release. + +Prepare a fresh short root only after the source and controls are committed and +the 185-case report exists: + +```powershell +pwsh -NoProfile -File Tools\windows\Prepare-FocusedUiCapture.ps1 ` + -Repository $PWD -OwnedRoot C:\f7 ` + -GraphCodeBinary graphcode-windows\zig-out\bin\graphcode-windows.exe ` + -ZmxBinary $env:GRAPHCODE_ZMX_ROOT\zig-out\bin\zmx.exe ` + -DaemonRuntimeRoot C:\k8\runtime ` + -BackendFixtureBinary C:\k8\runtime\copilot.exe ` + -OfflinePeerSource C:\k8\inputs\offline-peer-source.ps1 ` + -PreparedFixtureRoot C:\k8\fixtures ` + -PreflightReport C:\fresh-preflight\report.json +``` + +Use the returned paths and hashes with `Invoke-UiCapture.ps1 +-ExecutionMode FOCUSED_LIVE_CAPTURE`, including +`-FocusedAuthorizationPath`/`-FocusedAuthorizationSha256` and the normal exact +job, fixture-mapping, and preflight arguments. Preparation creates no process +or image. The supervisor imports the canonical controls before binding and +validating every prelaunch path; it never dot-sources the worker into a +path-owning wrapper scope. + Matched S seed receipts use the initialized `Roots.Root\zmx-seed.json`, not a path rebuilt from the diagnostic phase label. B and D retain their existing normal and per-session receipt layouts (D seeds share an app root). That directory is diff --git a/Tools/windows/UiCaptureDriver.psm1 b/Tools/windows/UiCaptureDriver.psm1 index b6101b52..005b5126 100644 --- a/Tools/windows/UiCaptureDriver.psm1 +++ b/Tools/windows/UiCaptureDriver.psm1 @@ -2124,7 +2124,8 @@ function Assert-CapMatchedDpiImage { [CmdletBinding()] param( [Parameter(Mandatory)]$Image, - [Parameter(Mandatory)]$Contract + [Parameter(Mandatory)]$Contract, + [Parameter()][string]$ExpectedEvidenceClassification = 'C8R_RDP_DPI192_QUALIFIED' ) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters @@ -2142,7 +2143,7 @@ function Assert-CapMatchedDpiImage { throw 'DPI-parametric matched image phase identity differs.' } $identity = $Contract.windowIdentities.($Image.phase) - if ($Image.evidenceClassification -cne 'C8R_RDP_DPI192_QUALIFIED' -or + if ($Image.evidenceClassification -cne $ExpectedEvidenceClassification -or $Image.geometrySchemaVersion -ne 3 -or $Image.geometryDerivation -cne 'AdjustWindowRectExForDpi' -or ($Image.logicalClientWH -join ',') -cne ($Contract.logicalClientWH -join ',') -or ($Image.physicalClientBounds -join ',') -cne @@ -3599,6 +3600,74 @@ function Get-CapMatchedSceneNames { 's-tabs-last-guard','s-restart-confirmed') } +function Get-CapMatchedFocusedSceneNames { + return @('s-tabs-agent-unknown','s-tabs-backend-idle','s-tabs-goal-needs-you','s-tabs-loop-running', + 's-tabs-two-agent-selected','s-tabs-two-shell-selected','s-tabs-split-right-focused', + 's-tabs-many-first','s-tabs-overflow-last','s-tabs-hover-close','s-tabs-last-guard') +} + +function Get-CapMatchedCapturePlan { + param([Parameter(Mandatory)]$Job) + if (-not $Job.PSObject.Properties['focusedCapture']) { + return [pscustomobject]@{ + phases=@('before','after') + sceneNames=@((Get-CapMatchedSceneNames)[0..16]) + includeRestart=$true + executionMode='LIVE_CAPTURE' + evidenceClassification=$(if ($Job.sceneAdapter -ceq 'S_MATCHED_V12_DPI_V1') { + 'C8R_RDP_DPI192_QUALIFIED' + } else { $null }) + officialParityEvidence=$null + } + } + $focused=$Job.focusedCapture + Assert-CapDataRecord $focused @{schemaVersion=[long];phases=[array];sceneNames=[array] + evidenceClassification=[string];officialParityEvidence=[bool]} 'Focused matched capture' + $expected=Get-CapMatchedFocusedSceneNames + if ($focused.schemaVersion -ne 1 -or ($focused.phases -join ',') -cne 'after' -or + ($focused.sceneNames -join ',') -cne ($expected -join ',') -or + @($focused.sceneNames | Sort-Object -Unique).Count -ne $expected.Count -or + $focused.evidenceClassification -cne 'F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' -or + $focused.officialParityEvidence -cne $false -or + $Job.sceneAdapter -cne 'S_MATCHED_V12_DPI_V1') { + throw 'Focused matched capture phases, scenes, classification or non-parity status differ.' + } + return [pscustomobject]@{ + phases=@('after') + sceneNames=@($expected) + includeRestart=$false + executionMode='FOCUSED_LIVE_CAPTURE' + evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2' + officialParityEvidence=$false + } +} + +function Assert-CapFocusedPrelaunchPaths { + param([Parameter(Mandatory)][string]$Root,[Parameter(Mandatory)]$Paths) + Assert-CapDataRecord $Paths @{outputDirectory=[string];runDirectory=[string] + processJournalPath=[string];authorizationPath=[string];jobPath=[string] + preflightPath=[string];authorityPath=[string]} 'Focused prelaunch paths' + $resolved=[ordered]@{} + foreach ($name in @('outputDirectory','runDirectory','processJournalPath','authorizationPath', + 'jobPath','preflightPath','authorityPath')) { + $path=[string]$Paths.$name + if ([string]::IsNullOrWhiteSpace($path)) {throw "Focused prelaunch path is null: $name."} + try {$resolved[$name]=Assert-CapArtifactPath -Root $Root -Path $path} + catch {throw "Focused prelaunch path is invalid: $name. $($_.Exception.Message)"} + } + foreach ($name in @('outputDirectory','runDirectory','processJournalPath')) { + if (-not (Test-Path -LiteralPath $resolved[$name] -PathType Container)) { + throw "Focused prelaunch directory is missing: $name." + } + } + foreach ($name in @('authorizationPath','jobPath','preflightPath','authorityPath')) { + if (-not (Test-Path -LiteralPath $resolved[$name] -PathType Leaf)) { + throw "Focused prelaunch file is missing: $name." + } + } + return [pscustomobject]$resolved +} + function Assert-CapMatchedFixture { param([Parameter(Mandatory)]$Fixture,[Parameter(Mandatory)][string]$ProjectRoot) Assert-CapDataRecord $Fixture @{schemaVersion=[long];sceneAdapter=[string];macSource=[string] @@ -3765,6 +3834,57 @@ function Get-CapMatchedTabObservation { evidenceMode='OWNED_UIA_STATE_NOT_GLYPHS_OR_VISUAL_PARITY';hover='VISUAL_ONLY_NOT_EXPOSED_BY_UIA'} } +function Get-CapMatchedChromeControlObservation { + param([Parameter(Mandatory)][object[]]$Rows,[Parameter(Mandatory)][int]$ExpectedProcessId, + [Parameter(Mandatory)][long]$ExpectedDpi,[Parameter(Mandatory)][long]$DpiBase) + if ($ExpectedDpi -le 0 -or $DpiBase -le 0) { + throw 'Matched chrome controls require a positive DPI geometry contract.' + } + $expectedWidth=[long][Math]::Truncate((24L*$ExpectedDpi+[Math]::Truncate($DpiBase/2))/$DpiBase) + $expectedHeight=[long][Math]::Truncate((22L*$ExpectedDpi+[Math]::Truncate($DpiBase/2))/$DpiBase) + $specs=@( + [pscustomobject]@{identity='workspace-split-right:control';name='Split Right'}, + [pscustomobject]@{identity='workspace-split-down:control';name='Split Down'}, + [pscustomobject]@{identity='workspace-new-tab:control';name='New Tab'} + ) + $expectedIds=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($spec in $specs) {$null=$expectedIds.Add((Get-CapMatchedAutomationId $spec.identity))} + if ($Rows.Count -ne 3 -or @($Rows | Where-Object {-not $expectedIds.Contains($_.AutomationId)}).Count) { + throw 'Matched chrome controls require exactly Split Right, Split Down and New Tab.' + } + $observed=[Collections.Generic.List[object]]::new() + foreach ($spec in $specs) { + $id=Get-CapMatchedAutomationId $spec.identity + $matches=@($Rows | Where-Object AutomationId -CEQ $id) + if ($matches.Count -ne 1) {throw 'Matched chrome controls contain a missing or duplicate stable identity.'} + $row=$matches[0] + if ($row.Name -cne $spec.name -or $row.ProcessId -isnot [long] -or + $row.ProcessId -ne $ExpectedProcessId -or $row.ParentAutomationId -cne 'graph' -or + $row.ControlType -cne 'Button' -or $row.Invokable -isnot [bool] -or -not $row.Invokable -or + $row.IsEnabled -isnot [bool] -or -not $row.IsEnabled -or + $row.IsOffscreen -isnot [bool] -or $row.IsOffscreen -or + $row.Bounds -isnot [array] -or $row.Bounds.Count -ne 4) { + throw 'Matched chrome control identity, owner, type or visibility differs.' + } + $width=[long]$row.Bounds[2]-[long]$row.Bounds[0] + $height=[long]$row.Bounds[3]-[long]$row.Bounds[1] + if ($width -ne $expectedWidth -or $height -ne $expectedHeight) { + throw 'Matched chrome control DPI-scaled 24x22 logical geometry differs.' + } + $observed.Add([pscustomobject]@{automationId=$id;name=$spec.name;bounds=@($row.Bounds) + physicalWidth=$width;physicalHeight=$height + logicalWidth=[Math]::Round($width*$DpiBase/$ExpectedDpi,3) + logicalHeight=[Math]::Round($height*$DpiBase/$ExpectedDpi,3) + controlType='Button';invokable=$true}) + } + for ($i=1;$i -lt $observed.Count;$i++) { + if ($observed[$i].bounds[0] -le $observed[$i-1].bounds[2]) { + throw 'Matched chrome controls overlap or are out of order.' + } + } + return @($observed.ToArray()) +} + function Assert-CapRestartEvidence { param([Parameter(Mandatory)]$Evidence,[Parameter(Mandatory)][string]$ExpectedDaemonSha256) $fail={param($Reason) throw "Restart evidence: $Reason"} @@ -3981,16 +4101,32 @@ function Read-CapRestartTranscript { function Assert-CapMatchedManifest { param([Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)]$Job) - $expected=@(foreach ($phase in @('before','after')) {foreach ($scene in Get-CapMatchedSceneNames) {"${phase}:$scene"}}) + $plan=Get-CapMatchedCapturePlan -Job $Job + $manifestClassification=if ($Manifest.PSObject.Properties['evidenceClassification']) { + $Manifest.evidenceClassification + } else {$null} + $manifestOfficial=if ($Manifest.PSObject.Properties['officialParityEvidence']) { + $Manifest.officialParityEvidence + } else {$null} + $expected=@(foreach ($phase in $plan.phases) { + foreach ($scene in $plan.sceneNames) {"${phase}:$scene"} + if ($plan.includeRestart) {"${phase}:s-restart-confirmed"} + }) $actual=@($Manifest.images | ForEach-Object {"$($_.phase):$($_.scene)"}) if ($Job.sceneAdapter -cnotin @('S_MATCHED_V12_V1','S_MATCHED_V12_DPI_V1') -or $Manifest.sceneAdapter -cne $Job.sceneAdapter -or $Manifest.lane -cne 'S' -or - $Manifest.status -cne 'COMPLETE' -or $Manifest.executionMode -cne 'LIVE_CAPTURE' -or - $actual.Count -ne 36 -or @($actual | Sort-Object -Unique).Count -ne 36 -or + $Manifest.status -cne 'COMPLETE' -or $Manifest.executionMode -cne $plan.executionMode -or + ($null -ne $plan.officialParityEvidence -and + ($manifestClassification -cne $plan.evidenceClassification -or + $manifestOfficial -cne $plan.officialParityEvidence)) -or + $actual.Count -ne $expected.Count -or @($actual | Sort-Object -Unique).Count -ne $expected.Count -or (($actual | Sort-Object) -join ',') -cne (($expected | Sort-Object) -join ',') -or $Manifest.g11Qualification -cne 'ASSESSABLE_SOURCE_CAPABILITY_NOT_RUNTIME_VALIDATED' -or - $Manifest.backendTurns -ne 0 -or $Manifest.realRestarts.Count -ne 2) {throw 'Matched manifest inventory or evidence qualification differs.'} - foreach ($phase in @('before','after')) { + $Manifest.backendTurns -ne 0 -or + $Manifest.realRestarts.Count -ne $(if ($plan.includeRestart) {$plan.phases.Count} else {0})) { + throw 'Matched manifest inventory or evidence qualification differs.' + } + foreach ($phase in $(if ($plan.includeRestart) {$plan.phases} else {@()})) { $restarts=@($Manifest.realRestarts | Where-Object phase -CEQ $phase) if ($restarts.Count -ne 1) {throw 'Matched manifest must retain one real restart per phase.'} $null=Assert-CapRestartEvidence -Evidence $restarts[0].evidence -ExpectedDaemonSha256 $Job.binaries.daemon.sha256 @@ -4066,7 +4202,12 @@ function Assert-CapSceneContract { throw "DPI-parametric $phase source window identity differs." } } - if ($contract.windowIdentities.before.sourceCommit -cne 'a8b09434fe41a61030fd290f7d065ec6d0db8005' -or + if ($Job.PSObject.Properties['focusedCapture']) { + if ($contract.windowIdentities.before.sourceCommit -cne $Job.head -or + $contract.windowIdentities.after.sourceCommit -cne $Job.head) { + throw 'Focused DPI-parametric current-source window identity differs.' + } + } elseif ($contract.windowIdentities.before.sourceCommit -cne 'a8b09434fe41a61030fd290f7d065ec6d0db8005' -or $contract.windowIdentities.before.sourceCommit -ceq $contract.windowIdentities.after.sourceCommit) { throw 'DPI-parametric historical baseline window identity differs.' } @@ -4095,6 +4236,7 @@ function Assert-CapSceneContract { if ($Job.binaries.backendFixture.sourceSha256 -ne $sourceHash) { throw 'DPI-parametric matched scene requires its canonical harmless backend source hash.' } + $null=Get-CapMatchedCapturePlan -Job $Job return } if ($Job.lane -ceq 'S' -and $Job.sceneAdapter -ceq 'S_MATCHED_V12_V1') { @@ -4127,6 +4269,7 @@ function Assert-CapSceneContract { $sourceHash=[Convert]::ToHexString([Security.Cryptography.SHA256]::HashData( [Text.UTF8Encoding]::new($false).GetBytes((Get-CapMatchedBackendSource)))) if ($Job.binaries.backendFixture.sourceSha256 -ne $sourceHash) {throw 'Matched scene requires its canonical harmless backend source hash.'} + $null=Get-CapMatchedCapturePlan -Job $Job return } if ($Job.lane -ceq 'D' -and $Job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') { @@ -4635,6 +4778,60 @@ function Assert-CapSourceArchive { return [pscustomobject]@{ tree = $tree; files = $expected.Count; gitlinks = @($gitlinks) } } +function Assert-CapFocusedJobIdentity { + [CmdletBinding()] + param([Parameter(Mandatory)][string]$Root,[Parameter(Mandatory)]$Job, + [Parameter(Mandatory)]$Authorization,[Parameter(Mandatory)][string]$JobSha256) + Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters + Assert-CapDataRecord $Job @{lane=[string];head=[string];base=[string];captureTree=[string] + sourceRepository=[string];binaries=[pscustomobject]} 'Focused job identity' + Assert-CapDataRecord $Authorization @{authorizationId=[string];lane=[string];head=[string] + sourceTree=[string];jobManifestSha256=[string];beforeBinarySha256=[string] + afterBinarySha256=[string]} 'Focused authorization identity' + $plan=Get-CapMatchedCapturePlan -Job $Job + if ($plan.executionMode -cne 'FOCUSED_LIVE_CAPTURE' -or + $Authorization.lane -cne $Job.lane -or $Authorization.head -cne $Job.head -or + $Authorization.sourceTree -cne $Job.captureTree -or + $Authorization.jobManifestSha256 -ne $JobSha256 -or $Job.base -cne $Job.head) { + throw 'Focused authorization does not bind the exact current source job.' + } + $repository=[IO.Path]::GetFullPath($Job.sourceRepository) + if (-not [IO.Path]::IsPathFullyQualified($Job.sourceRepository) -or + $Job.sourceRepository -notmatch '^[A-Za-z]:\\' -or $Job.sourceRepository.Substring(2).Contains(':') -or + $Job.sourceRepository -match '(^|\\)\.\.?($|\\)') { + throw 'Focused source repository must be an explicit safe local path.' + } + $ancestor=$repository + while ($ancestor) { + if ((Get-Item -LiteralPath $ancestor -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { + throw 'Focused source repository has a reparse-point ancestor.' + } + $ancestor=Split-Path -Parent $ancestor + } + if ([string](Invoke-CapAdmissionGit $repository @('rev-parse','HEAD')) -cne $Job.head -or + [string](Invoke-CapAdmissionGit $repository @('rev-parse','HEAD^{tree}')) -cne $Job.captureTree -or + @(Invoke-CapAdmissionGit $repository @('status','--porcelain','--untracked-files=all')).Count -ne 0) { + throw 'Focused source repository is not clean at the exact source head and tree.' + } + foreach ($phase in @('before','after')) { + $binary=$Job.binaries.$phase + Assert-CapDataRecord $binary @{path=[string];sha256=[string];source=[string] + binaryBuiltAtSource=[string]} "Focused $phase binary" + $path=Assert-CapArtifactPath -Root $Root -Path $binary.path + if ($binary.source -cne $Job.head -or $binary.binaryBuiltAtSource -cne $Job.head -or + $binary.sha256 -cnotmatch '^[A-Fa-f0-9]{64}$' -or + (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $binary.sha256 -or + $Authorization."${phase}BinarySha256" -ne $binary.sha256) { + throw "Focused $phase binary does not bind the exact current source head." + } + } + return [pscustomobject]@{ + head=$Job.head;tree=$Job.captureTree;lane=$Job.lane;sourceRepository=$repository + phases=@($plan.phases);scenes=@($plan.sceneNames) + attestation='FOCUSED_CURRENT_CLEAN_SOURCE_AND_HASH_BOUND_BINARIES' + } +} + function Assert-CapJobIdentity { [CmdletBinding()] param([Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)]$Job, @@ -5739,8 +5936,11 @@ function Assert-CapNoWorkerTransientArtifacts { } Export-ModuleMember -Function @( - 'Get-CapMatchedSceneNames','Assert-CapMatchedFixture','Get-CapMatchedAutomationId', - 'Get-CapMatchedVisibleTabIndexes','Get-CapMatchedTabObservation','Assert-CapRestartEvidence','Initialize-CapMatchedProxy', + 'Get-CapMatchedSceneNames','Get-CapMatchedFocusedSceneNames','Get-CapMatchedCapturePlan', + 'Assert-CapFocusedPrelaunchPaths', + 'Assert-CapMatchedFixture','Get-CapMatchedAutomationId', + 'Get-CapMatchedVisibleTabIndexes','Get-CapMatchedTabObservation','Get-CapMatchedChromeControlObservation', + 'Assert-CapRestartEvidence','Initialize-CapMatchedProxy', 'Read-CapRestartTranscript','Assert-CapMatchedManifest','Get-CapMatchedBackendSource', 'Get-CapDSceneNames', 'Assert-CapDFixture', @@ -5776,7 +5976,7 @@ Export-ModuleMember -Function @( 'Assert-CapFixtureMapping', 'Read-CapBoundJson', 'Assert-CapDataRecord', - 'Assert-CapJobIdentity', + 'Assert-CapJobIdentity','Assert-CapFocusedJobIdentity', 'Assert-CapSceneContract', 'Assert-CapDescendantLifetime', 'Test-CapGraphAppRootRole', From ed8bcac4372aa5024d4b3b5b40efbaa49c0c42c5 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Wed, 28 Oct 2026 09:55:00 -0700 Subject: [PATCH 2/4] Fix focused authorization duration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/Prepare-FocusedUiCapture.ps1 | 5 +++-- Tools/windows/Tests/UiCaptureDriver.Tests.ps1 | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Tools/windows/Prepare-FocusedUiCapture.ps1 b/Tools/windows/Prepare-FocusedUiCapture.ps1 index 3fcf635a..32c94ff7 100644 --- a/Tools/windows/Prepare-FocusedUiCapture.ps1 +++ b/Tools/windows/Prepare-FocusedUiCapture.ps1 @@ -150,6 +150,7 @@ $jobPath=Join-Path $root 'job.json' [IO.File]::WriteAllText($jobPath,($job | ConvertTo-Json -Depth 40),[Text.UTF8Encoding]::new($false)) $jobHash=(Get-FileHash -LiteralPath $jobPath -Algorithm SHA256).Hash $now=[DateTimeOffset]::UtcNow +$granted=$now.AddSeconds(-1) $authorization=[ordered]@{ schemaVersion=1L authorizationId=[guid]::NewGuid().ToString() @@ -173,8 +174,8 @@ $authorization=[ordered]@{ afterBinarySha256=$binaryHash sceneNames=@(Get-CapMatchedFocusedSceneNames) phases=@('after') - grantedUtc=$now.AddSeconds(-1).ToString('o') - expiresUtc=$now.AddMinutes($AuthorizationMinutes).ToString('o') + grantedUtc=$granted.ToString('o') + expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString('o') maxMinutes=$AuthorizationMinutes } $authorizationPath=Join-Path $root 'focused-authorization.json' diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index 9d136060..ca2c5076 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -2987,7 +2987,8 @@ function Invoke-CapMatchedContracts { $supervisorSource.Contains('Assert-CapFocusedPrelaunchPaths') -and $workerSource.Contains('function Read-CapMatchedChromeControls') -and $workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and - $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'")) ` + $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'") -and + $prepareSource.Contains('expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString(''o'')')) ` 'Focused supervisor, worker chrome reader or checked-in preparation entry point is not wired.' $focusedMutations=@('missing','extra','wrong-scene','wrong-phase','wrong-classification','parity-overclaim') foreach ($mutation in $focusedMutations) { From 19c1b1de2a753ca0608cebc001a2d8637e088d98 Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Wed, 28 Oct 2026 09:58:15 -0700 Subject: [PATCH 3/4] Prepare fresh focused fixture projects Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/Prepare-FocusedUiCapture.ps1 | 5 ++--- Tools/windows/Tests/UiCaptureDriver.Tests.ps1 | 4 +++- Tools/windows/UI-CAPTURE-SCENARIOS.md | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tools/windows/Prepare-FocusedUiCapture.ps1 b/Tools/windows/Prepare-FocusedUiCapture.ps1 index 32c94ff7..0e437731 100644 --- a/Tools/windows/Prepare-FocusedUiCapture.ps1 +++ b/Tools/windows/Prepare-FocusedUiCapture.ps1 @@ -6,7 +6,6 @@ param( [Parameter(Mandatory)][string]$DaemonRuntimeRoot, [Parameter(Mandatory)][string]$BackendFixtureBinary, [Parameter(Mandatory)][string]$OfflinePeerSource, - [Parameter(Mandatory)][string]$PreparedFixtureRoot, [Parameter(Mandatory)][string]$PreflightReport, [ValidateRange(3,30)][long]$AuthorizationMinutes = 20 ) @@ -29,7 +28,7 @@ $workerPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCaptureWorker.ps1' $supervisorPath=Join-Path $repositoryRoot 'Tools\windows\Invoke-UiCapture.ps1' foreach ($path in @($modulePath,$workerPath,$supervisorPath,$GraphCodeBinary,$ZmxBinary, (Join-Path $DaemonRuntimeRoot 'graphcoded.exe'),$BackendFixtureBinary,$OfflinePeerSource, - (Join-Path $PreparedFixtureRoot 'projects'),$PreflightReport)) { + $PreflightReport)) { if (-not (Test-Path -LiteralPath $path)) {throw "Focused capture input is missing: $path"} } Import-Module $modulePath -Force -WarningAction SilentlyContinue @@ -44,7 +43,6 @@ $null=Copy-Item -LiteralPath $GraphCodeBinary -Destination (Join-Path $inputs 'g $null=Copy-Item -LiteralPath $ZmxBinary -Destination (Join-Path $runtime 'zmx.exe') $null=Copy-Item -LiteralPath $BackendFixtureBinary -Destination (Join-Path $runtime 'copilot.exe') $null=Copy-Item -LiteralPath $OfflinePeerSource -Destination (Join-Path $inputs 'offline-peer-source.ps1') -$null=Copy-Item -LiteralPath (Join-Path $PreparedFixtureRoot 'projects') -Destination $fixtures -Recurse foreach ($file in Get-ChildItem -LiteralPath $DaemonRuntimeRoot -File) { if ($file.Name -cin @('zmx.exe','copilot.exe')) {continue} $null=Copy-Item -LiteralPath $file.FullName -Destination (Join-Path $runtime $file.Name) @@ -54,6 +52,7 @@ $copiedFixtureSource=Join-Path $inputs 'fixture-source.json' $null=Copy-Item -LiteralPath $fixtureSource -Destination $copiedFixtureSource $fixturePath=Join-Path $fixtures 'fixture.json' $destinationProjectRoot=Join-Path $fixtures 'projects' +$null=New-Item -ItemType Directory -Path $destinationProjectRoot $fixtureText=[IO.File]::ReadAllText($copiedFixtureSource).Replace( 'C:\\fixture',(($destinationProjectRoot | ConvertTo-Json -Compress).Trim('"'))) [IO.File]::WriteAllText($fixturePath,$fixtureText,[Text.UTF8Encoding]::new($false)) diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index ca2c5076..9f7b2150 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -2988,7 +2988,9 @@ function Invoke-CapMatchedContracts { $workerSource.Contains('function Read-CapMatchedChromeControls') -and $workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'") -and - $prepareSource.Contains('expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString(''o'')')) ` + $prepareSource.Contains('expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString(''o'')') -and + $prepareSource.Contains('$null=New-Item -ItemType Directory -Path $destinationProjectRoot') -and + -not $prepareSource.Contains("Join-Path `$PreparedFixtureRoot 'projects'")) ` 'Focused supervisor, worker chrome reader or checked-in preparation entry point is not wired.' $focusedMutations=@('missing','extra','wrong-scene','wrong-phase','wrong-classification','parity-overclaim') foreach ($mutation in $focusedMutations) { diff --git a/Tools/windows/UI-CAPTURE-SCENARIOS.md b/Tools/windows/UI-CAPTURE-SCENARIOS.md index 8063d8d8..5052af5b 100644 --- a/Tools/windows/UI-CAPTURE-SCENARIOS.md +++ b/Tools/windows/UI-CAPTURE-SCENARIOS.md @@ -236,7 +236,6 @@ pwsh -NoProfile -File Tools\windows\Prepare-FocusedUiCapture.ps1 ` -DaemonRuntimeRoot C:\k8\runtime ` -BackendFixtureBinary C:\k8\runtime\copilot.exe ` -OfflinePeerSource C:\k8\inputs\offline-peer-source.ps1 ` - -PreparedFixtureRoot C:\k8\fixtures ` -PreflightReport C:\fresh-preflight\report.json ``` From 21cb6c7eb6b2cff67e246f9f996cb708d822457d Mon Sep 17 00:00:00 2001 From: Colin Neilens Date: Wed, 28 Oct 2026 10:50:01 -0700 Subject: [PATCH 4/4] Use focused image count for final status Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Colin Neilens --- Tools/windows/Invoke-UiCaptureWorker.ps1 | 17 ++++++++---- Tools/windows/Tests/UiCaptureDriver.Tests.ps1 | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Tools/windows/Invoke-UiCaptureWorker.ps1 b/Tools/windows/Invoke-UiCaptureWorker.ps1 index 137df5d8..ec708e47 100644 --- a/Tools/windows/Invoke-UiCaptureWorker.ps1 +++ b/Tools/windows/Invoke-UiCaptureWorker.ps1 @@ -103,6 +103,16 @@ function Test-CapMatchedDpiAdapter { $job.sceneAdapter -ceq 'S_MATCHED_V12_DPI_V1' } +function Get-CapWorkerExpectedImageCount { + if ($transitionRehearsal) {return 0L} + if (Test-CapMatchedAdapter) { + return [long]($matchedCapturePlan.phases.Count*($matchedCapturePlan.sceneNames.Count+ + $(if ($matchedCapturePlan.includeRestart) {1} else {0}))) + } + if ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {return 20L} + return 10L +} + function Write-CapJsonAtomic { param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)]$Value) Write-CapTrace -Command $MyInvocation.MyCommand.Name -Parameters $PSBoundParameters @@ -2566,10 +2576,7 @@ try { elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {Invoke-CapDScenes -Phase $phase} else {Invoke-CapNormalScenes -Phase $phase;Invoke-CapActionsScene -Phase $phase} } - $expectedImages=if ($transitionRehearsal) {0} elseif (Test-CapMatchedAdapter) { - $matchedCapturePlan.phases.Count*($matchedCapturePlan.sceneNames.Count+ - $(if ($matchedCapturePlan.includeRestart) {1} else {0})) - } elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {20} else {10} + $expectedImages=Get-CapWorkerExpectedImageCount $expectedSeeds=if (Test-CapMatchedAdapter) { $matchedCapturePlan.phases.Count*@($matchedCapturePlan.sceneNames | Where-Object {$_ -cmatch '^s-tabs-'}).Count } elseif ($job.sceneAdapter -ceq 'D_QUICK_CHAT_V1') {4} else {2} @@ -2660,7 +2667,7 @@ try { ($transitionRehearsal -and $images.Count -eq 0 -and $checkpoints.Count -eq 36 -and $zmxSeedReceipts.Count -eq 22 -and $realRestarts.Count -eq 2 -and $phaseTransitions.Count -eq 1) -or (-not $rehearsal -and -not $transitionRehearsal -and - $images.Count -eq $(if (Test-CapMatchedAdapter) {36} else {10})))) { + $images.Count -eq (Get-CapWorkerExpectedImageCount)))) { 'COMPLETE' } else { 'FAILED' } try { diff --git a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 index 9f7b2150..94dcc542 100644 --- a/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 +++ b/Tools/windows/Tests/UiCaptureDriver.Tests.ps1 @@ -2987,6 +2987,8 @@ function Invoke-CapMatchedContracts { $supervisorSource.Contains('Assert-CapFocusedPrelaunchPaths') -and $workerSource.Contains('function Read-CapMatchedChromeControls') -and $workerSource.Contains('Read-CapMatchedChromeControls -AppRun $app') -and + $workerSource.Contains('function Get-CapWorkerExpectedImageCount') -and + $workerSource.Contains('$images.Count -eq (Get-CapWorkerExpectedImageCount)') -and $prepareSource.Contains("evidenceClassification='F2_TERMINAL_CHROME_FOCUSED_VALIDATION_V2'") -and $prepareSource.Contains('expiresUtc=$granted.AddMinutes($AuthorizationMinutes).ToString(''o'')') -and $prepareSource.Contains('$null=New-Item -ItemType Directory -Path $destinationProjectRoot') -and @@ -3021,6 +3023,7 @@ function Invoke-CapMatchedContracts { budgetAcceptedSeconds=@(810,1710);budgetRefusedSeconds=1711 names=$names;focused=$plan;focusedMutations=$focusedMutations prelaunchPaths=(Invoke-CapFocusedPrelaunchPathContract) + expectedImageCounts=(Invoke-CapFocusedExpectedImageCountContract) routing=(Invoke-CapMatchedSceneRoutingContract)} } Invoke-CapTest 'matched tab observations preserve exact owned IDs parents selection and close bounds' { @@ -4349,6 +4352,29 @@ function Invoke-CapFocusedPrelaunchPathContract { initializationOrder='WORKER_DEFINITIONS_FIRST_PATH_BINDING_SECOND'} } +function Invoke-CapFocusedExpectedImageCountContract { + $ast=[Management.Automation.Language.Parser]::ParseFile($workerPath,[ref]$null,[ref]$null) + $function=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -ceq 'Get-CapWorkerExpectedImageCount'},$true) + Assert-CapTest ($null -ne $function) 'Focused expected-image count helper is missing.' + & { + param($Definition) + . ([scriptblock]::Create($Definition)) + $transitionRehearsal=$false + $job=[pscustomobject]@{sceneAdapter='S_MATCHED_V12_DPI_V1'} + $matchedCapturePlan=[pscustomobject]@{phases=@('after');sceneNames=@(1..11);includeRestart=$false} + function Test-CapMatchedAdapter {return $true} + $focused=Get-CapWorkerExpectedImageCount + $matchedCapturePlan=[pscustomobject]@{phases=@('before','after');sceneNames=@(1..17);includeRestart=$true} + $full=Get-CapWorkerExpectedImageCount + $transitionRehearsal=$true + $transition=Get-CapWorkerExpectedImageCount + Assert-CapTest ($focused -eq 11 -and $full -eq 36 -and $transition -eq 0) ` + 'Worker terminal status image counts drifted from focused/full/rehearsal plans.' + [pscustomobject]@{focused=$focused;full=$full;transition=$transition} + } $function.Extent.Text +} + function Invoke-CapMatchedRestartRouteContract { $ast=[Management.Automation.Language.Parser]::ParseFile($workerPath,[ref]$null,[ref]$null) $function=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and