---
status: signed_off
phase: 07-driver-side-detection-thread
source: [07-06-PLAN.md, 07-VALIDATION.md, 07-CONTEXT.md]
started: 2026-05-04T05:39Z
updated: 2026-05-04T03:30Z
result: APPROVED
phase_outcome: GO
---

# Phase 7 — UAT (User Acceptance Test)

**Tested:** 2026-05-04
**Driver SHA:** 32aade3 (Wave 5 Task 1 head) + amendment (loadTrainingData fix, see Notes below)
**Rig:** Bigscreen Beyond + Win11 Pro
**Operator:** brandon@bigscreenvr.com

> **Pre-test prerequisites (D-26 — applies to every case below):**
> 1. Build artifacts current: `cmake --build build --config Release`.
> 2. Driver installed to `C:\Program Files (x86)\Steam\steamapps\common\SteamVR\drivers\micmap\` (run installer or manual copy of `build/driver/micmap/bin/win64/driver_micmap.dll` + `driver/resources/settings/default.vrsettings`).
> 3. vrserver.txt tail open: `Get-Content -Path "$env:LOCALAPPDATA\openvr\logs\vrserver.txt" -Wait -Tail 200` (PowerShell). Alternative location: `%PROGRAMFILES(X86)%\Steam\logs\vrserver.txt`.
> 4. Process Explorer (Sysinternals) open with `vrserver.exe` located.
> 5. UAT evidence directory created: `mkdir -p .planning/phases/07-driver-side-detection-thread/uat-evidence/`.
> 6. Driver SHA captured: `git rev-parse HEAD` — record in banner above.

## D-25 UAT Regimen (Six Cases)

| # | Case | Success Criterion | Status | Evidence |
|---|------|-------------------|--------|----------|
| 1 | Flag-ON in-process trigger | SC1 | ✅ | uat-evidence/d25-1-vrserver.txt — 4 TapCommand pushed (n=1..4), 4 UpdateBoolean down/up pairs, 0 POST /button (client suppressed via /health driver_detection_active=true) |
| 2 | HMD wake/sleep ×2 | SC3 / MIG-03 | ⚠ PASS-with-caveat | uat-evidence/d25-2-vrserver-pause-resume.txt — detection survives doff/don (TapCommand n=5,6 fired post-wake). Pause/Resume callbacks NOT exercised by Bigscreen Beyond proximity sensor (SteamVR EnterStandby fires on full-system standby, not on quick HMD doff). Pause/Resume code path verified by automated DetectionSettingsPropagation ctest. |
| 3 | 50-cycle Init→500ms→Cleanup stress | SC4 / MIG-04 | ✅ | uat-evidence/d25-3-ctest-output.txt — DeviceProviderLifecycleStress headless: PASS lifecycle_stress_50_cycles base=108 after=108 (delta=0, ≤5 budget) |
| 4 | Settings propagation < 50 ms | SC5 / MIG-06 | ✅ | uat-evidence/d25-4-ctest-output.txt — DetectionSettingsPropagation: PASS case_propagation_under_50ms elapsed_ms=0 |
| 5 | Flag-OFF regression | byte-identical to P6 closeout | ✅ | uat-evidence/d25-5-vrserver-flag-off.txt — 0 MicMap detection lines, 0 MicMap audio lines, 3 UpdateBoolean down/up pairs from hmd_button_test.exe POST /button → CommandQueue → /input/system/click |
| 6 | Coexistence handshake | Pitfall 10 / D-09..D-12 | ✅ covered-by-composition | D-25(1) proves single-tap suppression (0 POST /button while driver detection active). D-25(5) proves POST /button fallback path. curl /health verified driver_detection_active=true (flags ON) and =false (flags OFF). Mid-session mic-cover with flags OFF not directly tested but is a composition of (1)+(5) — no new code path. |

***
## D-25(1): Flag-ON in-process trigger / SC1

### Setup
1. `driver/resources/settings/default.vrsettings`: set BOTH `enable_driver_audio=true` AND `enable_driver_detection=true`. (After UAT, restore both to false per D-27.)
2. Reinstall driver: run `installer/MicMap-Setup-vX.Y.Z.exe` OR copy build artifacts to `C:\Program Files (x86)\Steam\steamapps\common\SteamVR\drivers\micmap\` per local dev setup. Ensure both the DLL AND the updated `default.vrsettings` are copied.
3. Boot SteamVR. Wear Bigscreen Beyond. Confirm HMD Ready state.
4. Confirm vrserver.txt opens: `%LOCALAPPDATA%\openvr\logs\vrserver.txt` (or `%PROGRAMFILES(X86)%\Steam\logs\vrserver.txt` depending on platform — check both).

### Test
1. Cover the configured microphone with a finger or palm for ~500 ms.
2. Observe: SteamVR dashboard toggles open.
3. Uncover; cooldown elapses; cover again. Dashboard toggles closed.
4. Repeat 3 times — expect 3 successful toggles.

### Pass criteria
- vrserver.txt contains the line `MicMap detection: TapCommand pushed (n=N)` for each successful trigger (3 lines minimum).
- vrserver.txt does NOT contain any `POST /button` traffic from `apps/micmap/main.cpp` for the same time window — verify via httpServer access log inspection (driver httpServer logs every POST /button at info level — search for `POST /button` in the test window). This proves the trigger went through the in-process path, NOT the v1.5 HTTP path.
- The cover-mic to dashboard-toggle latency is ≤ 1 second (subjective, but observable; should feel snappier than v1.5 because the HTTP hop is gone).

### Evidence
- Capture vrserver.txt excerpt covering the 3-trigger window. Save as `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-1-vrserver.txt`.
- Capture screenshot of SteamVR overlay showing dashboard toggle. Save as `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-1-toggle.png`.
- Confirm zero `POST /button` lines in the same window: `grep -c "POST /button" .planning/phases/07-driver-side-detection-thread/uat-evidence/d25-1-vrserver.txt` should be 0.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## D-25(2): HMD wake/sleep ×2 / SC3

### Setup
- Same as D-25(1) — both `enable_driver_audio=true` AND `enable_driver_detection=true`.
- Continue from the same SteamVR session as D-25(1) if convenient (reduces re-install churn).

### Test
1. Open Process Explorer (Sysinternals); locate `vrserver.exe`; note the Handles count and screenshot it.
2. With detection active (cover-mic toggle works per D-25(1)), simulate HMD sleep: remove the headset and place it on a flat surface so its proximity sensor fires sleep transition. Wait ≥ 5 seconds.
3. Re-don the headset (proximity wakes it). Wait ≥ 5 seconds.
4. Repeat sleep/wake cycle a second time.
5. Cover the mic — confirm it still triggers the dashboard.

### Pass criteria
- vrserver.txt contains paired log lines `MicMap detection: paused` and `MicMap detection: resumed` for each cycle (2 cycles × 2 lines = 4 minimum).
- Process Explorer handle count for `vrserver.exe` is stable (delta ≤ 10 across the test window — small drift acceptable for thread-pool churn).
- Cover-mic still triggers after each Resume (subjective verification; latency unchanged).

### Evidence
- Process Explorer screenshot showing handle count BEFORE and AFTER the 2-cycle test. Save as `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-2-handles-before.png` and `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-2-handles-after.png`.
- vrserver.txt log excerpt with the 4 paused/resumed lines. Save as `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-2-vrserver-pause-resume.txt`.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## D-25(3): 50-cycle Init→500ms→Cleanup stress / SC4 / MIG-04

### Setup
- Build artifacts up to date: `cmake --build build --config Release`.

### Headless verification (PRIMARY — automated)
1. Run `ctest --test-dir build -R DeviceProviderLifecycleStress -V > .planning/phases/07-driver-side-detection-thread/uat-evidence/d25-3-ctest-output.txt 2>&1`.
2. Pass criterion: exits 0 with `PASS lifecycle_stress_50_cycles base=N after=M` where `M - N <= 5` (handle delta within thread-pool churn budget).

### Real-rig verification (COMPLEMENT — Process Explorer audit)
1. Open Process Explorer; locate `vrserver.exe`; note Handles count; screenshot.
2. With both flags ON, restart SteamVR 5 times (use the SteamVR menu Restart, not full quit; or close and reopen the SteamVR client).
3. After 5 cycles, note Handles count again; screenshot.
4. Pass criterion: handle delta ≤ 20 across 5 cycles. (50 manual cycles is impractical; the headless test does the 50-cycle bar; 5 manual cycles validate that real-WASAPI handles also clean up cleanly.)

### Pass criteria (combined)
- Headless ctest exits 0 with handle delta ≤ 5 across 50 cycles.
- Real-rig 5-cycle restart shows handle delta ≤ 20 (or 4 per cycle).
- vrserver.txt contains no `AUDCLNT_E_DEVICE_IN_USE` errors during the cycles (`grep -c "AUDCLNT_E_DEVICE_IN_USE\|0x88890004" vrserver.txt` = 0).

### Evidence
- ctest output: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-3-ctest-output.txt`.
- Process Explorer screenshots before/after 5-cycle: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-3-handles-before.png`, `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-3-handles-after.png`.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## D-25(4): Settings propagation < 50 ms / SC5 / MIG-06

### Setup
- Build artifacts up to date.

### Test (headless)
1. Run `ctest --test-dir build -R DetectionSettingsPropagation -V > .planning/phases/07-driver-side-detection-thread/uat-evidence/d25-4-ctest-output.txt 2>&1`.
2. Pass criterion: exits 0 with `PASS case_propagation_under_50ms elapsed_ms=N` where `N < 50`.

### Pass criteria
- ctest exits 0 with elapsed_ms < 50.

### Evidence
- ctest output: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-4-ctest-output.txt`.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## D-25(5): Flag-OFF regression — byte-identical to P6 closeout

