docs(p0): SysRS/SysDes/SRS/SAD/SDD/Verification + traceability for Android P0 reconciliation

Full P0 Android documentation chain:

- SysRS: API 24 → API 28 reconciliation per DEC-004 (SysRS-288);
  add SysRS-305 (Android in-call audio mode), SysRS-306
  (RECORD_AUDIO runtime timing).
- SysDes: SysDes-152 (in-call audio mode subsystem), SysDes-153
  (RECORD_AUDIO permission flow), SysDes-154 (Android voice audio
  backend), SysDes-155 (macOS runtime baseline).
- SRS: SRS-187 → API 28; add SRS-208 (in-call audio mode), SRS-209
  (RECORD_AUDIO + listen-only fallback), SRS-210..215 (Android voice
  audio backend latency/preset/AEC/usage/sharing/foreground service);
  retarget SysDes anchors from generic SysDes-135 to SysDes-152/153/154.
- SAD: SAD-063 refreshed (API 28); add SAD-084 (audio mode
  controller), SAD-085 (permission adapter with listen-only),
  SAD-086 (foreground service), SAD-087 (macOS runtime baseline);
  formalize cross-cutting + platform-specific allocation pattern
  in §24.1.
- SDD: expand SDD-028 (BackIntentService); refresh SDD-073 (build
  config); add SDD-105 (JNI bootstrap), SDD-106 (permission
  requester), SDD-107 (foreground service), SDD-108 (audio mode
  controller), SDD-109 (AAB pipeline), SDD-110 (PTT capability),
  SDD-111..116 (Android voice audio backend), SDD-118 (Android
  bridge build automation), SDD-119 (iOS/macOS bridge build
  automation back-fill).
- Verification: create android-p0-acceptance.md TC-1..TC-18; add
  SWE4-UV-040..052, SWE5-IV-016..026, SWE6-SV-018..030,
  SYS4-SIV-015 strengthened + SYS4-SIV-017/018.
- Governance: traceability matrix v0.9.9 with end-to-end chain
  closure; DEC-032 documents the temporary abiFilters reduction to
  arm64-v8a only and its restore-by gate.

