Commit Graph
2 Commits
Author SHA1 Message Date
EdisonJwa 21945979a3 test(audio,ptt): comprehensive Windows P0 unit-test suite (L0-L11)
Layered test coverage for the Windows PTT subsystem ahead of the
v1.0.0-rc.8 official release sign-off.

L0 (refactor)
- Extract three pure-logic dispatchers from the existing WndProc /
  LowLevelKeyboardProc / LowLevelMouseProc bodies in
  crates/chanora_audio/src/ptt_backends/windows.rs:
    dispatch_raw_input(ctx, &RAWINPUT)
    dispatch_hook_keyboard(ctx, wparam, &KBDLLHOOKSTRUCT)
    dispatch_hook_mouse(ctx, wparam, &MSLLHOOKSTRUCT)
  Each takes a small Context (AtomicBinding + AudioTransmitGate +
  flags) and is callable without spinning up any Win32 plumbing.
  The real Win32 procs unchanged structurally; they unpack lparam
  and forward to the dispatchers. AtomicBinding / RawInputContext
  / HookContext / resolve_binding are now pub(crate) so the
  in-file test module can drive them.

L1 — windows_keymap full-table sweep (+13 tests)
  Every key_label_to_vk arm, all A-Z + a-z, all 0-9, F1-F20,
  navigation, modifiers, OEM punctuation, numpad. Exhaustive
  mouse_label_to_button cases including the 0x08 / 0x10 /
  unknown-bitmask fallbacks.

L2 — AtomicBinding lock-free correctness
  store/read round-trip, clear(), Default = zeros, single-writer
  / single-reader concurrency, many-readers / single-writer.

L3 — resolve_binding dispatcher tests
  All PttInputClass variants, well-known labels, unknown-label
  fallback, mismatched class+label rejection, mouse bitmask
  resolution.

L4 — Backend state-machine
  Both WindowsRawInputBackend and WindowsHookBackend:
  descriptor() pre-arm vs post-arm (L0Focused -> L2/L3), start()
  with None binding rejection, rebind() in-place, stop()
  clears + idempotent, stop() after stop() no-op.

L5 — dispatch_raw_input table
  Keyboard match/non-match, key-down/key-up via Flags & 0x01,
  no-binding short-circuit, mouse XBUTTON1/XBUTTON2 down/up
  matching the bound button, unhandled HID type. RAWINPUT structs
  built via mem::zeroed plus field-fill, owning the unsafe in
  the test layer where it belongs.

L6 — dispatch_hook_keyboard + dispatch_hook_mouse
  WM_KEYDOWN / WM_KEYUP / WM_SYSKEYDOWN / WM_SYSKEYUP for the
  keyboard path, WM_XBUTTONDOWN / WM_XBUTTONUP for the mouse
  path. Same shape as L5.

L7 — Privacy invariant (crates/chanora_audio/tests/ptt_privacy.rs)
  New cross-platform integration test installs a custom
  tracing_subscriber Layer that records every emitted event's
  target + field names. Exercises the public PTT API plus (on
  Windows) the backend factory. Asserts no field name in the
  banned list (vk, scan_code, keysym, key_label, bound_key,
  binding, platform_key, VKey, wVk, wScan, kbflags, mouseflags)
  is ever emitted and every field belongs to the DEC-027
  allow-list. Adds tracing-subscriber as a dev-dependency on
  chanora_audio.

L8 — Full-chain integration in core/chanora_core/src/ptt.rs
  Windows-only mod windows_full_chain_tests:
    zero-tail full chain (synchronous)
    default-tail full chain (200 ms wait then off)
    mid-press rebind abandons in-flight press

L9/L10/L11 — tools/windows-smoke.cmd + tools/windows-smoke.md
  Batch smoke script + operator doc. cargo build, flutter build,
  artifact existence + size checks, headless launch with stderr
  capture, bridge-initialised log assertion. Distinct exit codes
  per failure step. Doc explains invocation + common failure
  modes.

Verification (Linux)
- cargo check --workspace: clean.
- cargo test --workspace: 78 passed / 0 failed / 3 ignored.
  76 cross-platform unit tests (unchanged) plus the new
  ptt_privacy integration test plus one new ignored portal smoke
  test.

The Windows-gated tests (~49 new) compile and run on the Korean
Windows 11 host where they belong; cross-compile from Linux is
not configured locally. The smoke script is the production
acceptance gate for rc.8 on Windows.

Deviations from the original plan are minor (single ignored
real-runtime test rather than per-platform attribute, L7 uses
public API rather than pub(crate) dispatchers, dispatchers live
inside windows.rs rather than a sibling module) and documented
in the subagent report.
2026-05-16 00:47:10 +08:00
EdisonJwa 82d012a46b feat(ptt): live Linux GNOME-Wayland portal session flow (DEC-025)
Promotes the Linux backend from probe-only to a live
`org.freedesktop.portal.GlobalShortcuts` session, closing the
gen2 v0.9.3 baseline's last Linux-side code item. Both gaps I
flagged on the review pass are addressed:

  * Stop now closes the portal session through the dedicated
    `org.freedesktop.portal.Session` interface (not the
    request-cancel `Request` interface — that would only abort a
    pending Request, not release the bound shortcuts).
  * Ten new unit tests cover `classify_shortcuts_value`,
    `publish_bound`, `publish_l0`, and the `SHORTCUT_ID` stability
    contract using synthesised `OwnedValue` payloads. Live D-Bus
    coverage stays in the `linux_portal_smoke` ignored
    integration test (RR-PTT-004).