### Setup
1. `driver/resources/settings/default.vrsettings`: BOTH `enable_driver_audio=false` AND `enable_driver_detection=false` (the shipped-main default).
2. Reinstall driver (copy updated default.vrsettings to install location).
3. Boot SteamVR. Confirm HMD Ready.

### Test
1. Run `hmd_button_test.exe` (the existing v1.5 trigger harness — `build/apps/hmd_button_test/Release/hmd_button_test.exe`).
2. The harness sends a `POST /button` to the driver via `IDriverClient::tap()`.
3. Observe: SteamVR dashboard toggles via the v1.5 path.

### Pass criteria
- `hmd_button_test.exe` exits 0.
- SteamVR dashboard toggles in response to the harness call.
- vrserver.txt contains `POST /button` log entry from the driver's HTTP access log (or the equivalent `UpdateBooleanComponent(down) OK (handle=7)` / `UpdateBooleanComponent(up) OK (handle=7)` pair confirming the v1.5 trigger path executed end-to-end).
- vrserver.txt does NOT contain any `MicMap detection:` log lines (no detection thread active when flag is OFF — `grep -c "MicMap detection:" vrserver.txt` = 0).
- vrserver.txt does NOT contain any `MicMap audio:` log lines (no audio worker active when flag is OFF; matches P6 closeout SC4 byte-identical-to-P5 invariant — `grep -c "MicMap audio:" vrserver.txt` = 0).

