From 7b2f2e45a6eeeb7a912a43ef0af57c9066a0f86a Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Tue, 22 Sep 2026 18:07:43 -0700
Subject: [PATCH 1/9] Remove x64 gate for telemetry tests
---
.github/workflows/Build.Windows.Job.yml | 3 ---
tests/scripts/README.md | 2 +-
tests/scripts/run_telemetry_etw_smoke_test.ps1 | 2 ++
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/Build.Windows.Job.yml b/.github/workflows/Build.Windows.Job.yml
index 9c1b59d4c..9572c1091 100644
--- a/.github/workflows/Build.Windows.Job.yml
+++ b/.github/workflows/Build.Windows.Job.yml
@@ -77,17 +77,14 @@ jobs:
--features "${{ matrix.features }}"
- name: Build debug executor for isolated telemetry smoke tests
- if: matrix.arch == 'x64'
run: cargo build --locked -p wxc --features test-support
- name: Run isolated telemetry consent smoke test
- if: matrix.arch == 'x64'
working-directory: ${{ github.workspace }}
shell: pwsh
run: tests\scripts\run_telemetry_consent_smoke_test.ps1 -BinDir src\target\debug
- name: Run isolated telemetry ETW smoke test
- if: matrix.arch == 'x64'
working-directory: ${{ github.workspace }}
shell: pwsh
run: tests\scripts\run_telemetry_etw_smoke_test.ps1 -BinDir src\target\debug
diff --git a/tests/scripts/README.md b/tests/scripts/README.md
index 0b8b9edf1..f2244cd88 100644
--- a/tests/scripts/README.md
+++ b/tests/scripts/README.md
@@ -47,7 +47,7 @@ Linux / macOS (`.sh`):
| `run_processcontainer_all_tests.ps1` | Process container (AppContainer / BaseContainer) primitives suite — tier probes, rw/ro/denied matrix, enumeration-only grants, UI mitigations, DACL restore, crash recovery, schema 0.8 networking. Dispatches to the per-area `run_processcontainer_*_test.ps1` scripts | `wxc-exec.exe`, `wxc-ui-probe.exe`, `plm.exe` and `winhttp-proxy-shim.exe` beside `wxc-exec.exe` |
| `T3-Workloads.ps1` | Real workloads (pwsh, git, node, python, cmd) on top of the T3 primitives. A missing interpreter is reported as a skip, not a failure | `wxc-exec.exe`; `pwsh` / `git` / `node` / `python` each optional, gating their own cases |
| `run_telemetry_consent_smoke_test.ps1` | Consent maintenance, presentation, policy, and exit-code smoke tests | Debug `wxc-exec.exe` built with `test-support` |
-| `run_telemetry_etw_smoke_test.ps1` | Isolated consent flow plus public-provider ETW capture | Debug `wxc-exec.exe` built with `test-support`; ETW tooling |
+| `run_telemetry_etw_smoke_test.ps1` | Isolated consent flow plus public-provider ETW capture | Debug `wxc-exec.exe` built with `test-support`; ETW tooling; Administrator, otherwise the test skips |
| `run_on_repeat.ps1` | Stress test (loops core tests) | `wxc-exec.exe` |
Each `run_processcontainer__test.ps1` also runs standalone against a
diff --git a/tests/scripts/run_telemetry_etw_smoke_test.ps1 b/tests/scripts/run_telemetry_etw_smoke_test.ps1
index 4eb1b93d5..8cf067f9e 100644
--- a/tests/scripts/run_telemetry_etw_smoke_test.ps1
+++ b/tests/scripts/run_telemetry_etw_smoke_test.ps1
@@ -193,6 +193,8 @@ try {
$executionTimeoutSeconds = 60
$proc = Start-Process -FilePath $wxcExe -ArgumentList '--debug', $configFile -PassThru -NoNewWindow
+ # Caching the handle keeps ExitCode readable after the process exits.
+ $null = $proc.Handle
if (-not $proc.WaitForExit($executionTimeoutSeconds * 1000)) {
Stop-Process -Id $proc.Id -Force
$proc.WaitForExit()
From 5b632a574645e54570b8be967e2ab5975d0c5ec4 Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 12:59:09 -0700
Subject: [PATCH 2/9] Add release-path telemetry consent test
The consent store location and policy key both have a test-only override
branch gated on `any(test, all(feature = "test-support", debug_assertions))`,
so the shipped release shape -- real %LocalAppData% store and real HKLM
policy key -- is never executed. `cargo test --release` does not close this:
`cfg(test)` is on there, so the compiled function still contains the override.
Add a test that drives a release wxc-exec.exe through the CLI and asserts the
overrides are inert: it seeds a temp store saying granted and a registry key
saying blocked, sets both override variables, and requires the binary to
ignore them. It also covers real store creation and atomic replace, corruption
and stale-prompt recovery, piped-EOF fail-closed, and the HKLM policy ceiling.
Because it mutates real machine state it refuses to run without
-AcceptRealMachineMutation, and refuses debug binaries outright. The temporary
workflow runs it on ephemeral GitHub-hosted runners only and asserts
RUNNER_ENVIRONMENT, since self-hosted runners also set GITHUB_ACTIONS.
Refs #691
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../workflows/Temp.Telemetry.Release.Test.yml | 61 +++
tests/scripts/README.md | 1 +
.../run_telemetry_consent_release_test.ps1 | 414 ++++++++++++++++++
3 files changed, 476 insertions(+)
create mode 100644 .github/workflows/Temp.Telemetry.Release.Test.yml
create mode 100644 tests/scripts/run_telemetry_consent_release_test.ps1
diff --git a/.github/workflows/Temp.Telemetry.Release.Test.yml b/.github/workflows/Temp.Telemetry.Release.Test.yml
new file mode 100644
index 000000000..32cb453fc
--- /dev/null
+++ b/.github/workflows/Temp.Telemetry.Release.Test.yml
@@ -0,0 +1,61 @@
+# TEMPORARY. Validates tests/scripts/run_telemetry_consent_release_test.ps1
+# against GitHub-hosted runners before the release-path test is wired into
+# Build.Windows.Job.yml. Delete once that lands. See issue #691.
+#
+# The test mutates the real per-user consent store and the real HKLM policy
+# key, so it must only ever run on an ephemeral GitHub-hosted runner. Do not
+# add self-hosted labels to this workflow.
+name: TEMP - Telemetry release-path test
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ release-consent:
+ name: ${{ matrix.arch }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - arch: x64
+ runner: windows-2025
+ target: x86_64-pc-windows-msvc
+ - arch: arm64
+ runner: windows-11-arm
+ target: aarch64-pc-windows-msvc
+ runs-on: ${{ matrix.runner }}
+ defaults:
+ run:
+ working-directory: src
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Surface toolchain file at repo root
+ shell: bash
+ working-directory: ${{ github.workspace }}
+ run: cp src/rust-toolchain.toml rust-toolchain.toml
+
+ - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
+ with:
+ target: ${{ matrix.target }}
+ override: false
+ rustflags: ''
+
+ - name: Point cargo at the MxcDependencies feed
+ uses: ./.github/actions/setup-cargo-feed
+
+ # Consent is backend-independent and wxc defaults to no backend
+ # features, so the default release build is enough.
+ - name: Build release executor
+ run: cargo build --locked --release --target ${{ matrix.target }} -p wxc
+
+ - name: Run release-path telemetry consent test
+ working-directory: ${{ github.workspace }}
+ shell: pwsh
+ run: |
+ tests\scripts\run_telemetry_consent_release_test.ps1 `
+ -BinDir src\target\${{ matrix.target }}\release `
+ -AcceptRealMachineMutation
diff --git a/tests/scripts/README.md b/tests/scripts/README.md
index f2244cd88..f801c9219 100644
--- a/tests/scripts/README.md
+++ b/tests/scripts/README.md
@@ -48,6 +48,7 @@ Linux / macOS (`.sh`):
| `T3-Workloads.ps1` | Real workloads (pwsh, git, node, python, cmd) on top of the T3 primitives. A missing interpreter is reported as a skip, not a failure | `wxc-exec.exe`; `pwsh` / `git` / `node` / `python` each optional, gating their own cases |
| `run_telemetry_consent_smoke_test.ps1` | Consent maintenance, presentation, policy, and exit-code smoke tests | Debug `wxc-exec.exe` built with `test-support` |
| `run_telemetry_etw_smoke_test.ps1` | Isolated consent flow plus public-provider ETW capture | Debug `wxc-exec.exe` built with `test-support`; ETW tooling; Administrator, otherwise the test skips |
+| `run_telemetry_consent_release_test.ps1` | Consent path in a **release** executor, where the debug store/policy overrides are compiled out. Mutates the real consent store and HKLM policy, so it requires `-AcceptRealMachineMutation` and an ephemeral machine | Release `wxc-exec.exe`; Administrator for the HKLM policy section, otherwise that section skips |
| `run_on_repeat.ps1` | Stress test (loops core tests) | `wxc-exec.exe` |
Each `run_processcontainer__test.ps1` also runs standalone against a
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
new file mode 100644
index 000000000..56d509f76
--- /dev/null
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -0,0 +1,414 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+.SYNOPSIS
+ Exercises the telemetry consent path in a RELEASE wxc-exec, where the
+ debug-only store and policy overrides are compiled out.
+
+.DESCRIPTION
+ The debug smoke test redirects the consent store with
+ MXC_TEST_LOCALAPPDATA_OVERRIDE and the policy key with
+ MXC_TEST_POLICY_KEY_OVERRIDE. Both are gated on
+ `cfg(any(test, all(feature = "test-support", debug_assertions)))`, so a
+ shipped binary resolves the store through SHGetKnownFolderPath and reads
+ policy from HKLM. That release-only shape has no other coverage.
+
+ Reaching it requires giving up isolation: this test writes the real
+ per-user consent store and the real HKLM policy key. Run it only on a
+ machine you are willing to mutate. Prior state is backed up and restored,
+ but a crash mid-run can leave the store or the policy key modified.
+
+.PARAMETER BinDir
+ Directory holding a release wxc-exec.exe.
+
+.PARAMETER AcceptRealMachineMutation
+ Required acknowledgement. There is deliberately no CI auto-detection:
+ self-hosted runners also set GITHUB_ACTIONS, and those machines persist.
+#>
+
+[CmdletBinding()]
+param(
+ [string]$BinDir,
+ [switch]$AcceptRealMachineMutation
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not $AcceptRealMachineMutation) {
+ throw 'This test mutates the real consent store and HKLM policy. Pass -AcceptRealMachineMutation on an ephemeral machine.'
+}
+
+$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
+if (-not $BinDir) { $BinDir = Join-Path $repoRoot 'src\target\release' }
+elseif (-not [IO.Path]::IsPathRooted($BinDir)) { $BinDir = Join-Path $repoRoot $BinDir }
+$BinDir = [IO.Path]::GetFullPath($BinDir)
+$exe = Join-Path $BinDir 'wxc-exec.exe'
+if (-not (Test-Path $exe)) {
+ throw "Release wxc-exec.exe not found at '$exe'. Run 'cargo build --release -p wxc'."
+}
+if ($exe -match '\\debug\\') {
+ throw 'Debug binaries are refused: this test exists to exercise the release cfg shape.'
+}
+
+# GetFolderPath mirrors the SHGetKnownFolderPath call the executor makes;
+# %LOCALAPPDATA% is a separate, spoofable source of truth.
+$localAppData = [Environment]::GetFolderPath('LocalApplicationData')
+if (-not $localAppData) { throw 'Could not resolve the LocalApplicationData known folder.' }
+$mxcDir = Join-Path $localAppData 'mxc'
+$consentFile = Join-Path $mxcDir 'telemetry-consent.json'
+$policyKey = 'HKLM:\SOFTWARE\Policies\Mxc'
+
+$expectedBody = @'
+Help improve MXC and other Microsoft product including Windows by sharing optional diagnostic data with Microsoft.
+
+If enabled, MXC sends diagnostic information about product usage, performance, and reliability. MXC does not send your commands, file paths, credentials, or other customer content.
+'@ -replace "`r`n", "`n"
+
+function New-ConsentProcess {
+ param([string]$Arguments, [hashtable]$Environment)
+
+ $start = New-Object Diagnostics.ProcessStartInfo
+ $start.FileName = $exe
+ $start.Arguments = $Arguments
+ $start.UseShellExecute = $false
+ $start.RedirectStandardInput = $true
+ $start.RedirectStandardOutput = $true
+ $start.RedirectStandardError = $true
+ $start.CreateNoWindow = $true
+ if ($Environment) {
+ foreach ($name in $Environment.Keys) { $start.Environment[$name] = [string]$Environment[$name] }
+ }
+ $process = New-Object Diagnostics.Process
+ $process.StartInfo = $start
+ if (-not $process.Start()) { throw "Failed to start '$exe $Arguments'." }
+ return $process
+}
+
+# Reads both pipes concurrently. Native stderr never reaches PowerShell's error
+# stream this way, so the script behaves identically under 5.1 and 7.
+function Invoke-Consent {
+ param([string]$Arguments, [hashtable]$Environment)
+
+ $process = New-ConsentProcess -Arguments $Arguments -Environment $Environment
+ $stdout = $process.StandardOutput.ReadToEndAsync()
+ $stderr = $process.StandardError.ReadToEndAsync()
+ $process.StandardInput.Close()
+ $process.WaitForExit()
+ return [pscustomobject]@{
+ ExitCode = $process.ExitCode
+ StdOut = $stdout.Result
+ StdErr = $stderr.Result
+ }
+}
+
+function Invoke-ConsentJson {
+ param([string]$Arguments, [hashtable]$Environment)
+
+ $result = Invoke-Consent -Arguments $Arguments -Environment $Environment
+ if ($result.ExitCode -ne 0) {
+ throw "'$Arguments' exited $($result.ExitCode): $($result.StdErr)"
+ }
+ return ($result.StdOut | ConvertFrom-Json)
+}
+
+function Invoke-ConsentRequest {
+ param([string]$Decision, [hashtable]$Environment)
+
+ $process = New-ConsentProcess `
+ -Arguments '--telemetry-consent request --telemetry-consent-locale en-US' `
+ -Environment $Environment
+ $stderr = $process.StandardError.ReadToEndAsync()
+ $firstLine = $process.StandardOutput.ReadLine()
+ if (-not $firstLine) {
+ $process.WaitForExit()
+ throw "Consent request emitted no response: $($stderr.Result)"
+ }
+ $first = $firstLine | ConvertFrom-Json
+ if ($first.result -ne 'presentationRequired') {
+ $process.StandardInput.Close()
+ $process.WaitForExit()
+ return $first
+ }
+ if (-not $first.prompt -or -not $first.challenge) {
+ throw "Unexpected consent presentation: $firstLine"
+ }
+ if ($first.prompt.resourceVersion -ne 3 -or
+ $first.prompt.locale -ne 'en-US' -or
+ $first.prompt.title.text -ne 'Help improve Microsoft Products' -or
+ $first.prompt.body.text -ne $expectedBody -or
+ $first.prompt.affirmativeLabel.text -ne 'Yes' -or
+ $first.prompt.negativeLabel.text -ne 'No' -or
+ $first.prompt.learnMoreLabel.text -ne 'Privacy Statement' -or
+ $first.prompt.learnMoreUrl -ne 'https://go.microsoft.com/fwlink/?linkid=521839') {
+ throw 'The canonical consent resource drifted in the release build.'
+ }
+ $response = [pscustomobject]@{
+ challenge = $first.challenge
+ resourceVersion = $first.prompt.resourceVersion
+ decision = $Decision
+ }
+ $process.StandardInput.WriteLine(($response | ConvertTo-Json -Compress))
+ $finalRead = $process.StandardOutput.ReadLineAsync()
+ if (-not $finalRead.Wait(5000)) {
+ $process.Kill()
+ throw 'Consent process waited for stdin EOF instead of accepting the decision line.'
+ }
+ $process.StandardInput.Close()
+ $process.WaitForExit()
+ if ($process.ExitCode -ne 0) { throw "Consent request failed: $($stderr.Result)" }
+ return ($finalRead.Result | ConvertFrom-Json)
+}
+
+function Assert-Status {
+ param(
+ [object]$Value,
+ [string]$Stored,
+ [string]$Effective,
+ [string]$Policy,
+ [bool]$NeedsPrompt,
+ [string]$Step
+ )
+ if ($Value.storedState -ne $Stored -or
+ $Value.effectiveState -ne $Effective -or
+ $Value.policy -ne $Policy -or
+ $Value.needsPrompt -ne $NeedsPrompt) {
+ throw "$Step returned an unexpected response: $($Value | ConvertTo-Json -Compress)"
+ }
+}
+
+function Write-ConsentRecord {
+ param([string]$Consent, [int]$PromptVersion = 3, [string]$Locale = 'en-US')
+
+ $record = [pscustomobject]@{
+ schemaVersion = 2
+ consent = $Consent
+ source = 'release-path-test'
+ promptedMxcVersion = '0.0.0-release-test'
+ promptResourceVersion = $PromptVersion
+ promptLocale = $Locale
+ updatedAtEpoch = 0
+ }
+ [IO.File]::WriteAllText(
+ $consentFile,
+ ($record | ConvertTo-Json -Compress),
+ (New-Object Text.UTF8Encoding $false))
+}
+
+function Remove-RealConsentFile {
+ if (Test-Path $consentFile) { Remove-Item $consentFile -Force }
+}
+
+# --- Test sections ---------------------------------------------------------
+
+function Test-CliContract {
+ $invalid = Invoke-Consent -Arguments '--telemetry-consent invalid'
+ if ($invalid.ExitCode -ne 64) {
+ throw "Invalid consent action returned $($invalid.ExitCode); expected 64."
+ }
+ $conflict = Invoke-Consent -Arguments '--telemetry-consent status --config missing.json'
+ if ($conflict.ExitCode -ne 64) {
+ throw "Consent/config conflict returned $($conflict.ExitCode); expected 64."
+ }
+ $afterSeparator = Invoke-Consent -Arguments '--unknown-flag -- --telemetry-consent=request'
+ if ($afterSeparator.ExitCode -ne 2) {
+ throw "Consent-like text after '--' returned $($afterSeparator.ExitCode); expected 2."
+ }
+ Write-Host ' ok: release CLI exit-code contract'
+}
+
+function Test-FreshStore {
+ Remove-RealConsentFile
+ $fresh = Invoke-ConsentJson -Arguments '--telemetry-consent status'
+ Assert-Status $fresh 'undetermined' 'undetermined' 'unrestricted' $true 'fresh status'
+ Write-Host ' ok: real store resolves and reports undetermined when absent'
+}
+
+function Test-PipedEofFailsClosed {
+ Remove-RealConsentFile
+ $piped = Invoke-Consent -Arguments '--telemetry-consent request --telemetry-consent-locale en-US'
+ if ($piped.ExitCode -ne 1) {
+ throw "Piped request exited $($piped.ExitCode); expected 1: $($piped.StdErr)"
+ }
+ $responses = @($piped.StdOut -split '\r?\n' | Where-Object { $_ } | ForEach-Object { $_ | ConvertFrom-Json })
+ if ($responses.Count -ne 2 -or
+ $responses[0].result -ne 'presentationRequired' -or
+ $responses[1].result -ne 'presentationUnavailable') {
+ throw "Piped request returned an unexpected response: $($piped.StdOut)"
+ }
+ if (Test-Path $consentFile) { throw 'Piped request EOF created a consent record.' }
+ Write-Host ' ok: presenter EOF fails closed without creating the real store'
+}
+
+function Test-RealStoreLifecycle {
+ $denied = Invoke-ConsentRequest -Decision 'no'
+ if ($denied.result -ne 'denied') { throw 'Explicit No was not persisted as Denied.' }
+ if (-not (Test-Path $consentFile)) {
+ throw "Explicit No did not create the store at the known-folder path '$consentFile'."
+ }
+ $record = Get-Content $consentFile -Raw | ConvertFrom-Json
+ if ($record.schemaVersion -ne 2 -or
+ $record.consent -ne 'denied' -or
+ $record.promptResourceVersion -ne 3 -or
+ $record.promptLocale -ne 'en-US') {
+ throw "The persisted record is malformed: $(Get-Content $consentFile -Raw)"
+ }
+ Assert-Status (Invoke-ConsentJson -Arguments '--telemetry-consent status') `
+ 'denied' 'denied' 'unrestricted' $false 'denied status'
+ Write-Host " ok: store written to the real known-folder path"
+}
+
+# The property the cfg gating exists for. Only provable against release.
+function Test-OverridesAreCompiledOut {
+ $probeDir = Join-Path ([IO.Path]::GetTempPath()) "mxc_release_probe_$([guid]::NewGuid().ToString('N'))"
+ $probeStore = Join-Path $probeDir 'mxc'
+ New-Item -ItemType Directory -Path $probeStore -Force | Out-Null
+ $probeSubkey = "Software\MxcReleaseProbe\$([guid]::NewGuid().ToString('N'))"
+ $probePath = "HKCU:\$probeSubkey"
+ try {
+ # A store override, if honored, would report granted instead of denied.
+ $record = [pscustomobject]@{
+ schemaVersion = 2
+ consent = 'granted'
+ source = 'release-path-test-probe'
+ promptedMxcVersion = '0.0.0-release-test'
+ promptResourceVersion = 3
+ promptLocale = 'en-US'
+ updatedAtEpoch = 0
+ }
+ [IO.File]::WriteAllText(
+ (Join-Path $probeStore 'telemetry-consent.json'),
+ ($record | ConvertTo-Json -Compress),
+ (New-Object Text.UTF8Encoding $false))
+
+ # A policy override, if honored, would report blocked instead of unrestricted.
+ New-Item -Path $probePath -Force | Out-Null
+ Set-ItemProperty -Path $probePath -Name AllowTelemetry -Value 0 -Type DWord
+
+ $status = Invoke-ConsentJson -Arguments '--telemetry-consent status' -Environment @{
+ MXC_TEST_LOCALAPPDATA_OVERRIDE = $probeDir
+ MXC_TEST_LOCALAPPDATA_OVERRIDE_OWNER_PID = "$PID"
+ MXC_TEST_POLICY_KEY_OVERRIDE = $probeSubkey
+ MXC_TEST_POLICY_KEY_OVERRIDE_OWNER_PID = "$PID"
+ }
+ if ($status.storedState -ne 'denied') {
+ throw "MXC_TEST_LOCALAPPDATA_OVERRIDE redirected a release binary (storedState '$($status.storedState)'). The debug override is not compiled out."
+ }
+ if ($status.policy -ne 'unrestricted') {
+ throw "MXC_TEST_POLICY_KEY_OVERRIDE redirected a release binary (policy '$($status.policy)'). The debug override is not compiled out."
+ }
+ Write-Host ' ok: debug store and policy overrides are compiled out'
+ }
+ finally {
+ Remove-Item -Recurse -Force $probeDir -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force $probePath -ErrorAction SilentlyContinue
+ }
+}
+
+function Test-GrantAndCorruptionRecovery {
+ $granted = Invoke-ConsentRequest -Decision 'yes'
+ if ($granted.result -ne 'granted') { throw 'Explicit Yes was not persisted as Granted.' }
+ Assert-Status (Invoke-ConsentJson -Arguments '--telemetry-consent status') `
+ 'granted' 'granted' 'unrestricted' $false 'granted status'
+
+ [IO.File]::WriteAllText($consentFile, 'not json at all', (New-Object Text.UTF8Encoding $false))
+ $corrupt = Invoke-ConsentJson -Arguments '--telemetry-consent status'
+ if ($corrupt.effectiveState -eq 'granted') {
+ throw 'A corrupted real store was treated as a grant.'
+ }
+
+ Write-ConsentRecord -Consent 'granted' -PromptVersion 999
+ $stale = Invoke-ConsentJson -Arguments '--telemetry-consent status'
+ if ($stale.storedState -ne 'granted' -or $stale.effectiveState -ne 'undetermined') {
+ throw "A grant for an unsupported prompt version authorized collection: $($stale | ConvertTo-Json -Compress)"
+ }
+ Write-Host ' ok: corrupt and stale-prompt records fail closed at the real path'
+}
+
+function Test-PolicyCeiling {
+ Write-ConsentRecord -Consent 'granted'
+ Assert-Status (Invoke-ConsentJson -Arguments '--telemetry-consent status') `
+ 'granted' 'granted' 'unrestricted' $false 'policy absent'
+
+ New-Item -Path $policyKey -Force | Out-Null
+ Set-ItemProperty -Path $policyKey -Name AllowTelemetry -Value 0 -Type DWord
+ $blocked = Invoke-ConsentRequest -Decision 'yes'
+ if ($blocked.result -ne 'policyBlocked') {
+ throw "Blocked HKLM policy did not suppress presentation: $($blocked | ConvertTo-Json -Compress)"
+ }
+ Assert-Status $blocked 'granted' 'granted' 'blocked' $false 'blocked status'
+
+ Set-ItemProperty -Path $policyKey -Name AllowTelemetry -Value 3 -Type DWord
+ Assert-Status (Invoke-ConsentJson -Arguments '--telemetry-consent status') `
+ 'granted' 'granted' 'allowed' $false 'allowed status'
+
+ Set-ItemProperty -Path $policyKey -Name AllowTelemetry -Value 0 -Type DWord
+ $withdrawn = Invoke-ConsentJson -Arguments '--telemetry-consent withdraw'
+ if ($withdrawn.result -ne 'withdrawn') { throw 'Withdrawal while blocked did not report withdrawn.' }
+ Assert-Status $withdrawn 'denied' 'denied' 'blocked' $false 'withdrawal while blocked'
+ $again = Invoke-ConsentJson -Arguments '--telemetry-consent withdraw'
+ if ($again.result -ne 'withdrawn') { throw 'Repeated withdrawal was not idempotent.' }
+ Write-Host ' ok: real HKLM policy ceiling gates presentation and never grants'
+}
+
+# --- Run -------------------------------------------------------------------
+
+$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+$principal = New-Object Security.Principal.WindowsPrincipal($identity)
+$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+
+$mxcDirPreexisted = Test-Path $mxcDir
+$consentBackup = if (Test-Path $consentFile) { [IO.File]::ReadAllBytes($consentFile) } else { $null }
+$policyKeyPreexisted = Test-Path $policyKey
+$policyValueBackup = $null
+if ($policyKeyPreexisted) {
+ $policyValueBackup = (Get-ItemProperty -Path $policyKey -Name AllowTelemetry -ErrorAction SilentlyContinue).AllowTelemetry
+}
+
+Write-Host "Release executor : $exe"
+Write-Host "Consent store : $consentFile"
+Write-Host "Policy key : $policyKey"
+
+try {
+ if (-not $mxcDirPreexisted) { New-Item -ItemType Directory -Path $mxcDir -Force | Out-Null }
+
+ Test-CliContract
+ Test-FreshStore
+ Test-PipedEofFailsClosed
+ Test-RealStoreLifecycle
+ Test-OverridesAreCompiledOut
+ Test-GrantAndCorruptionRecovery
+
+ if ($isAdmin) {
+ Test-PolicyCeiling
+ }
+ else {
+ Write-Host ' skipped: HKLM policy ceiling (requires Administrator)' -ForegroundColor Yellow
+ }
+
+ Write-Host 'PASSED: release-path telemetry consent test' -ForegroundColor Green
+}
+finally {
+ if ($null -ne $consentBackup) {
+ New-Item -ItemType Directory -Path $mxcDir -Force | Out-Null
+ [IO.File]::WriteAllBytes($consentFile, $consentBackup)
+ }
+ elseif ($mxcDirPreexisted) {
+ Remove-RealConsentFile
+ }
+ else {
+ Remove-Item -Recurse -Force $mxcDir -ErrorAction SilentlyContinue
+ }
+
+ if ($isAdmin) {
+ if (-not $policyKeyPreexisted) {
+ Remove-Item -Recurse -Force $policyKey -ErrorAction SilentlyContinue
+ }
+ elseif ($null -eq $policyValueBackup) {
+ Remove-ItemProperty -Path $policyKey -Name AllowTelemetry -ErrorAction SilentlyContinue
+ }
+ else {
+ Set-ItemProperty -Path $policyKey -Name AllowTelemetry -Value $policyValueBackup -Type DWord
+ }
+ }
+}
From f0f1afee49b85f6ddb782c0647534033e39ed13d Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 13:06:53 -0700
Subject: [PATCH 3/9] trigger workflow
---
.github/workflows/Temp.Telemetry.Release.Test.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/Temp.Telemetry.Release.Test.yml b/.github/workflows/Temp.Telemetry.Release.Test.yml
index 32cb453fc..423ee49e2 100644
--- a/.github/workflows/Temp.Telemetry.Release.Test.yml
+++ b/.github/workflows/Temp.Telemetry.Release.Test.yml
@@ -8,7 +8,9 @@
name: TEMP - Telemetry release-path test
on:
- workflow_dispatch:
+ push:
+ branches:
+ - user/emichlin/telemetry-smoke-arm64
permissions:
contents: read
From 41f50cd2d7ca570941bf4c2a81cfe0951f369e13 Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 13:37:28 -0700
Subject: [PATCH 4/9] add switch to require admin
---
.github/workflows/Temp.Telemetry.Release.Test.yml | 3 ++-
tests/scripts/README.md | 2 +-
tests/scripts/run_telemetry_consent_release_test.ps1 | 11 ++++++++++-
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/Temp.Telemetry.Release.Test.yml b/.github/workflows/Temp.Telemetry.Release.Test.yml
index 423ee49e2..426ed0bee 100644
--- a/.github/workflows/Temp.Telemetry.Release.Test.yml
+++ b/.github/workflows/Temp.Telemetry.Release.Test.yml
@@ -60,4 +60,5 @@ jobs:
run: |
tests\scripts\run_telemetry_consent_release_test.ps1 `
-BinDir src\target\${{ matrix.target }}\release `
- -AcceptRealMachineMutation
+ -AcceptRealMachineMutation `
+ -RequirePolicyCeiling
diff --git a/tests/scripts/README.md b/tests/scripts/README.md
index f801c9219..37297c779 100644
--- a/tests/scripts/README.md
+++ b/tests/scripts/README.md
@@ -48,7 +48,7 @@ Linux / macOS (`.sh`):
| `T3-Workloads.ps1` | Real workloads (pwsh, git, node, python, cmd) on top of the T3 primitives. A missing interpreter is reported as a skip, not a failure | `wxc-exec.exe`; `pwsh` / `git` / `node` / `python` each optional, gating their own cases |
| `run_telemetry_consent_smoke_test.ps1` | Consent maintenance, presentation, policy, and exit-code smoke tests | Debug `wxc-exec.exe` built with `test-support` |
| `run_telemetry_etw_smoke_test.ps1` | Isolated consent flow plus public-provider ETW capture | Debug `wxc-exec.exe` built with `test-support`; ETW tooling; Administrator, otherwise the test skips |
-| `run_telemetry_consent_release_test.ps1` | Consent path in a **release** executor, where the debug store/policy overrides are compiled out. Mutates the real consent store and HKLM policy, so it requires `-AcceptRealMachineMutation` and an ephemeral machine | Release `wxc-exec.exe`; Administrator for the HKLM policy section, otherwise that section skips |
+| `run_telemetry_consent_release_test.ps1` | Consent path in a **release** executor, where the debug store/policy overrides are compiled out. Mutates the real consent store and HKLM policy, so it requires `-AcceptRealMachineMutation` and an ephemeral machine | Release `wxc-exec.exe`; Administrator for the HKLM policy section, otherwise that section skips unless `-RequirePolicyCeiling` is passed |
| `run_on_repeat.ps1` | Stress test (loops core tests) | `wxc-exec.exe` |
Each `run_processcontainer__test.ps1` also runs standalone against a
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
index 56d509f76..130521395 100644
--- a/tests/scripts/run_telemetry_consent_release_test.ps1
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -25,12 +25,18 @@
.PARAMETER AcceptRealMachineMutation
Required acknowledgement. There is deliberately no CI auto-detection:
self-hosted runners also set GITHUB_ACTIONS, and those machines persist.
+
+.PARAMETER RequirePolicyCeiling
+ Fails instead of skipping when the session is not elevated. CI passes this
+ so a runner that stops being elevated surfaces as a failure rather than
+ silently dropping the only coverage of the real HKLM policy key.
#>
[CmdletBinding()]
param(
[string]$BinDir,
- [switch]$AcceptRealMachineMutation
+ [switch]$AcceptRealMachineMutation,
+ [switch]$RequirePolicyCeiling
)
$ErrorActionPreference = 'Stop'
@@ -356,6 +362,9 @@ function Test-PolicyCeiling {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+if ($RequirePolicyCeiling -and -not $isAdmin) {
+ throw 'HKLM policy ceiling coverage was required but this session is not elevated.'
+}
$mxcDirPreexisted = Test-Path $mxcDir
$consentBackup = if (Test-Path $consentFile) { [IO.File]::ReadAllBytes($consentFile) } else { $null }
From c5201e912eee64bfb77ed2e858e51f817f546e72 Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 13:54:41 -0700
Subject: [PATCH 5/9] Add script to build workflow
---
.github/workflows/Build.Windows.Job.yml | 11 ++++
.../workflows/Temp.Telemetry.Release.Test.yml | 64 -------------------
2 files changed, 11 insertions(+), 64 deletions(-)
delete mode 100644 .github/workflows/Temp.Telemetry.Release.Test.yml
diff --git a/.github/workflows/Build.Windows.Job.yml b/.github/workflows/Build.Windows.Job.yml
index 9572c1091..d4d89372b 100644
--- a/.github/workflows/Build.Windows.Job.yml
+++ b/.github/workflows/Build.Windows.Job.yml
@@ -89,6 +89,17 @@ jobs:
shell: pwsh
run: tests\scripts\run_telemetry_etw_smoke_test.ps1 -BinDir src\target\debug
+ # Mutates the real consent store and HKLM policy, so it is confined to
+ # the ephemeral GitHub-hosted runners this job declares.
+ - name: Run release-path telemetry consent test
+ working-directory: ${{ github.workspace }}
+ shell: pwsh
+ run: |
+ tests\scripts\run_telemetry_consent_release_test.ps1 `
+ -BinDir src\target\${{ matrix.target }}\release `
+ -AcceptRealMachineMutation `
+ -RequirePolicyCeiling
+
- name: Upload binaries
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
diff --git a/.github/workflows/Temp.Telemetry.Release.Test.yml b/.github/workflows/Temp.Telemetry.Release.Test.yml
deleted file mode 100644
index 426ed0bee..000000000
--- a/.github/workflows/Temp.Telemetry.Release.Test.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-# TEMPORARY. Validates tests/scripts/run_telemetry_consent_release_test.ps1
-# against GitHub-hosted runners before the release-path test is wired into
-# Build.Windows.Job.yml. Delete once that lands. See issue #691.
-#
-# The test mutates the real per-user consent store and the real HKLM policy
-# key, so it must only ever run on an ephemeral GitHub-hosted runner. Do not
-# add self-hosted labels to this workflow.
-name: TEMP - Telemetry release-path test
-
-on:
- push:
- branches:
- - user/emichlin/telemetry-smoke-arm64
-
-permissions:
- contents: read
-
-jobs:
- release-consent:
- name: ${{ matrix.arch }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - arch: x64
- runner: windows-2025
- target: x86_64-pc-windows-msvc
- - arch: arm64
- runner: windows-11-arm
- target: aarch64-pc-windows-msvc
- runs-on: ${{ matrix.runner }}
- defaults:
- run:
- working-directory: src
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
-
- - name: Surface toolchain file at repo root
- shell: bash
- working-directory: ${{ github.workspace }}
- run: cp src/rust-toolchain.toml rust-toolchain.toml
-
- - uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2.0.0
- with:
- target: ${{ matrix.target }}
- override: false
- rustflags: ''
-
- - name: Point cargo at the MxcDependencies feed
- uses: ./.github/actions/setup-cargo-feed
-
- # Consent is backend-independent and wxc defaults to no backend
- # features, so the default release build is enough.
- - name: Build release executor
- run: cargo build --locked --release --target ${{ matrix.target }} -p wxc
-
- - name: Run release-path telemetry consent test
- working-directory: ${{ github.workspace }}
- shell: pwsh
- run: |
- tests\scripts\run_telemetry_consent_release_test.ps1 `
- -BinDir src\target\${{ matrix.target }}\release `
- -AcceptRealMachineMutation `
- -RequirePolicyCeiling
From 7a091a25c094568717dac59c6ef20b1ece57ba83 Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:08:44 -0700
Subject: [PATCH 6/9] lock files not handled properly
---
tests/scripts/run_telemetry_consent_release_test.ps1 | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
index 130521395..b0a8a9375 100644
--- a/tests/scripts/run_telemetry_consent_release_test.ps1
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -63,6 +63,9 @@ $localAppData = [Environment]::GetFolderPath('LocalApplicationData')
if (-not $localAppData) { throw 'Could not resolve the LocalApplicationData known folder.' }
$mxcDir = Join-Path $localAppData 'mxc'
$consentFile = Join-Path $mxcDir 'telemetry-consent.json'
+# The executor creates this alongside the store and leaves it behind once the
+# lock is released, so cleanup has to account for it too.
+$lockFile = Join-Path $mxcDir 'telemetry-consent.lock'
$policyKey = 'HKLM:\SOFTWARE\Policies\Mxc'
$expectedBody = @'
@@ -368,6 +371,7 @@ if ($RequirePolicyCeiling -and -not $isAdmin) {
$mxcDirPreexisted = Test-Path $mxcDir
$consentBackup = if (Test-Path $consentFile) { [IO.File]::ReadAllBytes($consentFile) } else { $null }
+$lockFilePreexisted = Test-Path $lockFile
$policyKeyPreexisted = Test-Path $policyKey
$policyValueBackup = $null
if ($policyKeyPreexisted) {
@@ -409,6 +413,10 @@ finally {
Remove-Item -Recurse -Force $mxcDir -ErrorAction SilentlyContinue
}
+ if ($mxcDirPreexisted -and -not $lockFilePreexisted) {
+ Remove-Item -Force $lockFile -ErrorAction SilentlyContinue
+ }
+
if ($isAdmin) {
if (-not $policyKeyPreexisted) {
Remove-Item -Recurse -Force $policyKey -ErrorAction SilentlyContinue
From ba244a8711f83b5bf2291b1903da27e49c84adf6 Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:19:24 -0700
Subject: [PATCH 7/9] backup withdrawl file
---
.../run_telemetry_consent_release_test.ps1 | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
index b0a8a9375..70663478d 100644
--- a/tests/scripts/run_telemetry_consent_release_test.ps1
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -66,6 +66,9 @@ $consentFile = Join-Path $mxcDir 'telemetry-consent.json'
# The executor creates this alongside the store and leaves it behind once the
# lock is released, so cleanup has to account for it too.
$lockFile = Join-Path $mxcDir 'telemetry-consent.lock'
+# A pending marker means an interrupted withdrawal, which reads fail-closed.
+# Every consent write clears it, so it has to be backed up and restored.
+$withdrawalFile = Join-Path $mxcDir 'telemetry-consent.withdrawal-pending'
$policyKey = 'HKLM:\SOFTWARE\Policies\Mxc'
$expectedBody = @'
@@ -372,6 +375,12 @@ if ($RequirePolicyCeiling -and -not $isAdmin) {
$mxcDirPreexisted = Test-Path $mxcDir
$consentBackup = if (Test-Path $consentFile) { [IO.File]::ReadAllBytes($consentFile) } else { $null }
$lockFilePreexisted = Test-Path $lockFile
+$withdrawalBackup = $null
+$withdrawalBackupWritten = $null
+if (Test-Path $withdrawalFile) {
+ $withdrawalBackup = [IO.File]::ReadAllBytes($withdrawalFile)
+ $withdrawalBackupWritten = [IO.File]::GetLastWriteTimeUtc($withdrawalFile)
+}
$policyKeyPreexisted = Test-Path $policyKey
$policyValueBackup = $null
if ($policyKeyPreexisted) {
@@ -417,6 +426,16 @@ finally {
Remove-Item -Force $lockFile -ErrorAction SilentlyContinue
}
+ if ($null -ne $withdrawalBackup) {
+ New-Item -ItemType Directory -Path $mxcDir -Force | Out-Null
+ [IO.File]::WriteAllBytes($withdrawalFile, $withdrawalBackup)
+ # The marker goes stale on mtime, so let it keep aging from where it was.
+ [IO.File]::SetLastWriteTimeUtc($withdrawalFile, $withdrawalBackupWritten)
+ }
+ elseif ($mxcDirPreexisted) {
+ Remove-Item -Force $withdrawalFile -ErrorAction SilentlyContinue
+ }
+
if ($isAdmin) {
if (-not $policyKeyPreexisted) {
Remove-Item -Recurse -Force $policyKey -ErrorAction SilentlyContinue
From bf47034809e58698698cd28bb88e5e9bc10514db Mon Sep 17 00:00:00 2001
From: Elliot Michlin <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:33:26 -0700
Subject: [PATCH 8/9] policy value kind is now restored
---
.../run_telemetry_consent_release_test.ps1 | 25 ++++++++++++++++---
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
index 70663478d..7c5aad21f 100644
--- a/tests/scripts/run_telemetry_consent_release_test.ps1
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -69,7 +69,8 @@ $lockFile = Join-Path $mxcDir 'telemetry-consent.lock'
# A pending marker means an interrupted withdrawal, which reads fail-closed.
# Every consent write clears it, so it has to be backed up and restored.
$withdrawalFile = Join-Path $mxcDir 'telemetry-consent.withdrawal-pending'
-$policyKey = 'HKLM:\SOFTWARE\Policies\Mxc'
+$policySubKey = 'SOFTWARE\Policies\Mxc'
+$policyKey = "HKLM:\$policySubKey"
$expectedBody = @'
Help improve MXC and other Microsoft product including Windows by sharing optional diagnostic data with Microsoft.
@@ -383,8 +384,22 @@ if (Test-Path $withdrawalFile) {
}
$policyKeyPreexisted = Test-Path $policyKey
$policyValueBackup = $null
+# The value kind is part of the state: policy.rs fail-closes on a non-DWORD
+# AllowTelemetry, so restoring a REG_SZ as a DWORD would change the machine.
+$policyValueKind = $null
if ($policyKeyPreexisted) {
- $policyValueBackup = (Get-ItemProperty -Path $policyKey -Name AllowTelemetry -ErrorAction SilentlyContinue).AllowTelemetry
+ $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($policySubKey)
+ if ($key) {
+ try {
+ $policyValueBackup = $key.GetValue(
+ 'AllowTelemetry', $null,
+ [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+ if ($null -ne $policyValueBackup) {
+ $policyValueKind = $key.GetValueKind('AllowTelemetry')
+ }
+ }
+ finally { $key.Dispose() }
+ }
}
Write-Host "Release executor : $exe"
@@ -440,11 +455,13 @@ finally {
if (-not $policyKeyPreexisted) {
Remove-Item -Recurse -Force $policyKey -ErrorAction SilentlyContinue
}
- elseif ($null -eq $policyValueBackup) {
+ elseif ($null -eq $policyValueKind) {
Remove-ItemProperty -Path $policyKey -Name AllowTelemetry -ErrorAction SilentlyContinue
}
else {
- Set-ItemProperty -Path $policyKey -Name AllowTelemetry -Value $policyValueBackup -Type DWord
+ $key = [Microsoft.Win32.Registry]::LocalMachine.CreateSubKey($policySubKey)
+ try { $key.SetValue('AllowTelemetry', $policyValueBackup, $policyValueKind) }
+ finally { $key.Dispose() }
}
}
}
From 0941b57fef4d0dcf9de8f036e99aa2e2c6ac06d6 Mon Sep 17 00:00:00 2001
From: Elliot <31219104+theelliotm@users.noreply.github.com>
Date: Wed, 23 Sep 2026 14:44:24 -0700
Subject: [PATCH 9/9] Fix process exit handling in telemetry consent test
Ensure the process waits for exit after killing it.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
tests/scripts/run_telemetry_consent_release_test.ps1 | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/scripts/run_telemetry_consent_release_test.ps1 b/tests/scripts/run_telemetry_consent_release_test.ps1
index 7c5aad21f..f032f8ebf 100644
--- a/tests/scripts/run_telemetry_consent_release_test.ps1
+++ b/tests/scripts/run_telemetry_consent_release_test.ps1
@@ -165,6 +165,7 @@ function Invoke-ConsentRequest {
$finalRead = $process.StandardOutput.ReadLineAsync()
if (-not $finalRead.Wait(5000)) {
$process.Kill()
+ $process.WaitForExit()
throw 'Consent process waited for stdin EOF instead of accepting the decision line.'
}
$process.StandardInput.Close()