Skip to content

chore(system-test): put the HeadlessBench rig in git, where a git clean cannot reach it - #261

Merged
tylerkron merged 3 commits into
mainfrom
builder/issue-254-headlessbench
Sep 5, 2026
Merged

tylerkron merged 3 commits into
mainfrom
builder/issue-254-headlessbench

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

closes #254

Not merging — for review.

This PR is net-additive, and that is the point. It does not make the codebase smaller or flatter. It buys durability for 413 lines of working system-test rig that existed only as untracked files in one worktree, where git clean -fdx or removing that worktree would have destroyed it.

Diff: +1449 / −7 across 8 files — 413 of those lines are the rescued source (Program.cs 380 + .csproj 33), 885 are a machine-generated lock file, 125 are the new README, and +26 / −7 is the integration into four existing files.

What is preserved

tools/system-test/HeadlessBench/ — Program.cs (380 lines), HeadlessBench.csproj (33), packages.lock.json (885, machine-generated). Committed substantially as-is: no rewrite, no tidy-up. A rescue mixed with a refactor is unreviewable, and improvements are noted as follow-ups rather than smuggled in here. bin//obj/ deliberately excluded (637 MB of build output).

It is the only thing in the repo that exercises the device → Core → view-model → rendered-window path end to end. The 530-test unit suite covers none of it, and AvaloniaCapture renders screens but never connects to hardware.

⚠️ It is not the version the ticket describes — filed as #260

#254's Current state says the rig gained 9 more matrix rows (DEV-TILE, CH-TILE, DEV-NAME, DEV-RATE, CH-DIO, CH-PWM, SD-LIST, DEV-NET, DEV-DEBUG) plus a DAQIFI_RESTORE_NAME repair mode. None of them are in the source that reached me — grep for all ten strings returns nothing, and the file's own header still says it covers the original seven. The line count agrees: 413 against the ticket's "~450".

So the 9-row expansion was lost before this rescue could catch it, which is #254's own failure mode arriving one revision too late. Recorded in #260 rather than papered over, with the spec for re-adding them.

Preserving the 7-row version is still worth doing. It builds, and it runs (below).

The four decisions, and why

1. Not in Daqifi.Avalonia.slnx. #254 asks for this, but it is also just the house convention: the slnx holds the five app/test projects and nothing else — not AvaloniaCapture, not WpfCapture, not the vendored OxyPlot. And slnx membership was never what gated anything, because CI builds by explicit csproj path, not by solution.

2. But it IS in CI, as a compile-only gate. This is the half of "keep it out of the solution" that would otherwise leave it to bit-rot, and it is the decision with a real trade, so:

The worry — "CI will build it and it needs hardware" — does not apply. Compiling is not running. The rig only opens a serial port when given --port; a dotnet build never does. No runner needs a board.

Why it earns its step. The rig reaches deep into internals — DaqifiViewModel, ConnectionDialogViewModel.ConnectManualSerialCommand, LoggingManager.Subscribe, shell.Plotter.LoggedPoints — none of it public API, all of it moving. #245 and #246 both deleted DaqifiViewModel members after searching the repo for readers. A rig outside the repo is invisible to that search; it would have been broken silently and nobody would have known until the next bench session. Now it breaks the build instead. That is exactly the rationale already written into this workflow for AvaloniaCapture ("a change that broke it landed green").

It is added in three places, mirroring AvaloniaCapture line for line: Restore, Build, and the from-scratch lock-derivation check. Never run in CI — that needs a board, and the comment says so.

3. The lock file is committed, and here is the proof rather than the assumption.

It is byte-identical to tools/parity-audit/AvaloniaCapture/packages.lock.json — diff between them is empty. That is not a coincidence: identical TFM (net10.0), no RID, the same six PackageReferences at the same versions, the same single ProjectReference. Identical restore inputs, identical output.

That identity carries the ubuntu leg for free: AvaloniaCapture's copy already passes CI's locked restore and the from-scratch-derivation check on ubuntu today, including the case-sensitivity trap from #106.

Rehearsed locally on top of that:

check result
dotnet msbuild ... -getProperty:RestoreLockedMode -p:ContinuousIntegrationBuild=true true — the gate arms for this project
locked restore from a cleared obj/ restored; lock file byte-unchanged
dotnet restore ... --force-evaluate lock file byte-unchanged
git status --short -- '**/packages.lock.json' after every build, including the -r iossimulator-arm64 one empty

No -r was passed to any restore. The rig's csproj already carries the NU1004 warning in its header; the new README repeats it.

