TASK-1.6: Implement FlaUI-Driven Measurement Capture
- Status: Proposed
- Date: 2026-04-27
- Spec: SLICE-1.6: FlaUI-Driven Measurement Capture
- Depends on: TASK-1.1: Implement Multi-Tag Telemetry, TASK-1.2: Implement Real Frame Payloads
Objective
Build a UI-Automation-driven capture rig that drives the real WPF main window through actual button clicks, runs dotnet-counters against the running process for metrics, produces 18-metric measurement rows in the established format, and closes the deferred slice-1-2-real-frame-payloads row. This replaces the retired SLICE-1.5 / SLICE-1.5.1 automation rig.
Scope
- new test project
tests/InspectionPrototype.AcceptanceTests(Windows-only,net10.0-windows) withFlaUI.Core+FlaUI.UIA3package references viaDirectory.Packages.props IUiDriver/FlaUiDriver/FakeUiDriverabstraction split — drivers live in a new shared library so unit tests inInspectionPrototype.Testscan use the fake without pulling FlaUI into the unit-test projectAutomationProperties.AutomationIdaudit on every operator-touched control inMainWindow.xaml; a build-time regression test asserts the canonical id set is present- a JSON-binding integration test for
SimulatorProfilesOptions(folding in the SLICE-1.2 review finding — verifyHighFrameRateactually loads fromappsettings.json) - two scenarios as xUnit
[Fact]s with[Trait("Category", "Capture")]:DemoBaselineFlaUi,MultiTagSoakFlaUi - new (smaller)
tools/Capture-Measurements.ps1wrapping build → app launch → dotnet-counters → dotnet test (filtered) → row extraction - runbook §3a refresh: FlaUI prerequisites + new orchestrator invocation; preserves the sleep-disable powercfg recipe; preserves the SLICE-1.5 retirement pointer
- capture the deferred
slice-1-2-real-frame-payloadsrow (10-min HighFrameRate scenario via the new rig) - update SLICE-1.5 / SLICE-1.5.1 spec/task banners from "planned SLICE-1.6" to "SLICE-1.6 (this slice)"
- update CLAUDE.md current-position block + roadmap-progress.md table + session-log entry
Non-Scope
- CI integration (FlaUI requires foreground window; out of scope until CI runners get RDP/VM display)
- accessibility audit, screenshot-diff regression testing, multi-monitor support, or any FlaUI usage beyond what the two scenarios need
- re-capturing rows 0, 0a, 0b, or
slice-1-1-multi-tag-telemetry— they remain valid historical evidence under the retired rig - replacing any of the existing 313 unit tests in
InspectionPrototype.Testswith FlaUI-based equivalents - migrating
Get-GcPauseP95/Get-LohAllocRateAvgextraction helpers (already correct from SLICE-1.2 Pass 2; reused as-is) - new measurement metrics beyond the 18 already produced by
ConvertTo-MeasurementRow
Touched Projects
tests/InspectionPrototype.AcceptanceTests/— new project, scenarios, FakeUiDriver testssrc/InspectionPrototype.UiDriver/(or similar) — new shared library,IUiDriver,FakeUiDriver.FlaUiDrivermay live in the AcceptanceTests project to keep FlaUI deps isolated, OR in this library if implementation finds it cleaner — either is acceptablesrc/InspectionPrototype.App/MainWindow.xaml— addAutomationProperties.AutomationIdto every operator-touched controltests/InspectionPrototype.Tests/— JSON-binding integration test forSimulatorProfilesOptions; AutomationId regression test (parses MainWindow.xaml as XML, asserts the canonical id set is present)tools/Capture-Measurements.ps1— re-introduced as a small wrapper (~80 lines)docs/runbook/capturing-measurements.md— §3a refreshdocs/specs/SLICE-1.5-automated-measurement-capture.mdanddocs/specs/SLICE-1.5.1-automated-capture-followups.md— banner updatedocs/tasks/TASK-1.5-implement-automated-measurement-capture.mdanddocs/tasks/TASK-1.5.1-implement-automated-capture-followups.md— banner updatedocs/reviews/phase-1-measurements.md— slice-1-2-real-frame-payloads row blockdocs/captures/— new CSV evidenceDirectory.Packages.props— FlaUI version pinCLAUDE.md,docs/reviews/roadmap-progress.md
AI Tool Guidance
Three Copilot passes; one-pass-per-session protocol as in TASK-1.5.
- Foundations — new AcceptanceTests project + FlaUI deps,
IUiDriver/FlaUiDriver/FakeUiDriver, AutomationId audit onMainWindow.xaml+ regression test, JSON-binding integration test. NO scenarios, NO orchestrator, NO captures. - Scenarios + orchestrator —
DemoBaselineFlaUi,MultiTagSoakFlaUi, scenario unit tests viaFakeUiDriver, newtools/Capture-Measurements.ps1. NO captures. - Capture + runbook + banner updates — run the 10-min HighFrameRate capture with sleep disabled, append row block, refresh runbook §3a, update SLICE-1.5/1.5.1 banners, update CLAUDE.md and roadmap-progress.md. NO code changes.
Acceptance Criteria Mapping
The implementation must satisfy all acceptance criteria from SLICE-1.6:
- Pass 1 covers criteria 1, 2, 3, 8 (existing tests still pass), plus the JSON-binding finding from the SLICE-1.2 review
- Pass 2 covers criteria 4, 5 (smoke run portion), 9 (Trait), and the FakeUiDriver portion of 8
- Pass 3 covers criteria 6, 7, 10
Copilot Agent Prompts
Pass 1 — Foundations
You are implementing Pass 1 of TASK-1.6: stand up the FlaUI acceptance-test
project, the IUiDriver abstraction, the AutomationId audit on MainWindow.xaml,
and a JSON-binding integration test for SimulatorProfilesOptions.
NO scenarios, NO orchestrator script, NO captures.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md (the requirements)
- docs/tasks/TASK-1.6-implement-flaui-capture.md (this task)
- src/InspectionPrototype.App/MainWindow.xaml
- src/InspectionPrototype.Presentation/ViewModels/MainViewModel.cs (find every IRelayCommand and the controls bound to them)
- src/InspectionPrototype.Infrastructure/InfrastructureServiceCollectionExtensions.cs
- src/InspectionPrototype.Infrastructure/Simulator/SimulatorProfilesOptions.cs
- src/InspectionPrototype.App/appsettings.json
- Directory.Packages.props
- tests/InspectionPrototype.Tests/InspectionPrototype.Tests.csproj (model for the new AcceptanceTests project)
## Scope of this pass
New project + FlaUI deps, driver abstraction, MainWindow.xaml ID audit + regression
test, SimulatorProfilesOptions JSON-binding integration test. NO scenarios, NO
orchestrator. NO captures.
## Deliverables
1. New shared library project src/InspectionPrototype.UiDriver/:
- InspectionPrototype.UiDriver.csproj with TargetFramework net10.0 (NOT
net10.0-windows — this library has zero WPF dependencies; consumers
in WPF projects bring their own)
- Reference: nothing project-internal; this library is standalone
- Files:
- IUiDriver.cs:
Task ClickByAutomationIdAsync(string id, TimeSpan? timeout = null);
Task<string> ReadTextByAutomationIdAsync(string id);
Task<bool> WaitForTextAsync(string id, string expected, TimeSpan timeout);
Task SelectComboBoxItemAsync(string id, string item);
Task<byte[]> CaptureScreenshotPngAsync();
(return byte[] for the screenshot to keep the interface free of System.Drawing types)
- FakeUiDriver.cs:
An in-process implementation backed by:
ConcurrentDictionary<string, FakeElement> _elements
where FakeElement holds CurrentText and an Action OnClick that
tests can configure. Methods record calls into a ClickLog list
that tests can assert against.
WaitForTextAsync polls _elements at 10 ms cadence (fast for tests).
SelectComboBoxItemAsync sets the element's CurrentText.
CaptureScreenshotPngAsync returns Array.Empty<byte>() (tests don't
care about pixel content; just that the method is callable).
2. Add InspectionPrototype.UiDriver to Directory.Packages.props centrally:
- No new package deps (FakeUiDriver uses BCL only).
- Verify the new project compiles standalone with no warnings.
3. New test project tests/InspectionPrototype.AcceptanceTests/:
- InspectionPrototype.AcceptanceTests.csproj with:
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>false</UseWPF> (we drive the WPF app from outside; this project itself isn't WPF)
Reference InspectionPrototype.UiDriver
Package references (centralize versions in Directory.Packages.props):
FlaUI.Core (latest stable; record the chosen version in commit message)
FlaUI.UIA3 (matching version)
xunit, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk (match InspectionPrototype.Tests)
- Files:
- FlaUiDriver.cs — implements IUiDriver using FlaUI.UIA3.UIA3Automation:
Constructor takes a System.Diagnostics.Process and uses
FlaUI.Core.Application.Attach(process) to bind.
ClickByAutomationIdAsync polls the AutomationElement tree at 100 ms
intervals until an element with matching AutomationId is found and
IsEnabled, then invokes InvokePattern.Invoke().
WaitForTextAsync polls at 100 ms; reads element.Properties.Name (or
element.AsTextBlock().Text for TextBlock).
SelectComboBoxItemAsync uses ExpandCollapsePattern then selects the
child whose Name matches.
All time-bounded methods throw TimeoutException with a message
naming the element id and timeout.
- SmokeTest.cs ([Trait("Category","Smoke")]):
A trivial xUnit [Fact] that constructs FakeUiDriver and verifies
ClickByAutomationIdAsync records the click in the log. This proves
the project builds and references resolve. NO scenarios yet.
4. AutomationProperties.AutomationId audit in src/InspectionPrototype.App/MainWindow.xaml:
- Add namespace if missing:
xmlns:auto="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
(AutomationProperties is in the default WPF presentation namespace)
- Required automation IDs (these are the ones SLICE-1.6 scenarios depend on):
Buttons: ConnectButton, DisconnectButton, RefreshCatalogButton,
LoadRecipeButton, HomeButton, StartRunButton, StopButton,
ApplySimulatorProfileButton
ComboBoxes: RecipeSelectorComboBox, SimulatorProfileSelectorComboBox
TextBlocks: WorkflowStateText, ConnectionStateText, ActiveSimulatorProfileText
- Find each in MainWindow.xaml; add AutomationProperties.AutomationId="<Id>".
If a control name doesn't match expected (e.g. it's not the right
ContentPresenter wrapping a Button), audit the binding: the IRelayCommand
in MainViewModel that maps to the operator action determines which
button gets which id.
5. Tests under tests/InspectionPrototype.Tests/Presentation/:
- MainWindowAutomationIdRegressionTest.cs:
* Read MainWindow.xaml as an XML document at test time
* Use XPath or LINQ to XML to find every element with an
AutomationProperties.AutomationId attribute
* Assert each id in the canonical set above is present exactly once
* Print actionable failure: "Missing AutomationId 'StartRunButton' in
MainWindow.xaml. SLICE-1.6 scenarios depend on this id."
* The test must NOT instantiate the WPF app (no [STAThread], no
Application.Current); just parse XAML as XML.
6. Tests under tests/InspectionPrototype.Tests/Infrastructure/:
- SimulatorProfilesOptionsBindingTests.cs (NEW):
* Build an IConfiguration with InMemoryCollection containing a
"Simulator:Profiles:0:Name" = "TestProfile" key (and the rest of
the required SimulatorProfile fields)
* Register options via services.AddOptions<SimulatorProfilesOptions>()
.BindConfiguration(SimulatorProfilesOptions.SectionName)
(mirror the production registration in
InfrastructureServiceCollectionExtensions)
* Build the service provider, resolve IOptions<SimulatorProfilesOptions>
* Assert options.Profiles contains "TestProfile" with the bound fields
* This is the regression test that catches the silent-ignore bug from
the SLICE-1.2 review (where PostConfigure-only registration meant
JSON profiles were ignored).
## Constraints
- Do NOT add scenarios (Pass 2).
- Do NOT add the orchestrator script (Pass 2).
- Do NOT capture anything (Pass 3).
- Do NOT add any production-code dependency on FlaUI. FlaUI lives ONLY in
InspectionPrototype.AcceptanceTests; the rest of the codebase doesn't know
it exists.
- Do NOT change MainViewModel, IRelayCommand, or any binding plumbing.
AutomationId is purely a XAML addition.
- The MainWindowAutomationIdRegressionTest must be in InspectionPrototype.Tests
(the existing pure-domain test project), NOT in AcceptanceTests. The
regression check has no FlaUI dependency.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release (filtered to exclude Trait="Capture" if any exist)
Manual smoke tests:
- launch the app interactively
- in the open window, mentally verify each AutomationId is present by
using Inspect.exe (Windows SDK) or AccessibilityInsights.
Confirm: clicking on each named control finds an element whose
AutomationId matches the expected name.
- if Inspect.exe is unavailable, the regression test in deliverable 5 is
the necessary-and-sufficient signal.
## Report format when finished
- files created and modified
- the chosen FlaUI package version (recorded in commit message)
- confirmation that all 313+ existing tests still pass plus the new
AutomationId regression and JSON-binding integration tests
- a single commit hash
- commit message: "feat(test): add InspectionPrototype.AcceptanceTests + FlaUI driver, MainWindow AutomationIds, JSON-binding regression test (pass 1/3 of TASK-1.6)"Pass 2 — Scenarios + orchestrator
You are implementing Pass 2 of TASK-1.6. Pass 1 (AcceptanceTests project, IUiDriver
abstraction, MainWindow AutomationId audit, JSON-binding test) is already merged.
This pass adds the two scenario [Fact]s and the new tools/Capture-Measurements.ps1
wrapper. NO captures.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md (criteria 4, 5, 9; FakeUiDriver portion of 8)
- src/InspectionPrototype.UiDriver/IUiDriver.cs (Pass 1 output)
- src/InspectionPrototype.UiDriver/FakeUiDriver.cs (Pass 1 output)
- tests/InspectionPrototype.AcceptanceTests/FlaUiDriver.cs (Pass 1 output)
- tests/InspectionPrototype.AcceptanceTests/SmokeTest.cs (Pass 1 output, will be replaced)
- tools/MeasurementExtraction.psm1 (existing — reused unchanged)
- docs/runbook/capturing-measurements.md (§4.1, §4.2 step lists with inter-run Home — scenarios mirror these)
Pass 1 must be merged. Confirm by checking that the AcceptanceTests project
builds and the AutomationId regression test passes.
## Scope of this pass
Two scenario [Fact]s with [Trait("Category","Capture")], scenario unit tests
via FakeUiDriver, new tools/Capture-Measurements.ps1. NO captures, NO new
abstractions.
## Deliverables
1. tests/InspectionPrototype.AcceptanceTests/Scenarios/DemoBaselineFlaUi.cs:
- public class DemoBaselineFlaUi
- [Fact, Trait("Category", "Capture")]
- public async Task RunAsync()
reads DURATION_SECONDS env var (parses int; default 600)
reads APP_PROCESS_ID env var (parses int; throws if missing)
attaches FlaUiDriver to the named process
runs the scenario flow:
await driver.ClickByAutomationIdAsync("ConnectButton");
await driver.WaitForTextAsync("ConnectionStateText", "Connected", 10s);
await driver.ClickByAutomationIdAsync("RefreshCatalogButton");
await driver.WaitForTextAsync("WorkflowStateText", "Idle", 10s); // catalog populated; recipe selectable
await driver.SelectComboBoxItemAsync("RecipeSelectorComboBox", "Standard 5-Point Wafer Scan");
await driver.ClickByAutomationIdAsync("LoadRecipeButton");
await driver.WaitForTextAsync("WorkflowStateText", "Idle", 10s);
await driver.ClickByAutomationIdAsync("HomeButton");
await driver.WaitForTextAsync("WorkflowStateText", "Ready", 30s);
using var durationCts = new CancellationTokenSource(TimeSpan.FromSeconds(durationSeconds));
while (!durationCts.IsCancellationRequested) {
await driver.ClickByAutomationIdAsync("StartRunButton");
await driver.WaitForTextAsync("WorkflowStateText", "Running", 10s);
// wait for terminal state — Completed, Stopped, Faulted, Aborted
// either parameterize WaitForTextAsync to take a predicate, or
// poll Read for any-of-set; pick the cleaner option
await WaitForTerminalStateAsync(driver, durationCts.Token);
if (durationCts.IsCancellationRequested) break;
await driver.ClickByAutomationIdAsync("HomeButton");
await driver.WaitForTextAsync("WorkflowStateText", "Ready", 30s);
}
// tear-down
var ws = await driver.ReadTextByAutomationIdAsync("WorkflowStateText");
if (ws == "Running") {
await driver.ClickByAutomationIdAsync("StopButton");
// best-effort; don't fail the test if Stop hangs
}
await driver.ClickByAutomationIdAsync("DisconnectButton");
- factor the run loop into private methods so the unit test (deliverable 3)
can drive a single iteration against FakeUiDriver
2. tests/InspectionPrototype.AcceptanceTests/Scenarios/MultiTagSoakFlaUi.cs:
- same shape as DemoBaselineFlaUi, with one extra step before Connect:
await driver.SelectComboBoxItemAsync("SimulatorProfileSelectorComboBox", "MultiTag");
await driver.ClickByAutomationIdAsync("ApplySimulatorProfileButton");
await driver.WaitForTextAsync("ActiveSimulatorProfileText", "MultiTag", 5s);
- reuses the same private run-loop method as DemoBaselineFlaUi if practical;
if not, accept a small amount of duplication
3. tests/InspectionPrototype.Tests/Scenarios/ScenarioFlowTests.cs (NEW, in
the existing pure-xUnit test project, NOT AcceptanceTests):
- Reference InspectionPrototype.UiDriver (FakeUiDriver only)
- Tests:
FakeUiDriver_DrivesDemoBaselineFlow_RecordsExpectedClickSequence:
construct FakeUiDriver
pre-seed _elements with the canonical AutomationIds and "Connected"/"Ready"
text values
invoke a single iteration of the run loop (extract from
DemoBaselineFlaUi if needed, or duplicate as a test fixture)
assert ClickLog contains:
["ConnectButton", "RefreshCatalogButton", "LoadRecipeButton",
"HomeButton", "StartRunButton", "HomeButton", "DisconnectButton"]
Scenario_HandlesProfileSelection_BeforeConnect:
(MultiTagSoakFlaUi-equivalent)
assert SelectComboBoxItemAsync("SimulatorProfileSelectorComboBox",...)
was called BEFORE ClickByAutomationIdAsync("ConnectButton")
- These tests do NOT require a window; they verify scenario logic in
isolation.
4. New tools/Capture-Measurements.ps1 (~80 lines):
- parameters:
[Parameter(Mandatory)][string]$Scenario # "DemoBaseline" or "MultiTagSoak"
[Parameter(Mandatory)][int]$DurationSeconds
[Parameter(Mandatory)][string]$OutputCsv
[string]$CommitHash = ''
[string]$Profile = '' # optional; if set, uses --start-with-profile fallback in app launch
[string]$SliceTag = ''
[switch]$AllowDirty
[switch]$AppendToTable
- sequence:
0. Refuse if working tree dirty (unless -AllowDirty).
Default $CommitHash to (git rev-parse --short HEAD).
1. Build Release. Fail script if build fails.
2. Launch the app VISIBLY (no -WindowStyle Hidden):
$appProc = Start-Process -FilePath $AppExe -PassThru
(no --scenario flag; the app starts in normal interactive mode)
3. Poll `dotnet-counters ps` for the new PID (timeout 30 s).
4. Start `dotnet-counters collect` against the PID with
--counters InspectionPrototype,System.Runtime, --refresh-interval 1,
--format csv, --output $OutputCsv.
5. Wait for the app's window to be ready (poll FlaUI from PowerShell
via `Add-Type -Path` to load FlaUI assemblies, OR — simpler —
just sleep 3 seconds; the test itself will retry-poll and so a
fixed sleep is fine).
6. Set environment vars:
$env:APP_PROCESS_ID = $appProc.Id
$env:DURATION_SECONDS = $DurationSeconds
7. Run the test:
dotnet test tests/InspectionPrototype.AcceptanceTests --configuration Release `
--filter "FullyQualifiedName~$Scenario" --no-build --logger "console;verbosity=detailed"
Capture exit code.
8. Stop dotnet-counters (Wait-Process with 5 s timeout, then Stop-Process -Force).
9. Close the app: try `$appProc.CloseMainWindow()` then Wait-Process 5 s,
then `Stop-Process -Force` if still running.
10. Verify $OutputCsv exists and is non-empty.
11. Import-Module .\tools\MeasurementExtraction.psm1 -Force.
12. Run ConvertTo-MeasurementRow with the captured args and print the
18-metric markdown row block. If $AppendToTable, append to
docs/reviews/phase-1-measurements.md under "## Phase 1 rows".
13. If the dotnet-test exit code was non-zero: rename the CSV to
${OutputCsv}.partial, exit 1.
- Document the new flow in the script's comment block at the top.
## Constraints
- Do NOT capture row blocks in this pass — Pass 3 owns the actual SLICE-1.2
row capture.
- Do NOT add new automation IDs to MainWindow.xaml. If a scenario needs
an id that doesn't exist, that's a Pass 1 gap; STOP and add it via a
small Pass 1 fixup commit before continuing.
- Do NOT add a `--start-with-profile` CLI flag to the app yet. The spec
authorizes one as a fallback if FlaUI's ComboBox driving is flaky, but
determine whether it's needed first by running a smoke scenario manually.
- Scenario [Fact]s must NOT run as part of a default `dotnet test` invocation.
The Trait("Category","Capture") is the gate; the orchestrator's
--filter "FullyQualifiedName~$Scenario" picks the specific test.
- Do NOT touch CLAUDE.md, roadmap-progress.md, or runbook §3a in this
pass — Pass 3 owns those.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release (default — should NOT include the Capture-trait scenarios)
Manual smoke test (60 s scenario, not the full 10 minutes):
- tools/Capture-Measurements.ps1 -Scenario DemoBaseline -DurationSeconds 60 `
-OutputCsv docs/captures/_smoke.csv -AllowDirty
- Confirm:
app window appears, FlaUI clicks ARE visible (cursor doesn't move,
but buttons appear pressed; status text changes)
runs.completed > 0 in the printed row block
script exits 0
CSV exists at the named path
- Delete the smoke CSV before commit.
- If the scenario hits a flaky FlaUI step (especially the ComboBox), file
the symptom in the report and add the `--start-with-profile` fallback to
the app NOW (within this pass) per the SLICE-1.6 spec's verification note.
## Report format when finished
- files created and modified
- the smoke-test stdout (the 18-metric markdown row block) — for evidence
that the new rig produces the same format as the retired one
- the chosen approach for waiting on terminal state in the run loop
(predicate-based WaitForText, or poll-and-check) — note in commit message
- whether `--start-with-profile` fallback was needed, and why
- a single commit hash
- commit message: "feat(test,tools): add FlaUI scenario [Fact]s, fake-driver scenario tests, capture orchestrator wrapper (pass 2/3 of TASK-1.6)"Pass 3 — Capture + runbook + banner updates
You are implementing Pass 3 of TASK-1.6, the final pass. Passes 1 and 2 are
merged. This pass runs the deferred slice-1-2-real-frame-payloads capture
under the new rig, refreshes runbook §3a, updates the SLICE-1.5 / SLICE-1.5.1
banners, and updates session-handoff documents. NO code changes.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-1.6-flaui-capture.md (criteria 6, 7, 10)
- docs/specs/SLICE-1.2-real-frame-payloads.md (criterion 6 of that slice — frames.dropped=0, frames.ingested>=17500)
- docs/runbook/capturing-measurements.md (current §3a — sleep-disable + retirement pointer)
- docs/reviews/phase-1-measurements.md (slice-1-1-multi-tag-telemetry row to mirror)
- docs/specs/SLICE-1.5-automated-measurement-capture.md (banner to update)
- docs/specs/SLICE-1.5.1-automated-capture-followups.md (banner to update)
- docs/tasks/TASK-1.5-implement-automated-measurement-capture.md (banner to update)
- docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md (banner to update)
- docs/tasks/TASK-1.2-implement-real-frame-payloads.md (status block — SLICE-1.2 closes when this pass lands)
- CLAUDE.md, docs/reviews/roadmap-progress.md
- tools/Capture-Measurements.ps1 (Pass 2 output)
## Scope of this pass
Capture, table edit, runbook §3a refresh, banner updates, session-handoff
updates. NO code changes.
## Deliverables
1. Disable system sleep BEFORE the capture. Note the previous values for
restoration:
powercfg /change standby-timeout-ac 0
powercfg /change monitor-timeout-ac 0
2. Run the SLICE-1.2 capture under the new rig:
$date = Get-Date -Format 'yyyy-MM-dd'
tools/Capture-Measurements.ps1 -Scenario MultiTagSoak `
-DurationSeconds 600 `
-OutputCsv "docs/captures/slice-1-2-high-fps-$date.csv" `
-CommitHash $(git rev-parse --short HEAD) `
-Profile HighFrameRate `
-SliceTag slice-1-2-real-frame-payloads `
-AppendToTable
Confirm:
* exit code 0
* frames.dropped == 0 (criterion 6 of SLICE-1.2)
* frames.ingested >= 17500 (criterion 6 of SLICE-1.2)
* 18 metrics in the printed row block, including non-em-dash values
for gc-pause-p95 and LOH-alloc-rate avg
* tags.active == 50 in the CSV (regression check)
* the row block was appended to phase-1-measurements.md
If any of these fails, STOP. Capture the failure mode in the report; do
NOT proceed to the table edit, runbook refresh, or banner updates. Hand off.
3. Refresh runbook §3a in docs/runbook/capturing-measurements.md:
- Keep the "Disable system sleep" subsection as-is.
- Replace the "the retired automated capture rig" paragraph with the
new SLICE-1.6 rig description:
- one-paragraph rationale
- new orchestrator invocation:
tools/Capture-Measurements.ps1 -Scenario <name> -DurationSeconds <secs> `
-OutputCsv docs/captures/<name>-<date>.csv `
-CommitHash $(git rev-parse --short HEAD)
- subsection "FlaUI prerequisites":
foreground window required (no headless or RDP-disconnected)
no screen lock during capture (locks the workstation, dispatcher pauses)
Windows display scaling = 100% recommended (FlaUI element coords
use logical pixels; non-100% can shift hit-test under some controls)
- subsection "Registered scenarios" (mirror the structure from before):
DemoBaseline | DemoBaselineFlaUi
MultiTagSoak | MultiTagSoakFlaUi
- subsection "When to fall back to manual §3":
fast exploratory captures (manual is faster than the rig for <60s runs)
machines without FlaUI installed (partial dev environments)
- Remove the "retired rig" Note block — the rig is no longer retired
in the abstract; SLICE-1.6 IS the rig. Add one short historical line:
"An earlier headless rig (SLICE-1.5) drove ICommand directly without
rendering the UI; that rig was retired 2026-04-27 in favor of this
UI-Automation approach."
4. Update SLICE-1.5 / SLICE-1.5.1 banners:
- In docs/specs/SLICE-1.5-automated-measurement-capture.md, replace
"Status: Superseded by SLICE-1.6 (FlaUI capture, planned)" with
"Status: Superseded by SLICE-1.6 (FlaUI capture, shipped)" and update
the banner prose to reference SLICE-1.6 as a real, landed slice.
- Same edit in docs/specs/SLICE-1.5.1-automated-capture-followups.md.
- Same edit in docs/tasks/TASK-1.5-implement-automated-measurement-capture.md.
- Same edit in docs/tasks/TASK-1.5.1-implement-automated-capture-followups.md.
5. Update TASK-1.2 status block:
- "Status: Passes 1 & 2 complete; Pass 3 deferred" becomes
"Status: Passes 1 & 2 complete; Pass 3 captured under SLICE-1.6"
- Add a one-line note at the bottom referencing the slice-1-2-real-frame-payloads
row block in phase-1-measurements.md and the CSV path under docs/captures/.
6. Update CLAUDE.md "Current position" block:
- Phase: 1 (Simulator to scale) — SLICE-1.6 complete; SLICE-1.2 closed
(Pass 3 row captured under the FlaUI rig)
- Last completed: TASK-1.6 Pass 3 — captured slice-1-2-real-frame-payloads
row (10-min HighFrameRate, 18 metrics, frames.dropped=0,
frames.ingested=<count>); refreshed runbook §3a; updated SLICE-1.5/1.5.1
banners to "shipped"; commit <hash>
- Next action: SLICE-1.3 (Encoder-rate motion); spec needs to be written
- Blocked on: nothing
- Last updated: <today's date>
7. Append session-log entry to docs/reviews/roadmap-progress.md naming the
row block, frames.dropped, frames.ingested, gc-pause-p95, LOH-alloc-rate-avg,
and the commit hash. Mark SLICE-1.6 as Completed in the progress table;
move SLICE-1.2 from "In Progress (Pass 3 deferred)" to "Completed".
8. Restore powercfg settings:
powercfg /change standby-timeout-ac <previous_value_in_minutes>
powercfg /change monitor-timeout-ac <previous_value_in_minutes>
## Constraints
- Do NOT make any code or test changes.
- Do NOT capture without disabling system sleep (deliverable 1).
- Do NOT move on to deliverables 3+ if the capture in deliverable 2 fails.
- Do NOT modify SLICE-1.5 or SLICE-1.5.1 spec/task BODIES — only the banner
blocks at the top.
- Do NOT re-capture rows 0, 0a, 0b, or slice-1-1-multi-tag-telemetry.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release (default; Capture-trait scenarios still excluded)
Plus:
- the docs/captures/slice-1-2-high-fps-<date>.csv file exists and is committed
- the row block is in docs/reviews/phase-1-measurements.md with all 18
metrics filled, frames.dropped = 0, frames.ingested >= 17500
- runbook §3a renders correctly (no broken markdown, no stale links)
- SLICE-1.5 / SLICE-1.5.1 banners reference the now-shipped SLICE-1.6
- CLAUDE.md current-position block reflects SLICE-1.6 closure
## Report format when finished
- files created and modified
- the captured row block (the 18-metric markdown table) included in the report
- gc-pause-p95 and LOH-alloc-rate-avg values, with one-sentence interpretation
(e.g. "p95 GC pause was X ms; LOH alloc rate Y MB/s — consistent with
2 MB × 30 fps = 60 MB/s")
- a single commit hash
- commit message: "feat(measurements): close SLICE-1.6 with slice-1-2-real-frame-payloads row; runbook §3a refreshed (pass 3/3 of TASK-1.6)"Operator notes
- One pass per Copilot session. Same protocol as TASK-1.5.
- Pass 1's
IUiDriverlives in a standalone library, not in the AcceptanceTests project. This is non-obvious; the agent may suggest collapsing it. Refuse — theFakeUiDriverneeds to be reachable from the existingInspectionPrototype.Testsproject for scenario unit tests, and pulling the FlaUI dep transitively into that project would break the "FlaUI is isolated" rule. - Pass 1's MainWindow
AutomationIdaudit is non-cosmetic. A missing id silently breaks Pass 2's scenarios; the regression test in deliverable 5 is the load-bearing canary. - Pass 2's smoke test is the load-bearing verification. A 60-second scenario run that produces a non-zero
runs.completedproves the FlaUI driver actually drives the UI. If this fails, do not proceed to Pass 3. - Pass 3 must run with sleep disabled. Same rule as TASK-1.2's deferred Pass 3 — the LOH-alloc-rate metric the slice exists to measure is diluted by any sleep duration.
- The FlaUI ComboBox quirk is real. If selecting the simulator profile via FlaUI proves unreliable in Pass 2, the spec authorizes adding a
--start-with-profile <name>CLI flag to the app within this slice, not as a follow-up. Pass 2 owns that decision. - Update the index files only at the end of the phase, not per-slice. Same rationale as earlier tasks.