### Evidence
- vrserver.txt excerpt: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-5-vrserver-flag-off.txt`.
- hmd_button_test.exe stdout: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-5-hmd-button-test-output.txt`.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## D-25(6): Coexistence handshake / Pitfall 10

### Setup
1. `driver/resources/settings/default.vrsettings`: BOTH `enable_driver_audio=true` AND `enable_driver_detection=true`.
2. Reinstall driver. Boot SteamVR.
3. Launch `micmap.exe` client (the v1.5 GUI client — still operational in P7 per D-12).
4. Confirm both processes are running (Task Manager / Process Explorer).

### Test (single-tap verification)
1. From a terminal: `curl -s http://127.0.0.1:27015/health | jq . > .planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-health-flag-on.json`.
   - Pass: response contains `"driver_detection_active": true`.
2. Cover the configured mic for ~500 ms.
3. Observe: SteamVR dashboard toggles ONCE (not twice).
4. Uncover, cooldown, repeat 3 times.

### Test (mid-session flag-flip resume)
1. With test running, edit `driver/resources/settings/default.vrsettings` and set `enable_driver_detection=false`. Save. Copy to install location.
2. Restart SteamVR (keep `micmap.exe` client running).
3. After SteamVR re-boot: `curl -s http://127.0.0.1:27015/health | jq . > .planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-health-flag-off.json` should now show `"driver_detection_active": false`.
4. Wait ≥ 1 second (the DriverClient cache TTL).
5. Cover the mic.
6. Observe: SteamVR dashboard toggles via the CLIENT trigger path (v1.5 POST /button — verify by `grep "POST /button" vrserver.txt` shows fresh entries, OR by `UpdateBooleanComponent(down)/up` pairs in the post-flip window).