Trace: full chain SysRS → SysDes → SRS → SAD → SDD → Code → Verification.
This commit is contained in:
EdisonJwa
2026-05-18 12:48:28 +08:00
parent 4c19410556
commit 5aa51c310f
13 changed files with 2487 additions and 17 deletions
@@ -3,7 +3,7 @@
**Document type:** Software Unit Verification Plan
**Process alignment:** ASPICE SWE.4 Software Unit Verification
**Version:** 0.9.2
**Version:** 0.9.14
**Status:** Baseline Candidate
**Language:** English
**Product:** Chanora
@@ -416,3 +416,212 @@ Unit verification results shall be summarized in a unit verification summary rep
| Version | Date | Description |
|---|---|---|
| 0.9.3 | 2026-05-15 | Added desktop PTT unit-verification items SWE4-UV-035 through SWE4-UV-039: `DesktopPttBackend` trait + `PttCapabilityLevel`, platform backends, `PttController` + `AudioTransmitGate`, `PttSanitizer` + `PttCapabilityBadge`, `MissedKeyUpWatchdog`. |
## 11. Android P0 Unit Verification Addendum
**SWE4-UV-040**: Verify Android build configuration unit (`SDD-073` expanded).
- Source SDD: SDD-073
- Verification method: Gradle task assertion + static inspection of `android/app/build.gradle.kts`
- Pass criteria: `minSdk` literal equals `28` (DEC-004); `ndk.abiFilters` set equals `{arm64-v8a, armeabi-v7a, x86_64}` (per SDD-073 item 4 and SDD-118 item 3, `x86` 32-bit is explicitly out of scope and shall not be in the set); release `signingConfig` references the CI signing key reference (not debug); R8/ProGuard `minifyEnabled = true` on release; `bundle { abi.enableSplit = true }`; assemble target produces an AAB artifact at `app/build/outputs/bundle/release/app-release.aab`.
- Evidence: Gradle log + manifest dump
**SWE4-UV-041**: Verify `AndroidPermissionRequester` state machine (`SDD-106`).
- Source SDD: SDD-106
- Source SRS: SRS-209
- Verification method: Dart/Kotlin unit test with a permission-platform test double
- Pass criteria: State transitions are exhaustive for `{granted, denied, permanently_denied, revoked_mid_session}`; transitioning out of `granted` clamps `AudioTransmitGate::set(false)`; re-entering `granted` does NOT auto-transmit (requires explicit user gesture); permanently-denied surfaces a settings-deep-link intent rather than re-prompting.
- Evidence: Test result
**SWE4-UV-042**: Verify `BackIntentService` Android branches (`SDD-028` expanded).
- Source SDD: SDD-028
- Source SRS: SRS-163
- Verification method: Dart unit test with platform-channel test double for both `OnBackInvokedCallback` (API 33+) and `OnBackPressedDispatcher` (API ≤ 32) paths.
- Pass criteria: Modal-first rule — if a modal is open, back closes the modal and does not pop the route. PTT-active-ignore — while `transmit_active == true` the back event is consumed without effect. Route-pop ordering — otherwise the topmost non-modal route pops. The API 33+ branch registers a `OnBackInvokedCallback` at `PRIORITY_DEFAULT`; the pre-33 branch registers an `OnBackPressedCallback`. Switching API level in the test double switches the active branch deterministically.
- Evidence: Test result
**SWE4-UV-043**: Verify `AndroidJniBootstrap` library load and init ordering (`SDD-105`).
- Source SDD: SDD-105
- Verification method: Instrumented unit test on Android (or Robolectric where applicable)
- Pass criteria: `System.loadLibrary("chanora_bridge")` is invoked from the `Application` subclass `onCreate` (not from an Activity); `JNI_OnLoad` runs exactly once per process and captures the `JavaVM*`; bridge init occurs before any FFI call from Dart; double-init is a no-op.
- Evidence: Test result + logcat trace
**SWE4-UV-044**: Verify `AndroidVoiceForegroundService` lifecycle unit (`SDD-107`).
- Source SDD: SDD-107
- Source SRS: SRS-111
- Verification method: Kotlin unit test + Robolectric `ServiceController`
- Pass criteria: `startForeground` is called within 5 s of `onStartCommand` (API 26+ requirement); notification channel exists with `IMPORTANCE_LOW`; `foregroundServiceType` includes `microphone` on API 30+; service stops cleanly on disconnect intent and does not leak its wakelock; restart policy is `START_NOT_STICKY` (no auto-respawn after force-stop).
- Evidence: Test result
**SWE4-UV-045**: Verify `AndroidAudioModeController` snapshot/restore + refcount (`SDD-108`).
- Source SDD: SDD-108
- Source SRS: SRS-208
- Source SysRS: SysRS-305
- Verification method: Kotlin unit test with `AudioManager` test double
- Pass criteria: First acquire snapshots prior `AudioManager.getMode()` and sets `MODE_IN_COMMUNICATION`; nested acquires increment refcount without re-snapshotting; matched release decrements; final release restores the snapshotted prior mode exactly once; when `AudioManager` is unavailable (e.g. injected null), controller logs a sanitized warning and degrades to no-op without throwing.
- Evidence: Test result
**SWE4-UV-046**: Verify `AndroidPttCapability` reports `L0Focused` and pins `FocusedPttBackend` (`SDD-110`).
- Source SDD: SDD-110
- Verification method: Rust + Dart unit test
- Pass criteria: On `target_os = "android"` the `DesktopPttBackend` factory returns `FocusedPttBackend` unconditionally (no ladder evaluation); `PttCapabilityBadge` renders the localized `L0Focused` label with the Android-specific explanation string; no Android branch attempts to bind global hotkeys.
- Evidence: Test result + UI screenshot
| SDD Range | SWE.4 Coverage |
|---|---|
| SDD-028 (expanded) | SWE4-UV-042 |
| SDD-073 (refreshed) | SWE4-UV-040 |
| SDD-105 | SWE4-UV-043 |
| SDD-106 | SWE4-UV-041 |
| SDD-107 | SWE4-UV-044 |
| SDD-108 | SWE4-UV-045 |
| SDD-110 | SWE4-UV-046 |
### Android voice audio backend unit-verification items (SDD-111..SDD-116 wave)
**SWE4-UV-047**: Verify `AndroidVoiceStreamConfig` builder records requested vs. achieved fields.
- Source SDD: SDD-112 (config struct), SDD-111 (trait surface)
- Source SRS: SRS-210, SRS-211, SRS-213, SRS-214
- Source SAD: SAD-077, SAD-081
- Verification method: Rust unit test with a fake Oboe builder return path (no device)
- Pass criteria: Given a `MobileVoiceStreamConfig` requesting `PERFORMANCE_MODE_LOW_LATENCY`, input preset `VoiceCommunication`, output usage `VoiceCommunication`, content type `Speech`, and sharing mode `Exclusive`, the resulting `AndroidVoiceStreamConfig` records the requested values verbatim AND exposes `achieved_*` accessors populated from the fake builder's grant values. Mismatch between requested and achieved is observable via the accessors with no panics. Both input and output stream configs are independently recorded.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: none directly (unit-level; matrix consumes the accessors per-device)
**SWE4-UV-048**: Verify input preset fallback ladder is observable at each step.
- Source SDD: SDD-111 (trait surface, open() error pathway), SDD-112 (preset field)
- Source SRS: SRS-211
- Source SAD: SAD-077
- Verification method: Rust unit test with a scripted Oboe builder mock that returns `ErrorIllegalArgument` for successive presets
- Pass criteria: With the mock denying `VoiceCommunication`, `open()` retries with `VoicePerformance`; with the mock denying both, `open()` retries with `Generic` (`Unprocessed`/default). Each retry is recorded in a structured field (e.g. `attempted_presets: Vec<InputPreset>`) and the final `achieved_input_preset()` returns the preset that was granted. If all presets fail, `open()` returns `BackendError::InputPresetUnavailable` (or the SDD-111 equivalent). No silent fallback.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: none directly (matrix records `achieved_input_preset` per device)
**SWE4-UV-049**: Verify sharing-mode fallback Exclusive→Shared is observable.
- Source SDD: SDD-111, SDD-112 (sharing mode field)
- Source SRS: SRS-214
- Source SAD: SAD-077
- Verification method: Rust unit test with a fake Oboe builder return path
- Pass criteria: When the fake builder accepts `SharingMode::Exclusive`, `achieved_sharing_mode()` returns `Exclusive` and no retry occurs. When the fake builder rejects `Exclusive` (returns the documented Oboe error path for exclusive denial), the unit retries with `Shared`, and `achieved_sharing_mode()` returns `Shared`. The original request (`Exclusive`) is still recorded for diagnostics.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: none directly (matrix records `achieved_sharing_mode` per device)
**SWE4-UV-050**: Verify hardware effects per-effect success/failure logic with software-AEC fallback.
- Source SDD: SDD-113 (per-effect construction + software fallback signal)
- Source SRS: SRS-212
- Source SAD: SAD-077, SAD-081
- Verification method: Rust unit test with a mocked JNI surface (`new_object`-style indirection) representing `AcousticEchoCanceler.create`, `NoiseSuppressor.create`, `AutomaticGainControl.create`
- Pass criteria: For each of AEC, NS, AGC: (a) success path — mock returns a non-null `GlobalRef`, effect is recorded as engaged, `setEnabled(true)` is invoked once; (b) failure path — mock returns null OR throws, the unit records the per-effect failure, does NOT abort the other two, and signals the engine to engage the software equivalent (per SDD-113 item 5 — for AEC failure the engine's software AEC must be flagged on). Order of effect construction does not affect outcome (no cross-effect coupling). `release()` is called on every engaged `GlobalRef` exactly once when the unit is torn down.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: per-device AEC/NS/AGC availability column
**SWE4-UV-051**: Verify performance-mode achieved recording feeds SRS-210 latency tier classification.
- Source SDD: SDD-111 (`achieved_performance_mode`), SDD-112 (mode field)
- Source SRS: SRS-210
- Source SAD: SAD-077
- Verification method: Rust unit test
- Pass criteria: When the fake builder grants `PERFORMANCE_MODE_LOW_LATENCY`, `achieved_performance_mode()` returns `LowLatency` and the SRS-210 tier classifier (the unit-level pure function that maps achieved mode → latency target) returns the 150 ms tier. When the fake builder grants `PERFORMANCE_MODE_NONE` or `POWER_SAVING`, the classifier returns the 250 ms tier. The classifier is a pure function reachable from the engine without a device.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: per-device `achieved_performance_mode` column
**SWE4-UV-052**: Verify output stream usage and content type are set correctly.
- Source SDD: SDD-112 (output config fields)
- Source SRS: SRS-213
- Source SAD: SAD-077
- Verification method: Rust unit test inspecting the builder calls captured by a mock Oboe builder
- Pass criteria: The output stream-construction path invokes `setUsage(VoiceCommunication)` and `setContentType(Speech)` exactly once on the builder before `openStream`. No other usage / content-type combination is reachable from the production path (the test asserts there is no code path that constructs the output stream with `Media` / `Music` defaults). `achieved_output_usage()` returns `VoiceCommunication` and `achieved_output_content_type()` returns `Speech` on a successful open.
- Evidence: Rust unit test result
- SDD-116 matrix row satisfied: per-device `achieved_output_usage` / `achieved_output_content_type` column
| SDD Range | SWE.4 Coverage |
|---|---|
| SDD-111 | SWE4-UV-047, SWE4-UV-048, SWE4-UV-049, SWE4-UV-051 |
| SDD-112 | SWE4-UV-047, SWE4-UV-048, SWE4-UV-049, SWE4-UV-051, SWE4-UV-052 |
| SDD-113 | SWE4-UV-050 |
| SDD-115 | (lifecycle sequencing — see SWE.5 integration items) |
| SDD-116 | (device matrix — see SWE.6 + android-p0-acceptance TC-14..TC-18) |
## Baseline Candidate 0.9.4 Update
| Version | Date | Description |
|---|---|---|
| 0.9.4 | 2026-05-18 | Added Android P0 unit-verification items SWE4-UV-040 through SWE4-UV-046 covering SDD-028 (expanded back-intent branches), SDD-073 (refreshed build config + AAB), SDD-105 (JNI bootstrap), SDD-106 (permission state machine + transmit clamp), SDD-107 (foreground service lifecycle), SDD-108 (audio mode controller refcount + restore), SDD-110 (PTT capability pin). |
| 0.9.5 | 2026-05-18 | Added Android voice audio backend unit-verification items SWE4-UV-047 through SWE4-UV-052 covering SDD-111 (trait surface), SDD-112 (config requested vs achieved), SDD-113 (per-effect hardware engagement + software fallback) and feeding SRS-210/211/213/214 unit-level classification. |
| 0.9.14 | 2026-05-18 | Wave 3 reviewer follow-up: allocated real SWE.4 IDs for two test sets that had been cited under provisional `SWE4-UV-NNN` placeholders. SWE4-UV-053..055 anchor the three `TransmitModeSelector` permission-state clamp tests in `crates/chanora_audio/src/transmit_selector.rs` (commit 635d160 was cited under the "SWE4-UV-026 family" without real anchors). SWE4-UV-056..057 anchor the two missing `DiagnosticExport::with_android_audio` rendering tests in `crates/chanora_diagnostics/src/lib.rs` (commit aea50a1 introduced the field/builder/`to_text` branch but the unit-test pair was queued for the commit-8 amend). Closes the Wave 3 traceability gap; downstream test cites in commits 4 and 8 are amended to reference these IDs. |
## 12. Android Permission Clamp + Diagnostics Android-Audio Unit Verification Addendum (Wave 3 — v0.9.14)
**Background.** Wave 3 reviewer audit flagged two newly-landed test groups whose source comments cited verification IDs as `SWE4-UV-NNN` placeholders rather than real allocated anchors:
1. Three `TransmitModeSelector` permission-state tests at `crates/chanora_audio/src/transmit_selector.rs:343-435` (introduced by commit 635d160). The reviewer noted at `transmit_selector.rs:343` that these tests cite "SWE4-UV-026 family" without real IDs.
2. Two missing unit tests for the new `chanora_diagnostics` Android-audio surface (`android_audio: Option<String>` field, `with_android_audio` builder, `[audio.android]` `to_text` branch) introduced by commit aea50a1; queued for the commit-8 amend.
This addendum allocates the five required anchors. SWE4-UV-052 was the prior high-water mark; SWE4-UV-053..057 are the next free contiguous range.
**SWE4-UV-053**: Verify `TransmitModeSelector` clamps transmit to `false` when `permission_state == Denied` (or `PermanentlyDenied`/`Unknown`) regardless of PTT, channel, or transmit-mode state.
- Source SDD: SDD-106 §6
- Source SRS: SRS-209 (fail-safe listen-only default)
- Verification method: Rust unit test
- Test code path: `crates/chanora_audio/src/transmit_selector.rs::tests::permission_state_denied_clamps_transmit_to_false` (lines 348-384)
- Pass criteria: With `TransmitMode::Ptt`, `in_channel = true`, `ptt_held = true` and the default `Granted` permission state the gate evaluates `true`. Publishing `PermissionGate::Denied` flips the gate to `false`. `PermanentlyDenied` and `Unknown` likewise clamp `false`. The clamp wins over `TransmitMode::Continuous`.
- Evidence: `cargo test -p chanora_audio` result
- Notes: Establishes the fail-safe semantics: cold-launch `Unknown` is listen-only per SRS-209.
**SWE4-UV-054**: Verify `TransmitModeSelector` releases the permission clamp on transition back to `Granted` and resumes normal PTT-driven evaluation.
- Source SDD: SDD-106 §6
- Source SRS: SRS-209
- Verification method: Rust unit test
- Test code path: `crates/chanora_audio/src/transmit_selector.rs::tests::permission_state_granted_releases_clamp` (lines 389-409)
- Pass criteria: After `Denied` clamps the gate to `false`, transitioning to `PermissionGate::Granted` (with PTT held + in channel) restores the gate to `true` on the next state tick. Releasing the PTT key then drops the gate per normal PTT semantics — the clamp is no longer in effect.
- Evidence: `cargo test -p chanora_audio` result
- Notes: Pairs with SWE4-UV-053; together they cover the clamp on/off edges.
**SWE4-UV-055**: Verify `TransmitModeSelector` ignores `PermissionState` events for non-`RECORD_AUDIO` permissions (contract: bridge filters before reaching the selector).
- Source SDD: SDD-106 §5
- Verification method: Rust unit test (contract pin)
- Test code path: `crates/chanora_audio/src/transmit_selector.rs::tests::permission_state_for_other_permission_does_not_clamp` (lines 418-435)
- Pass criteria: The selector exposes no setter for non-`RECORD_AUDIO` permissions. With `TransmitMode::Continuous` + `in_channel = true` and no call to `set_permission_state`, the selector remains in its `Granted` default and the gate evaluates `true`. Demonstrates the contract that unrelated permission events (e.g. `POST_NOTIFICATIONS`) cannot reach this code path.
- Evidence: `cargo test -p chanora_audio` result
- Notes: This is a negative/contract test; the JNI bridge is responsible for filtering by permission name before invoking `set_permission_state`.
**SWE4-UV-056**: Verify `DiagnosticExport::with_android_audio(Some(yaml))` renders the `[audio.android]` section between `[metadata]` and `[recent logs]` in `to_text()`.
- Source SDD: SDD-116 item 3 (diagnostics matrix surface), SDD-112 item 10 / SDD-113 item 7 (producing crate)
- Verification method: Rust unit test
- Test code path: `crates/chanora_diagnostics/src/lib.rs::tests::android_audio_renders_between_metadata_and_logs` (to be added by commit-8 amend)
- Pass criteria: Building a `DiagnosticExport::from_sink(...)` and chaining `.with_android_audio(Some("perf: low_latency\n".into()))` produces a `to_text()` output where the substring `[audio.android]` appears (a) after the `[metadata]` header, (b) before the `[recent logs]` header, and (c) is immediately followed by the supplied YAML fragment verbatim. The YAML fragment is not re-redacted by the diagnostics bundle (SDD-090 places sanitisation in the producing crate).
- Evidence: `cargo test -p chanora_diagnostics` result
- Notes: Section ordering is part of the SDD-116 matrix contract; downstream tooling parses by header.
**SWE4-UV-057**: Verify `DiagnosticExport` with default `android_audio = None` omits the `[audio.android]` header entirely (negative test).
- Source SDD: SDD-116 item 3 (negative case — non-Android or pre-session)
- Verification method: Rust unit test
- Test code path: `crates/chanora_diagnostics/src/lib.rs::tests::android_audio_absent_omits_section` (to be added by commit-8 amend)
- Pass criteria: A `DiagnosticExport::from_sink(...)` constructed without `.with_android_audio(...)` (i.e. `android_audio == None`) produces a `to_text()` output that does **not** contain the substring `[audio.android]`. The `[metadata]` and `[recent logs]` sections remain present in order. Calling `.with_android_audio(None)` explicitly is equivalent (idempotent omission).
- Evidence: `cargo test -p chanora_diagnostics` result
- Notes: Pairs with SWE4-UV-056; together they cover the conditional-render branch in `DiagnosticExport::to_text`.
| SDD Range | SWE.4 Coverage |
|---|---|
| SDD-106 §5 | SWE4-UV-055 |
| SDD-106 §6 | SWE4-UV-053, SWE4-UV-054 |
| SDD-116 item 3 | SWE4-UV-056, SWE4-UV-057 |