Live session lifecycle (gen2 Q5b — lazy, single backend instance):

  1. `start(gate, binding)` spawns one `tokio::spawn` worker that
     owns an async `zbus::Connection` (sharing the bridge's
     tokio runtime per Q4a).
  2. `CreateSession` with fresh random `handle_token` /
     `session_handle_token` tokens. The worker awaits the portal
     `Response` signal via a `RequestProxy` subscription and
     extracts `session_handle` from the results dict.
  3. `BindShortcuts(session_handle, [("chanora-ptt", { description
     = "Chanora push-to-talk" })], "", {})`. The portal opens its
     own system-managed dialog asking the user to choose a key
     — Chanora itself never reads raw key events. The audio
     engine continues at `L0Focused` while the dialog is open;
     the descriptor watch publishes the transition once the
     portal returns.
  4. On `response_code == 0`: classify the `trigger_description`
     substring (heuristic: contains "mouse" -> MouseSideButton,
     else Keyboard), publish `L2GlobalHoldToTalk` (or `L3` for
     mouse) through the watch sender. The raw trigger_description
     string is never logged (DEC-027 / SRS-202).
  5. On `response_code == 1` (cancelled) or `>= 2` (failure):
     publish `L0Focused` through the watch sender. The user can
     retry via the UI "Configure" button (gen2 Q6a).
  6. The worker enters a `tokio::select!` loop multiplexing the
     `cmd_rx` channel (Rebind / Stop) and the `Activated` /
     `Deactivated` signals. Matching signals scoped to this
     session handle and `chanora-ptt` shortcut id drive
     `gate.set(true/false)`.
  7. `Rebind` re-runs `BindShortcuts` on the same session.
  8. `Stop` calls `org.freedesktop.portal.Session.Close()` on
     the session-handle object path, clears the gate, exits.

UX (gen2 Q3a): when `_pttBackendId == 'gnome-wayland-portal'`,
the Flutter "Configure" button skips the in-app
`_PttBindingCaptureDialog` and shows a SnackBar telling the user
their desktop environment will open its own shortcut dialog.
The button delegates to `setPttBinding(keyboard, "portal")`
which nudges the backend; the portal handles the rest. New ARB
key `pttConfigurePortalRedirect` in en + zh-Hans.

Trait surface (cross-cutting):

  * `DesktopPttBackend::descriptor_watch()` is a new trait method
    with a default impl returning a never-firing receiver.
    Backends with async capability transitions (only the Linux
    portal backend today) override it to return the live watch
    sender's receiver.
  * `chanora_core::ChanoraSession::start_audio` subscribes to the
    active backend's `descriptor_watch()` and spawns a forwarder
    task that re-emits `SessionEvent::PttCapability` on every
    transition. The initial value is emitted synchronously.

`Cargo.toml` (Linux-only):

  * `futures-util` (std features, no executor) for stream
    consumption on the portal signal subscriptions.
  * `rand 0.8` for fresh per-process portal tokens.
  * `zbus` continues at v5 with the `tokio` + `blocking-api`
    features.

Tests
-----

  * `chanora_audio` rises from 8 to 18 unit tests. New
    coverage on the Linux module:
      - `classify_returns_none_when_shortcut_id_missing`
      - `classify_returns_keyboard_for_typical_trigger_description`
      - `classify_returns_keyboard_when_trigger_description_missing`
      - `classify_detects_mouse_substring`
      - `classify_is_case_insensitive_on_mouse_substring`
      - `publish_bound_keyboard_publishes_L2_with_keyboard_class`
      - `publish_bound_mouse_publishes_L3`
      - `publish_bound_none_publishes_L2_keyboard_default`
      - `publish_l0_clears_descriptor`
      - `shortcut_id_is_stable`
  * Workspace total: 67 unit + integration tests, all green with
    `CHANORA_DISABLE_KEYRING=1` (was 57 at v1.0.0-rc.4).
  * New `crates/chanora_audio/tests/linux_portal_smoke.rs`
    ignored integration test (RR-PTT-004 evidence path). Run on
    a GNOME-on-Wayland host with
    `cargo test -p chanora_audio --test linux_portal_smoke -- --ignored --nocapture`.

Documentation
-------------

  * `docs/architecture/desktop-ptt-architecture.md` §5.3 rewritten
    to describe the realised lifecycle; v0.9.4 change-history
    entry added.
  * `docs/governance/product-decision-register.md` v0.9.10
    change-history entry recording the code-side promotion. No
    decision rows mutate.
  * `docs/release/release-readiness-go-nogo-record.md` RR-PTT-004
    flipped from `Open` to `Implemented (live trace pending)`;
    v0.9.5 change-history entry.

Verification
------------

  * `cargo test --workspace`: 67/67 green.
  * `cargo deny check`: advisories ok, bans ok, licenses ok,
    sources ok.
  * `cargo about generate --offline`: zero new warnings.
  * `tools/dump_flutter_licenses.sh`: 94 packages, 0 without
    LICENSE.
  * `flutter analyze`: clean.
  * `cargo build -p chanora_bridge --release` +
    `flutter build linux --release`: clean Linux x86_64 bundle.
  * Live portal trace (RR-PTT-004) — **not run**. The dev shell
    is a TTY without a Wayland session. The user will run the
    ignored smoke test from inside a GNOME-on-Wayland session
    when available.

No Windows / macOS / iOS live verification in this commit (hosts
unavailable). The Windows + macOS backend scaffolds remain in
place reporting their target capability honestly; live OS-call
wiring is queued for their respective platform owners'
reference hosts per `docs/governance/staged-release-plan.md`.
2026-05-15 16:43:45 +08:00