Skip to content

test: pin the frame-loop invariants behind the #35 fixes (tests now run in CI) - #37

Merged
willwade merged 3 commits into
mainfrom
test/frame-loop-invariants
Aug 25, 2026
Merged

willwade merged 3 commits into
mainfrom
test/frame-loop-invariants

Conversation

@willwade

@willwade willwade commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #36: the choppiness diagnosis came from ad-hoc instrumentation. The deterministic parts of what that instrumentation revealed are now pinned as tests so the same regressions can't silently return; the genuinely timing-based parts (RAF cadence, jitter) stay manual — CI runners have no real compositor and timing assertions would flake.

Invariant from the #35 investigation Test
text-size callback returns 0 on success (v0.1.17 inverted this; engine discarded every measurement, ~2,500 re-measures/sec) TextMeasurerTests.Measure_success_returns_zero_per_dasher_h_contract
repeated measurements hit the frontend cache (keyed text+font+size; Invalidate on font change) TextMeasurerTests (cache suite)
engine timeline is clamped — pause gaps/scheduler hiccups never arrive as multi-second deltas (engine consumes them as zoom amount) EngineTimelineTests.StepFor_*
engine-side cache actually warms (callback stops firing in steady state) EngineCApiTests.Text_size_callback_hits_engine_cache_in_steady_state — integration vs the real dasher.dll, catches an inverted interpretation on either side of the ABI
permitted-values probe returns full count before fetch (DasherCore #58; settings dropdowns rely on it) EngineCApiTests.Permitted_values_probe_returns_full_count_before_fetch

Implementation notes:

  • TextMeasurer and EngineTimeline extracted from DasherCanvas (behaviour unchanged; the measurement backend is injectable so unit tests need no font manager — Avalonia 12's headless API churn made that the robust route).
  • Integration tests use NativeBridge directly with the real dasher.dll + DasherCore/Data; they pass vacuously if the artifacts are absent (local dev without the DLL) and bite in CI.
  • Build Installer workflow now runs dotnet test after building dasher.dll, before publish — so every PR and release build runs all 59 tests including the engine integrations.

Type of change

  • Bug fix
  • New feature
  • Cross-platform / parity change
  • Refactor / tooling / docs

Cross-platform impact

  • This changes a capability that users see on other platforms.
  • This introduces a new UX or hardware interaction.

Definition of Done

  • CI is green (tests now run in CI as part of this PR)
  • Tests added for new behaviour (this PR is the tests)
  • Feature matrix updated (n/a)
  • Docs updated (commit message documents the manual-vs-CI split)
  • Commits are signed off (DCO)

Greptile Summary

The PR extracts frame-timeline and text-measurement behavior into testable components and adds native-engine integration coverage to CI.

  • Adds deterministic tests for timeline clamping, text-measurement return semantics, and caching.
  • Exercises permitted-value probing and engine-side text-size caching against the native library.
  • Requires native-engine availability during the installer workflow’s test step.
  • Correctly distinguishes optional missing local artifacts from CI initialization and library-loading failures.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
tests/Dasher.Windows.Tests/EngineCApiTests.cs Adds native integration tests and now makes initialization or native-library loading failures fail CI instead of passing vacuously.
.github/workflows/build-installer.yml Runs the test project after building the native library and requires the engine artifacts during that CI step.
src/Dasher.Windows/Engine/TextMeasurer.cs Extracts cached text measurement behind an injectable backend while retaining the native callback’s zero-on-success contract.
src/Dasher.Windows/Engine/EngineTimeline.cs Extracts the bounded frame-step calculation for deterministic unit testing.
src/Dasher.Windows/Controls/DasherCanvas.cs Delegates text measurement and timeline clamping to the extracted components without changing their intended behavior.

Reviews (3): Last reviewed commit: "test: require engine in CI; bump DasherC..." | Re-trigger Greptile

The #35 diagnosis came from ad-hoc instrumentation; the parts of it
that are deterministic are now CI-tested so the same regressions cannot
silently return:

- TextMeasurer (extracted from DasherCanvas): the dasher.h return-code
  contract (0 = success, non-zero = fall back to the engine estimate) —
  v0.1.17 inverted this and the engine discarded every measurement while
  re-calling ~21x per frame; plus cache semantics (repeat hits, keyed by
  text+font+size, Invalidate on font change). Measurement backend is
  injectable so the tests need no font manager
- EngineTimeline (extracted from StepFrame): the engine-time clamp —
  pause gaps and scheduler hiccups must never reach the engine as huge
  time deltas (it consumes them as zoom amount); degenerate/negative
  deltas step the minimum
- EngineCApiTests (integration, real dasher.dll + DasherCore/Data):
  the permitted-values probe returns the full count before fetch
  (DasherCore #58, which the settings dropdowns rely on), and the
  text-size callback stops firing once the engine cache is warm (the
  regression shape of #33/#36). These skip when the engine artifacts
  are absent locally; CI builds the DLL first
- Build Installer workflow now runs dotnet test after building
  dasher.dll, before publish

Frame pacing itself (RAF cadence, jitter) stays manual verification:
CI runners have no real compositor and timing assertions would flake.

Signed-off-by: will wade <willwade@gmail.com>
Comment thread tests/Dasher.Windows.Tests/EngineCApiTests.cs
…ping

TryCreateEngine now distinguishes missing artifacts (legit local skip)
from dasher_create returning a null handle with artifacts present — the
latter must fail, or an ABI/init regression would pass undetected
(caught in review of #37).

Signed-off-by: will wade <willwade@gmail.com>
return EngineAvailability.Ready;
}
catch (DllNotFoundException) { return EngineAvailability.ArtifactsMissing; }
catch (BadImageFormatException) { return EngineAvailability.ArtifactsMissing; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Invalid native libraries pass CI

When CI supplies a corrupt, incompatible, or wrong-architecture dasher.dll, BadImageFormatException is classified as ArtifactsMissing, causing both native integration tests to return without assertions and pass despite the unusable native build.

Suggested change
catch (BadImageFormatException) { return EngineAvailability.ArtifactsMissing; }

Writing the engine integration tests surfaced a real DasherCore bug:
dasher_create threw regex_error whenever the settings file did not exist
yet and the user-dir path contained a backslash escape MSVC's std::regex
rejects (digit-leading components parse as backreferences, 'c'-leading
as invalid control escapes) - ~70% of fresh GUID temp dirs. The Windows
tests were failing non-deterministically on exactly that dice roll.
Fixed upstream in DasherCore #59 (ScanFiles never compiles an absolute
path as a regex); submodule bumped to v0.2.6.

Also makes the CI engine tests impossible to skip silently: the Run
tests step sets DASHER_TESTS_REQUIRE_ENGINE=1 (with the freshly built
DLL on PATH), so missing artifacts or an init failure fails the build -
previously the integration tests passed vacuously on CI (105 ms total)
because dasher.dll/Data were not resolvable in the test host, so CI
green said nothing about the ABI. Local runs without artifacts still
skip.

Signed-off-by: will wade <willwade@gmail.com>
@willwade
willwade merged commit 5e06fe6 into main Aug 25, 2026
3 checks passed
@willwade
willwade deleted the test/frame-loop-invariants branch August 25, 2026 20:07
willwade added a commit that referenced this pull request Sep 8, 2026
…oard bridge (RFC 0015) (#52)

## Summary

Implements the frontend half of the RFC 0015 context-awareness amendment
for Windows (governance PR #37; research in #50; engine CAPI in
DasherCore PR #83 — submodule pinned to that branch pending merge/tag).

Dasher 5's "direct mode knows the context" was really a session shadow
buffer — it never read the target field. This PR restores that parity
and exceeds it: predictions now continue from **text already in the
target field**, which v5 never could.

### Context seeding (RFC 0015 tiers 1–3)

- **`TargetContextReader`** (`Services/TargetContextReader.cs`): reads
the focused control's text + caret via **UI Automation `TextPattern`**
(modern apps: WPF, UWP, browsers, Office, Electron) with a
**`WM_GETTEXT`/`EM_GETSEL` fallback** for classic EDIT/RichEdit controls
(Notepad, dialogs). All reads run on a background thread with a **300 ms
hard budget** — an unresponsive provider can never stall a mode switch;
failures return `null`.
- **Seeding triggers**:
- *Keyboard-mode entry* — reads the field you were typing in, so
predictions continue mid-sentence
- *Target-window change* — switching fields **re-reads and re-seeds**
the new field (better than v5, which reset to empty); read failure
degrades to empty seed (v5 parity, never a dead mode)
- **Caret conversion**: UIA reports UTF-16 code units; the engine wants
UTF-8 bytes — converted with the new `dasher_byte_offset_from_utf16`
CAPI (the shared, tested converter from DasherCore #83), so CJK/accented
text anchors correctly
- 150 ms debounce for focus churn; `KbLog` traces each seed

### Clipboard bridge (mini-bar)

Four new buttons on the keyboard-mode mini-bar:
- **Copy** — engine buffer → system clipboard
- **Cut / Paste / Select-All** — `Ctrl+X/V/A` chords injected into the
target via `SendInput`; they act on the *target's* selection, which only
the target can do — the same reason v5 implemented them frontend-side
rather than in control mode

## Verification
- Build green, 77/77 tests
- Needs a manual pass: type into Notepad (fallback path), a browser
textarea (UIA path), switch between two apps (re-seed), non-ASCII text
(conversion), the four mini-bar buttons

## Depends on
- DasherCore PR #83 (`dasher_seed_buffer`, `dasher_set_offset`, unit
converters) — submodule pinned to `feat/context-capi`; **re-pin to the
tagged release before merge**

## Type of change
- [x] New feature (cross-platform parity + capability v5 lacked)

## Definition of Done
- [x] Build + tests green
- [x] Commits signed off (DCO)






































<!-- greptile_comment -->

<h3>Greptile Summary</h3>

The PR adds target-field context seeding and clipboard controls for
direct keyboard mode, with centralized target-window tracking and
ownership checks.
- Reads focused-field text and caret positions through UI Automation
with a Win32 edit-control fallback.
- Re-seeds predictions on foreground and same-window focus changes.
- Adds Copy, Cut, Paste, and Select-All controls to the keyboard
mini-bar.
- Adds target-window identity and UI regression tests.

<h3>Confidence Score: 5/5</h3>

The PR appears safe to merge.

No blocking failure remains.

<details><summary><h3>Important Files Changed</h3></summary>




| Filename | Overview |
|----------|----------|
| src/Dasher.Windows/Services/TargetContextReader.cs | Adds bounded
target-context reads, ownership validation, and layered caret discovery;
the latest rcCaret fix resolves the previously reported cross-thread
caret-coordinate defect. |
| src/Dasher.Windows/Services/KeyboardTargetTracker.cs | Centralizes
rooted target tracking, focus and foreground hooks, stale-event
handling, and foreground restoration. |
| src/Dasher.Windows/Services/TargetWindowIdentity.cs | Defines and
tests a shared GA_ROOT-based target ownership predicate. |
| src/Dasher.Windows/Views/MainWindow.axaml.cs | Integrates debounced
context seeding, generation invalidation, target tracking, and clipboard
bridge actions. |
| src/Dasher.Windows/Views/MainWindow.axaml | Adds keyboard mini-bar
controls for copying engine text and sending target-side Cut, Paste, and
Select-All commands. |

</details>


<details open><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
  participant W as WinEvent hooks
  participant T as KeyboardTargetTracker
  participant M as MainWindow
  participant R as TargetContextReader
  participant E as DasherCore
  W->>T: Foreground or focus event
  T->>T: Root and validate target HWND
  T->>M: TargetChanged
  M->>M: Debounce and generation check
  M->>T: Verify target foreground
  M->>R: ReadAsync(target HWND)
  R->>R: UIA TextPattern or Win32 fallback
  R-->>M: Text and UTF-16 caret
  M->>E: Convert caret to UTF-8 offset
  M->>E: Seed buffer
```
</details>

<sub>Reviews (19): Last reviewed commit: ["fix(keyboard): caret rect
from
GUITHREAD..."](6726fc3)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=61122413)</sub>

<!-- /greptile_comment -->

---------

Signed-off-by: will wade <willwade@gmail.com>
Co-authored-by: will wade <will wade@gmail.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.

1 participant