### Pass criteria
- Single dashboard toggle per mic-cover when both flags ON (no double-tap).
- `curl` confirms `driver_detection_active=true` when both flags ON.
- After mid-session flag-OFF + driver restart, `curl` confirms `driver_detection_active=false`.
- Before the mid-session flip, micmap.exe client logs contain `onTrigger: driver_detection_active=true, suppressing` for each mic-cover.
- After the mid-session flip + ≥1s cache TTL, those suppression lines stop appearing AND vrserver.txt shows `POST /button` traffic (or equivalent UpdateBooleanComponent down/up pairs) resuming — the client trigger path is alive again.

### Evidence
- curl output (both states): `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-health-flag-on.json`, `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-health-flag-off.json`.
- vrserver.txt excerpts: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-vrserver-coexistence.txt` (covers both phases — both-flags-ON window AND post-flip client-resume window).
- micmap.exe log excerpt with the suppression line transitions: `.planning/phases/07-driver-side-detection-thread/uat-evidence/d25-6-micmap-log.txt`.

### Sign-off
- Status: ⬜ pending → ✅ pass / ❌ fail (operator initials + date)

***
## Sign-Off Table (final)

| Case | Result | Operator | Date |
|------|--------|----------|------|
| D-25(1) flag-ON in-process trigger | ✅ PASS | brandon@bigscreenvr.com | 2026-05-04 |
| D-25(2) HMD wake/sleep ×2 | ⚠ PASS-with-caveat (functional goal met; pause/resume not exercised by proximity doff — see Notes) | brandon@bigscreenvr.com | 2026-05-04 |
| D-25(3) 50-cycle stress | ✅ PASS (headless ctest, delta=0) | brandon@bigscreenvr.com | 2026-05-04 |
| D-25(4) settings propagation < 50 ms | ✅ PASS (headless ctest, elapsed_ms=0) | brandon@bigscreenvr.com | 2026-05-04 |
| D-25(5) flag-OFF regression | ✅ PASS | brandon@bigscreenvr.com | 2026-05-04 |
| D-25(6) coexistence handshake | ✅ PASS (covered-by-composition of D-25(1) + D-25(5)) | brandon@bigscreenvr.com | 2026-05-04 |

**Phase 7 GO/NO-GO:** ✅ **GO** — all 6 cases signed off (1 with caveat).

## Notes — UAT Findings

### Defect found and fixed during D-25(1)

First D-25(1) attempt failed: 0 `MicMap detection: TapCommand pushed` lines despite mic-covers. Root cause: `DetectionRunner::Start` created the FFT detector but never called `loadTrainingData()`. Without a trained noise profile, `analyze()` returns near-zero confidence; state machine never fires. The existing `%APPDATA%\MicMap\training_data.bin` (4156 bytes, written by v1.5 client) sat unused.

**Fix (committed during UAT):** Added `loadTrainingData(%APPDATA%\MicMap\training_data.bin)` to `DetectionRunner::Start`, after `createFFTDetector` and before `createStateMachine`. Fail-soft: missing/corrupt profile leaves detector untrained but driver stays alive (P5 HTTP fallback path remains usable). P9 will make the driver the sole writer of `training_data.bin`; P7 is read-only consumer.

After fix: D-25(1) re-run yielded 4 TapCommand pushed in ~6s of mic-covers, latency 7-11ms TapCommand→UpdateBoolean down. SC1 satisfied.

### D-25(2) caveat

Bigscreen Beyond proximity sensor (HMD doff/don) does NOT trigger SteamVR's `EnterStandby` callback on the MicMap driver — `EnterStandby` fires on full-system standby (~minutes idle), not on quick proximity transitions. The `Pause()`/`Resume()` code path is wired (07-04) and is exercised by the headless `DetectionSettingsPropagation` ctest. Functional goal of MIG-03 (detection survives wake) was verified by mic-cover succeeding after 2 doff/don cycles (TapCommand n=5, n=6 at 01:04 — see d25-2 evidence).

***
## Closeout — Restore main-branch flag defaults (D-27)

After all six cases sign off GREEN:

1. Revert `driver/resources/settings/default.vrsettings` so both flags are `false`:
   ```json
   "enable_driver_audio": false,
   "enable_driver_detection": false,
   ```
2. Commit message: `docs(07): UAT GO; restore main-branch flag defaults to OFF (D-27)`.
3. P10's cutover plan flips both defaults to `true` in one commit.

This mirrors the P6 D-19/D-20 closeout pattern: main-branch ships with detection-default-OFF discipline; the cutover phase (P10) is the single point where the v1.6 defaults flip to ON.

***
## Operator-side CLI invocations (for reference during D-25 runs)

**Driver build (OpenVR-present configuration; required for `driver_micmap` target):**
```bash
cmake --build build --config Release --target driver_micmap
```

**Driver DLL output path:**
```
build/driver/micmap/bin/win64/driver_micmap.dll
```

**vrserver.txt log path** (typical default; confirm with operator if SteamVR install is non-standard):
```
%LOCALAPPDATA%\openvr\logs\vrserver.txt
```
or, when running from the local SteamVR developer install:
```
%PROGRAMFILES(X86)%\Steam\logs\vrserver.txt
```

**hmd_button_test.exe location** (built into the OpenVR-present build under `apps/hmd_button_test/`):
```
build/apps/hmd_button_test/Release/hmd_button_test.exe
```

**Quick-grep helpers for vrserver.txt evidence:**
```bash
grep -c "MicMap detection: TapCommand pushed" "<vrserver.txt path>"   # Expect ≥3 for D-25(1) PASS
grep -c "POST /button" "<vrserver.txt path>"                          # MUST be 0 for D-25(1) PASS, ≥1 for D-25(5)/D-25(6) post-flip
grep -E "MicMap detection: (paused|resumed)" "<vrserver.txt path>"    # Expect 4 lines for D-25(2) PASS
grep -c "MicMap detection:" "<vrserver.txt path>"                     # MUST be 0 for D-25(5) PASS
grep -c "MicMap audio:" "<vrserver.txt path>"                         # MUST be 0 for D-25(5) PASS
grep -c "AUDCLNT_E_DEVICE_IN_USE\|0x88890004" "<vrserver.txt path>"   # MUST be 0 for D-25(3) PASS
```

**curl helpers for D-25(6):**
```bash
curl -s http://127.0.0.1:27015/health | jq .driver_detection_active   # Expect: true (both flags ON) / false (after mid-session flip)
```

**ctest helpers for D-25(3) + D-25(4):**
```bash
ctest --test-dir build -R DeviceProviderLifecycleStress -V
ctest --test-dir build -R DetectionSettingsPropagation -V
```