4. It was untracked, not ignored. git check-ignore -v on all three files exits 1 — nothing in any .gitignore matches. Nothing needed fixing, and nothing was changed; bin//obj/ are already covered repo-wide.

Documentation

New tools/system-test/HeadlessBench/README.md — what it does, how to run it, the output layout, the two traps it encodes, and the two build constraints. tools/parity-audit/README.md is untouched (it is a live merge-conflict hotspot between #256 and #258).

Three consequential doc fixes my own change forced, not gold-plating:

  • Directory.Build.props said "Six of the seven projects carry lock files" — adding an eighth governed project makes that seven of eight.
  • .github/scripts/refresh_lock_files.sh gains the project in PROJECTS, or its lock file is the one nothing ever refreshes — the exact staleness the script's own comment warns about.
  • .github/dependency-updates/README.md enumerates the leaf projects twice; both now include it.

It runs — against the real bench board

Board lock held; connect / stream 5 s / disconnect only. No firmware, WiFi, SD, reboot or power-cycle. The rig redirects DAQIFI_DATA_DIR to <out>/appdata itself, and the run demonstrably built a whole app-data tree there (DB, config, Logs/, SentryCache/) — the real ~/Library/Application Support/DAQiFi was never opened. lsof on the port after exit: nothing.

[OK]   app boot
[OK]   CONN-USB/works: connected /dev/cu.usbmodem1101 in 5.6 s; ConnectedDevices=1; status='Connected'
[OK]   DEV-INFO/works: serial='9090539562006014104' fw='3.7.2' name='UITest-191020' channels=32
[OK]   CH-AI/works: 'AI0' IsActive=True subscribed=True; CanToggleLogging=True
[FAIL] STREAM-AI/works: 401 samples in 5.0 s after a 2.0 s start latency -> 79.7 Hz effective
       (set 100 Hz) on 'AI0'; IsStreaming after stop=False
[OK]   LOG-SESSION/works: IsLogging toggled on/off; LoggingManager.Active=False; IsLogging=False
[OK]   GRAPH-LIVE/works: PlotLogger had 401 points across 1 series while streaming
[OK]   CONN-DISC/works: shell.ConnectedDevices=0; manager.ConnectedDevices=0

The one [FAIL] is the board, not the app, and the rig is what proves it. Its unexpected probe separates the layers:

STREAM-AI finding :: arrival gaps: max 14 ms, 0 over 100 ms across 402 samples;
  device-stamped span 4.01 s vs wall-clock 5.03 s (ratio 0.797; device counter 42000000 Hz)
  -> 99.8 Hz by device timestamps, 79.7 Hz by wall clock

The board believes it sampled at 100 Hz; wall clock says it did not. That is the known fw-3.7.2 timebase behaviour — not filed, and not an app regression. Distinguishing "the device sampled slower" from "the app got the samples late" is the entire reason this rig exists, and it did it unprompted.

Also seen and deliberately not chased here — logged in #260: after a successful disconnect, ConnectionStatusString still reads 'Connected' while both device collections are empty.

Verification

gate result
dotnet build Daqifi.Avalonia.Desktop succeeded, 191 warnings
dotnet build Daqifi.Avalonia.iOS -c Release -r iossimulator-arm64 --no-restore succeeded, 192 warnings
dotnet test Daqifi.Avalonia.Tests 530 / 530 passed
python3 .github/scripts/test_refresh_lock_files.py OK, now against 8 lock files
workflow YAML parses; job list unchanged

Both warning figures are MSBuild all-project totals, ~20 of which are third_party AVP1002.

Warning delta on both gate heads: zero — by construction, not by luck. The only change to any file MSBuild reads is inside an XML comment in Directory.Build.props. No .cs, .axaml, .csproj or property that Desktop, iOS or Tests compile is touched, and the new project is in neither head's graph, so their inputs are byte-identical to main's.

Disclosed: the new project does add one first-party warning to its own compile — Program.cs(346,13) CS0618: 'Bitmap.Save(Stream, int?)' is obsolete. AvaloniaCapture already emits that same warning twice from the identical opaque.Save(stream) call, so the CI job compiling both goes from 2 to 3. Left alone on purpose: fixing it is an improvement to the rig, which this PR is not for; it should be fixed in both files together.

The Android head is not buildable on this Mac (workload absent) — CI covers it.

Review outcome

Qodo round 1 raised 6 bugs, all against the rescued Program.cs. Round 2 on the same head: 0. Every thread answered and resolved.

One was rebutted — "rendered UI is never asserted". The PNG is evidence, not the assertion; each Step's UI half is the bound view-model state (ai.IsActive + LoggingManager.SubscribedChannels, shell.Plotter.LoggedPoints, ConnectedDevices), none of which a failed render makes pass. Pixel assertions are AvaloniaCapture's job, and Qodo's own precedent search cited #16 rejecting the same thing as non-fatal.

Five were conceded, and the strongest deserves naming: CONN-DISC calls ConnectionManager.Instance.Disconnect while a user's click runs DaqifiViewModel.DisconnectDeviceCommand, which also unsubscribes active channels from LoggingManager, removes the firmware notification and clears SelectedDevice. The row can pass while that cleanup is broken — a genuine asymmetry, since the connect side does go through the dialog's command. Verified against the source, not conceded on trust.

Program.cs is still byte-for-byte what was recovered. That is the deliberate line: this PR's review anchor is that the rescued file is unmodified, and the original is no longer reachable to diff a changed version against. So the conceded fixes went to #260, and — because a silent gap is the real hazard — all five are now written into the rig's README under "Known gaps — read before trusting a green run". A green run can no longer be over-read.

The one thing that was fixed here is the part I actually authored: the README's run example used a fixed --out path while results.jsonl is append-only, which is what made stale-result accumulation reproducible. The example now stamps the directory, with an explicit warning.

Also corrected, prompted by chasing that disconnect finding: I had filed ConnectionStatusString still reading 'Connected' after disconnect as a possible app bug. It is not one — ConnectionManager.ConnectionStatus is documented as the status of the most recent Connect call, explicitly "Not a per-device result" (IsDisconnected is the connectivity signal; misreading it was #212). Withdrawn on #260.

Merge note

#256 also touches .github/workflows/build.yml, but only a "24 screens" → "25 screens" comment at line 160; this PR inserts a comment block and two build lines below it. Different lines, so git should merge them, but whoever lands second should glance at it. Neither #255, #256 nor #258 touches tools/system-test.

Verbatim copy of tools/system-test/HeadlessBench from the worktree it lived in
untracked. bin/ and obj/ deliberately excluded. Integration with CI, the
lock-file refresh script and a README follow in this branch.

Refs #254
Compile it in CI, refresh its lock file with the others, and document it.

Refs #254
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Preserve HeadlessBench system-test rig with CI compile coverage

🧪 Tests ✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Preserves the seven-row, hardware-backed HeadlessBench rig as a tracked system-test project.
• Exercises connection, streaming, logging, rendering, and disconnection through user-facing paths.
• Adds compile-only CI and lock-file maintenance to prevent silent rig decay.
Diagram

sequenceDiagram
    actor User as Test Operator
    participant Bench as HeadlessBench
    participant VM as App ViewModels
    participant Core as Device Core
    participant UI as Headless Window
    participant Files as Run Artifacts
    User->>Bench: Start with serial port
    Bench->>VM: Boot app and connect
    VM->>Core: Open device path
    Core-->>VM: Return status and samples
    Bench->>VM: Toggle channel and logging
    VM->>UI: Update live state
    Bench->>UI: Pump and capture
    Bench->>Files: Write JSONL and PNGs
    Bench->>VM: Disconnect device
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add HeadlessBench to the solution
  • ➕ Standard solution builds would discover the project automatically.
  • ➕ IDE users could find and build the rig more easily.
  • ➖ Breaks the repository convention that excludes tool projects from the solution.
  • ➖ Provides little CI benefit because workflows already build explicit project paths.
  • ➖ Broadens routine solution operations with a specialized hardware tool.
2. Run on a self-hosted hardware runner
  • ➕ Continuously validates serial communication and rendered behavior, not only compilation.
  • ➕ Detects runtime regressions across the complete device-to-window path.
  • ➖ Requires dedicated boards, serial-port isolation, maintenance, and recovery automation.
  • ➖ Hardware availability and timing behavior could introduce flaky required checks.
  • ➖ Significantly expands scope beyond preserving the existing rig.
3. Move the rig to a separate repository
  • ➕ Separates hardware-lab tooling and its operational dependencies from the application repository.
  • ➕ Allows independent ownership and release cadence.
  • ➖ Makes internal API changes invisible to repository-wide searches and compile gates.
  • ➖ Recreates the durability and synchronization risks this PR addresses.
  • ➖ Complicates coordinated dependency and lock-file maintenance.

Recommendation: Keep the rig in this repository, outside the solution, and compile it explicitly in CI. This best preserves the rescued implementation while immediately detecting internal API drift; automated hardware execution is a valuable follow-up once reliable runner infrastructure exists.

Files changed (8) +1420 / -7

Tests (2) +413 / -0
HeadlessBench.csprojDefine the HeadlessBench executable project +33/-0

Define the HeadlessBench executable project

• Creates a RID-less .NET 10 executable referencing the main Avalonia project and required headless rendering packages. The project intentionally remains outside the solution.

tools/system-test/HeadlessBench/HeadlessBench.csproj

Program.csAdd hardware-backed headless system-test sequence +380/-0

Add hardware-backed headless system-test sequence

• Boots the real Avalonia application headlessly and exercises seven connection, device, channel, streaming, logging, plotting, and disconnection checks against physical hardware. It records timing probes, JSONL evidence, screenshots, and isolated application data; scripted mode remains an unimplemented stub.

tools/system-test/HeadlessBench/Program.cs

Documentation (3) +103 / -5
README.mdDocument HeadlessBench lock-file coverage +5/-3

Document HeadlessBench lock-file coverage

• Adds HeadlessBench to the documented set of leaf projects and explains why project-by-project restoration must include it outside the solution.

.github/dependency-updates/README.md

Directory.Build.propsUpdate governed project lock-file counts +2/-2

Update governed project lock-file counts

• Adjusts the dependency-lock policy commentary to account for HeadlessBench as the seventh governed project carrying a lock file.

Directory.Build.props

README.mdDocument HeadlessBench operation and constraints +96/-0

Document HeadlessBench operation and constraints

• Explains the rig's end-to-end purpose, supported checks, command-line interface, artifacts, data isolation, known traps, and compile-only CI treatment.

tools/system-test/HeadlessBench/README.md

Other (3) +904 / -2
refresh_lock_files.shRefresh the HeadlessBench dependency lock +4/-2

Refresh the HeadlessBench dependency lock

• Adds the HeadlessBench project to the canonical lock-file refresh list and updates comments describing projects excluded from the solution.

.github/scripts/refresh_lock_files.sh

build.ymlCompile and validate HeadlessBench in CI +15/-0

Compile and validate HeadlessBench in CI

• Restores and builds HeadlessBench explicitly without running its hardware sequence. Also includes its lock file in unchanged-restore and from-scratch derivation checks.

.github/workflows/build.yml

packages.lock.jsonLock HeadlessBench dependency closure +885/-0

Lock HeadlessBench dependency closure

• Commits the generated net10.0 dependency graph for reproducible locked restores, including the main application project and Avalonia runtime dependencies.

tools/system-test/HeadlessBench/packages.lock.json

@qodo-code-review

qodo-code-review Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Rendered UI is never asserted ✗ Dismissed 🐞 Bug ≡ Correctness
Description
Capture converts rendering failures into null artifacts, while each Step decides pass/fail
solely from device or view-model state. The rig can therefore pass every advertised UI check when
the window cannot render or the rendered UI is wrong.
Code

tools/system-test/HeadlessBench/Program.cs[R349-352]

+        catch (Exception ex)
+        {
+            Console.WriteLine($"[WARN] capture {name}: {ex.Message}");
+            return null;
Relevance

●● Moderate

The documented end-to-end intent supports failing render errors, but similar capture omissions were
previously rejected as non-fatal.

PR-#16

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The DEV-INFO and other steps derive their boolean verdict from model state and merely pass
Capture(...) as an artifact. Capture catches every rendering exception and returns null, so no
rendered-window condition can fail those steps despite the README's explicit end-to-end UI assertion
claim.

tools/system-test/HeadlessBench/Program.cs[131-165]
tools/system-test/HeadlessBench/Program.cs[328-353]
tools/system-test/HeadlessBench/README.md[3-10]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
HeadlessBench treats screenshots as optional artifacts rather than assertions, so rendering failures and incorrect rendered UI can still produce passing checks.

## Issue Context
The rig is documented as asserting both Core/device state and rendered-window state. `Capture` currently returns `null` after any rendering failure, and callers do not incorporate that result into the step verdict.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[131-224]
- tools/system-test/HeadlessBench/Program.cs[328-353]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Hardware cleanup is not guaranteed ✗ Dismissed 🐞 Bug ☼ Reliability
Description
Once logging starts, stop, event unsubscription, and device disconnection are only performed on the
normal sequential path. Any exception during measurement or result emission skips those operations,
potentially leaving the board streaming and the serial connection open until process termination.
Code

tools/system-test/HeadlessBench/Program.cs[R193-196]

+            shell.IsLogging = true;
+            var started = PumpUntil(() => first is not null, TimeSpan.FromSeconds(10));
+            var latency = sw.Elapsed.TotalSeconds;
+            if (started) { PumpFor(TimeSpan.FromSeconds(_seconds)); }
Relevance

●● Moderate

Cleanup-on-exception is reliability-critical, but no closely matching accepted precedent was found
for this hardware sequence.

PR-#124

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Logging is enabled at line 193, while its only explicit stop and handler removal occur at lines
204-206 and disconnect occurs later at 255. An exception reaches the outer finally, which only calls
lifetime shutdown; DaqifiViewModel.Dispose cleans owned subscriptions but does not stop this
measurement or run the ConnectionManager disconnect path.

tools/system-test/HeadlessBench/Program.cs[193-206]
tools/system-test/HeadlessBench/Program.cs[251-256]
tools/system-test/HeadlessBench/Program.cs[101-111]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[2905-2967]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Exceptions after logging begins can bypass logging shutdown, event unsubscription, dialog cleanup, and device disconnection.

## Issue Context
The outer finally only shuts down the Avalonia lifetime. The main view-model disposal path removes subscriptions but does not perform the harness's required logging stop and ConnectionManager disconnect sequence.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[120-266]
- tools/system-test/HeadlessBench/Program.cs[101-111]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[2905-2967]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Results accumulate across runs ✓ Resolved 🐞 Bug ≡ Correctness
Description
Every result is appended to an existing results.jsonl, but startup never clears or recreates that
file. Reusing the documented output path mixes stale and current verdicts, so consumers cannot tell
which records belong to the latest run.
Code

tools/system-test/HeadlessBench/Program.cs[R283-285]

+        if (artifact is not null) { rec["artifact"] = artifact; }
+        if (seconds is not null) { rec["seconds"] = Math.Round(seconds.Value, 2); }
+        File.AppendAllText(Path.Combine(_out, "results.jsonl"), JsonSerializer.Serialize(rec, Json) + "\n");
Relevance

●●● Strong

Accepted harness precedent explicitly clears prior outputs to prevent stale artifacts masking
current results.

PR-#16
PR-#179

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Startup only creates the shots and appdata directories. Emit always uses File.AppendAllText,
and the README's example uses a stable /tmp/headlessbench-run path without requiring it to be new,
making stale record accumulation directly reproducible.

tools/system-test/HeadlessBench/Program.cs[53-61]
tools/system-test/HeadlessBench/Program.cs[277-286]
tools/system-test/HeadlessBench/README.md[30-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
HeadlessBench appends current results to any results.jsonl left by an earlier run.

## Issue Context
The output directories are created at startup, but the result file is never truncated, replaced, or rejected when already present.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[53-61]
- tools/system-test/HeadlessBench/Program.cs[277-286]
- tools/system-test/HeadlessBench/README.md[30-57]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Setup failures escape diagnostics ✗ Dismissed 🐞 Bug ☼ Reliability
Description
Output-directory creation, environment setup, and icon registration run before the guarded app-boot
block. A permission, invalid-path, or setup error therefore terminates with an unhandled exception
instead of the harness's documented controlled failure output.
Code

tools/system-test/HeadlessBench/Program.cs[R57-60]

+        Directory.CreateDirectory(Path.Combine(_out, "shots"));
+        Directory.CreateDirectory(Path.Combine(_out, "appdata"));
+        Environment.SetEnvironmentVariable("DAQIFI_TEST_MODE", "1");
+        Environment.SetEnvironmentVariable("DAQIFI_DATA_DIR", Path.Combine(_out, "appdata"));
Relevance

●●● Strong

Directly matches accepted AvaloniaCapture precedent requiring setup failures to use controlled
diagnostics.

PR-#20
PR-#78

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The first try block begins only at line 69, after directory creation and other setup. Past accepted
fixes in the sibling AvaloniaCapture harness established that path and data-directory setup must be
inside the controlled failure boundary.

tools/system-test/HeadlessBench/Program.cs[53-82]
PR-#20
PR-#78

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fallible setup executes outside every try/catch and can crash the harness without a controlled failure record or diagnostic.

## Issue Context
Argument path normalization is handled, but directory creation, environment-variable setup, and icon registration can still fail before the app-boot catch is entered. Similar setup-boundary defects were previously accepted in AvaloniaCapture.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[53-82]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Disconnect bypasses user cleanup ✗ Dismissed 🐞 Bug ≡ Correctness
Description
The harness calls ConnectionManager.Disconnect directly instead of the view-model disconnect
command, bypassing channel unsubscription, firmware-notification cleanup, and selected-device
clearing. Consequently CONN-DISC can pass while the real user disconnect path is broken, and
LoggingManager retains the subscribed channel.
Code

tools/system-test/HeadlessBench/Program.cs[R254-256]

+        sw.Restart();
+        ConnectionManager.Instance.Disconnect(device);
+        PumpUntil(() => shell.ConnectedDevices.Count == 0 && ConnectionManager.Instance.ConnectedDevices.Count == 0, TimeSpan.FromSeconds(10));
Relevance

●● Moderate

The concern is plausible, but historical evidence does not confirm whether harnesses must invoke the
complete user disconnect command.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
HeadlessBench subscribes the channel at lines 159-160 and later invokes only the connection manager.
The application's disconnect command first unsubscribes every active channel, then disconnects and
clears firmware/selection state, proving the harness bypasses part of the user's path.

tools/system-test/HeadlessBench/Program.cs[155-165]
tools/system-test/HeadlessBench/Program.cs[251-263]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1023-1045]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
HeadlessBench disconnects through the lower-level manager rather than the actual view-model command, bypassing user-path cleanup and weakening the CONN-DISC check.

## Issue Context
The harness manually subscribes the analog channel. `DaqifiViewModel.DisconnectDevice` unsubscribes active channels and performs additional UI cleanup before delegating to ConnectionManager.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[155-165]
- tools/system-test/HeadlessBench/Program.cs[251-263]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs[1023-1045]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
6. Logging session never verified ✗ Dismissed 🐞 Bug ≡ Correctness
Description
The LOG-SESSION row passes when a sample arrived and the logging toggles returned to false,
without checking that a session or any database samples were persisted.
Database/session-finalization failures can therefore be reported as a successful logging session.
Code

tools/system-test/HeadlessBench/Program.cs[R217-219]

+            Step(2, "LOG-SESSION", "works", started && !LoggingManager.Instance.Active && !shell.IsLogging,
+                 $"IsLogging toggled on/off; LoggingManager.Active={LoggingManager.Instance.Active}; IsLogging={shell.IsLogging} (DB row count is a follow-up Step)",
+                 shot);
Relevance

●● Moderate

Persistence verification is semantically reasonable, but historical evidence does not establish this
harness’s expected database assertion scope.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The row's predicate checks only started, LoggingManager.Active, and shell.IsLogging.
LoggingManager performs session finalization separately on a fire-and-forget task, and the harness
neither waits for it nor validates the resulting database/session data.

tools/system-test/HeadlessBench/Program.cs[193-219]
Daqifi.Avalonia/Daqifi.Desktop/Loggers/LoggingManager.cs[109-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The LOG-SESSION result does not verify creation or persistence of a logging session, allowing database failures to pass.

## Issue Context
Stopping logging schedules session sample-count persistence asynchronously. The harness currently tests only toggle state and explicitly leaves database row verification as a follow-up.

## Fix Focus Areas
- tools/system-test/HeadlessBench/Program.cs[193-219]
- Daqifi.Avalonia/Daqifi.Desktop/Loggers/LoggingManager.cs[109-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⏭️ Skipped: The latest push only adds and edits README documentation, with no runtime, configuration, test, or build behavior changes.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

… green run means more than it does

Qodo round 1 was right on three counts that are documentation problems, not
rescue problems:

  * The run example used a fixed --out path while results.jsonl is appended and
    never truncated, so following the README literally mixes two runs' verdicts
    with nothing marking the boundary. The example now stamps the directory.
  * CONN-DISC calls ConnectionManager.Disconnect, but a user's click runs
    DaqifiViewModel.DisconnectDeviceCommand, which also unsubscribes active
    channels from LoggingManager, removes the firmware notification and clears
    SelectedDevice. The row can pass while that cleanup is broken.
  * LOG-SESSION asserts toggle state only; session finalization is fire-and-forget
    and unverified.

Program.cs is deliberately untouched — this PR's review anchor is that the
rescued rig is a verbatim copy. The fixes are queued on #260.

Refs #254, #260
@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e0d32ed

@tylerkron

Copy link
Copy Markdown
Contributor Author

Shepherd review — blast radius, merge order, and two disclosure defects

This PR changes Directory.Build.props and .github/workflows/build.yml, so it can affect
every project and every other open PR. Four PRs are open whose CI greens all predate this
change
(#255 8db9f90a, #256 49be77c2, #258 3cc7c432, #259 7a6043f8), and the
repo's ruleset has strict: false, so GitHub will keep showing those stale greens as if
they covered the new main. I test-merged and ran the real gate rather than reading badges.

Measured against main = b9242618, PR head = e0d32ed3.

1. Directory.Build.props is genuinely comment-only

Not taken on trust. I parsed both revisions as XML with comments stripped and compared the
full element tree — tags, sorted attributes, text and tails:

main sha256: 6eeddb119ec3d9c4e957e909bb69ed906ae039bfca0e66082ebd869f0f598f9d
head sha256: 6eeddb119ec3d9c4e957e909bb69ed906ae039bfca0e66082ebd869f0f598f9d
IDENTICAL AFTER STRIPPING COMMENTS: True

Nothing MSBuild reads has changed. That is what makes the blast radius acceptable, and
it is why no other PR's warning counts or lock files are invalidated. The new comment text
is also factually right: this file governs 8 projects (9 .csproj minus the vendored
OxyPlot, which has its own Directory.Build.props), and 7 carry lock files — WpfCapture
is the deliberate opt-out. Counted both.

Independently confirmed by measurement: I captured the first-party warning set (by code,
excluding vendored third_party AVP1002) on my own merge-base b9242618 and on
e0d32ed3 with --no-incremental builds. diff of the two sets is empty — zero delta,
zero new codes. Desktop total 191 warnings, matching the body exactly.

2. The new CI gate cannot break on a hardware-less runner

Three references, all in the desktop job (runs-on: ubuntu-latest) — restore, build,
and the from-scratch lock-derivation restore. There is no dotnet run, and the rig only
opens a serial port when given --port. The claim holds.

Reverse direction — does the gate reach the other PRs' work? It names one .csproj
explicitly; there is no directory glob, so it is not the #225 hazard. But it does
ProjectReference Daqifi.Avalonia, so it compiles whatever any other PR changes in the
app. That is the real coupling, and it is the point of the gate — so I built it against
every combination rather than reasoning about it (table below).

The modified locked-restore step still restores every existing project: refresh_lock_files.sh
now lists 8 projects, exactly matching the 8 committed packages.lock.json files, and it
passes -r nowhere (--force-evaluate only). No existing lock file is rewritten to a single RID.

3. The lock-file identity claim is true

$ cmp tools/system-test/HeadlessBench/packages.lock.json \
      tools/parity-audit/AvaloniaCapture/packages.lock.json
(no output — byte-identical)

And it is identical for a reason a reviewer can check: diff of the two .csproj files
shows differences only in comments and AllowUnsafeBlocks (which does not affect restore).
Same TFM, same six PackageReferences, same single ProjectReference, both RID-less —
identical restore inputs. So AvaloniaCapture's existing ubuntu green carries this one, and
the NU1004 precedent from #85 does not apply. No lock drift appeared after any build
here, including the -r iossimulator-arm64 one.

4. Merge-order matrix — measured, not queried

Every row is a real git merge plus a real build. Gate = desktop build + HeadlessBench
compile
(the new gate) + dotnet test; the two bracketing rows also got the full iOS
-c Release -r iossimulator-arm64 leg.

combination merge HeadlessBench tests verdict
main + #261 fast-forward builds 530 / 530 safe (+ iOS Release ✓)
#261 + #255 clean builds 560 / 560 safe
#261 + #256 clean (build.yml auto-merged) builds 538 / 538 safe
#261 + #258 clean builds 535 / 535 safe
#261 + #259 clean builds 535 / 535 safe
all five see note builds 578 / 578 safe (+ iOS Release ✓)

Test counts are exactly additive (530 + 30 + 8 + 5 + 5 = 578), so nothing interferes.
Desktop stayed at 191 warnings on the five-way, and no lock file moved in any combination.

main + #261 is a fast-forward, which is worth stating plainly: the merge result is
e0d32ed3, so the green CI on that SHA is not an approximation of the post-merge state —
it is the post-merge state.

No merge order is required. #261 is safe to land in any position relative to the other
four. The reason is structural, and the builds confirm it: #261 touches no file that any
existing project compiles
— no .cs, .axaml, .csproj or MSBuild-visible property —
so it cannot invalidate the other four's greens in either direction. The one new coupling,
the HeadlessBench compile, is green against all four individually and together.

Two things for whoever lands second, neither of them #261's doing:

5. Two disclosure defects — filed on #260, not pushed

The 7-vs-16 row gap is disclosed accurately and I did not try to reconstruct the missing
rows. grep for all ten strings (DEV-TILE, CH-TILE, DEV-NAME, DEV-RATE, CH-DIO,
CH-PWM, SD-LIST, DEV-NET, DEV-DEBUG, DAQIFI_RESTORE_NAME) returns 0 — they
genuinely are not in the source. Deferring the five conceded Qodo findings out of a rescue
was the right discipline. But the deferral chain did not close:

  1. The README's pointer was false. It says the five gaps are "Tracked in HeadlessBench: the 9 extra matrix rows and DAQIFI_RESTORE_NAME did not survive the rescue, and --scripted is still a stub #260". HeadlessBench: the 9 extra matrix rows and DAQIFI_RESTORE_NAME did not survive the rescue, and --scripted is still a stub #260
    carried one of them (--scripted). The other four — including the strongest,
    CONN-DISC — were documented in the README and nowhere else. I have recorded all four
    on HeadlessBench: the 9 extra matrix rows and DAQIFI_RESTORE_NAME did not survive the rescue, and --scripted is still a stub #260, so the pointer is now true. I verified the CONN-DISC entry against the source
    myself: DisconnectDevice unsubscribes active channels, calls
    ConnectionManager.Disconnect, calls RemoveFirmwareNotification, and clears
    SelectedDevice — the rig does only the second of the four. The README's description of
    that is exact.
  2. The README contradicts itself on that same gap. Line ~21, under "It goes through the
    user's code path, not a convenient one", says disconnection is
    ConnectionManager.Instance.Disconnect(device), "which is what the Devices pane calls" —
    the opposite of the Known gaps entry 60 lines below. The same wording is in Program.cs
    at the CONN-DISC block. A reader who stops at the selling-point section gets exactly
    the false confidence the Known gaps section exists to prevent.

Defect 2 needs a commit, and I have deliberately not pushed it: this PR has the largest
blast radius of anything open, its review is settled and its CI is green on the current head,
and a one-paragraph rewording is not worth resetting either. Filed on #260 instead.

Not verified here

The Android head does not build on this Mac (workload absent) — CI covers it and is green
on e0d32ed3
. I did not re-run the bench rig; the builder's non-destructive run stands,
and its one [FAIL] is the known fw-3.7.2 timebase discrepancy, correctly not filed.

Every other number in the body re-derived against the final diff and checks out: +1449/−7
across 8 files, 413 rescued lines (380 + 33), +26/−7 of integration, 530/530, 191 warnings,
6 bugs → 0 (6 Qodo threads, all resolved), 6/7 bench rows.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review at e0d32ed3c1cb6cc66b7ccc355cdd19b69d3fd681 (settled re-check on both surfaces: 6 qodo threads, 0 unresolved, Bugs (0); all six real CI jobs green on that exact SHA). No merge order is required — #261 is safe in any position relative to #255, #256, #258 and #259: main+#261 is a fast-forward, all four pairings and the five-way merge build clean with the new HeadlessBench gate and pass 578/578, and the only MSBuild-visible change in the PR is inside an XML comment, so it cannot invalidate the other four`s greens in either direction.

@qodo-code-review

Copy link
Copy Markdown

Qodo-clean, CI green — ready for review at e0d32ed3c1cb6cc66b7ccc355cdd19b69d3fd681 (settled re-check on both surfaces: 6 qodo threads, 0 unresolved, Bugs (0); all six real CI jobs green on that exact SHA). No merge order is required — #261 is safe in any position relative to #255, #256, #258 and #259: main+#261 is a fast-forward, all four pairings and the five-way merge build clean with the new HeadlessBench gate and pass 578/578, and the only MSBuild-visible change in the PR is inside an XML comment, so it cannot invalidate the other four`s greens in either direction.

The PR is clean from Qodo’s perspective: all six findings are resolved, with zero active findings. Based on the reported CI results for e0d32ed3c1cb6cc66b7ccc355cdd19b69d3fd681, it is ready for review; no merge ordering is required.

@tylerkron
tylerkron added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit be12078 Sep 5, 2026
7 checks passed
@tylerkron
tylerkron deleted the builder/issue-254-headlessbench branch September 5, 2026 23:53
tylerkron added a commit that referenced this pull request Sep 6, 2026
…-RATE

CONN-DISC now drives DaqifiViewModel.DisconnectDeviceCommand rather than
ConnectionManager.Instance.Disconnect, and asserts the unsubscribe and the
SelectedDevice clearing that command also does. LOG-SESSION waits for the
session's persisted SampleCount. RunHardwareSequence cleans up on the
exception path. Setup runs inside the guard and truncates results.jsonl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The HeadlessBench system-test rig is untracked — a git clean would destroy it

1 participant