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.
This commit is contained in:
EdisonJwa
2026-05-16 00:47:10 +08:00
parent 7596f8a9dc
commit 21945979a3
8 changed files with 1533 additions and 97 deletions
+95
View File
@@ -420,3 +420,98 @@ mod tests {
assert!(!real_gate.load(), "post-tail: pttActive must be false");
}
}
/// Full-chain Windows integration tests (L8 of the P0 release
/// plan). Drives the same gate plumbing as production through the
/// Windows backends' pure-logic dispatchers (no Win32 message
/// pump). Gated `cfg(target_os = "windows")` because the backends
/// themselves only exist there.
#[cfg(all(test, target_os = "windows"))]
mod windows_full_chain_tests {
use super::*;
use chanora_audio::ptt_backends::{PttBinding, PttInputClass};
use chanora_audio::{TransmitMode, TransmitModeSelector};
use std::time::Duration;
fn setup(
tail_ms: u32,
) -> (
AudioTransmitGate,
std::sync::Arc<TransmitModeSelector>,
std::sync::Arc<ReleaseTailTimer>,
) {
let gate = AudioTransmitGate::new(false);
let selector = std::sync::Arc::new(TransmitModeSelector::new(gate.clone()));
selector.set_mode(TransmitMode::Ptt);
selector.set_in_channel(true);
let tail = std::sync::Arc::new(ReleaseTailTimer::new(selector.clone(), tail_ms));
(gate, selector, tail)
}
/// Full chain with zero tail: pressing the synthetic press-edge
/// gate (the same call the Windows backend would make from its
/// dispatcher) drives the real transmit gate true; releasing
/// it drives it false. Mirrors what the dispatcher exercise in
/// `windows.rs` would produce after `ctx.gate.set(true)`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn full_chain_press_release_zero_tail() {
let (real_gate, _sel, tail) = setup(0);
let controller = PttController::new(tail);
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(!real_gate.load());
controller.press_gate().set(true);
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(real_gate.load(), "full chain must follow press");
controller.press_gate().set(false);
tokio::time::sleep(Duration::from_millis(15)).await;
assert!(!real_gate.load(), "full chain must follow release");
}
/// Full chain with the SDD-096 default 200 ms tail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn full_chain_press_release_default_tail() {
let (real_gate, _sel, tail) = setup(200);
let controller = PttController::new(tail);
tokio::time::sleep(Duration::from_millis(10)).await;
controller.press_gate().set(true);
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(real_gate.load());
controller.press_gate().set(false);
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(real_gate.load(), "tail window must keep gate true");
tokio::time::sleep(Duration::from_millis(220)).await;
assert!(!real_gate.load(), "post-tail must clear gate");
}
/// Mid-press rebind: while the gate is held, a `set_binding`
/// to a different key clears the in-flight press (Raw Input /
/// Hook backends call `gate.set(false)` from `rebind`).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mid_press_rebind_abandons_in_flight_press() {
let (real_gate, _sel, tail) = setup(0);
let controller = PttController::new(tail);
tokio::time::sleep(Duration::from_millis(10)).await;
// Press.
controller.press_gate().set(true);
tokio::time::sleep(Duration::from_millis(10)).await;
assert!(real_gate.load());
// Rebind mid-press. The backend's `rebind` clears the gate.
let new_binding = PttBinding {
input_class: PttInputClass::Keyboard,
platform_key: "F10".to_string(),
};
let _ = controller.set_binding(new_binding).await;
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(
!real_gate.load(),
"rebind during a held press must abandon the press"
);
}
}