test+diag: SWE.4 unit tests for Rust paths, Dart service tests, diagnostics audio.android section
Verification + diagnostics: - apps/chanora_flutter/test/services/back_intent_policy_test.dart: 8-case truth table for the BackIntentPolicy pure function (SWE4-UV-042). - apps/chanora_flutter/test/services/back_intent_service_test.dart: 5 channel-routing tests (SWE4-UV-042, SWE5-IV-019 unit slice). - apps/chanora_flutter/test/services/android_permissions_service_test.dart: 12 tests covering inbound channel events, outbound requests, state-machine transitions, and non-Android short-circuit (SWE4-UV-041). - SDD/SRS trace headers added to alpha_e2e_test.dart, beta_e2e_test.dart, widget_test.dart so the existing test ↔ ID mapping is discoverable by grep. - chanora_diagnostics: extends DiagnosticExport with android_audio: Option<String> for the SDD-116 evidence schema (requested / achieved performance mode + sharing mode + input preset + sample rate + frames per burst, per-effect engagement, latency tier). The bridge's export_diagnostics() now embeds the Android section when current_android_audio_diagnostics() returns Some. All 93 workspace Rust tests and 26 Dart test/services tests pass. Trace: SDD-090, SDD-112, SDD-113, SDD-116, SWE4-UV-041, SWE4-UV-042, SWE4-UV-045, SWE4-UV-047, SWE4-UV-048, SWE4-UV-049, SWE4-UV-051, SWE4-UV-052, SWE5-IV-019.
This commit is contained in:
@@ -606,6 +606,12 @@ pub struct DiagnosticExport {
|
||||
/// Number of currently registered known-secret values. The
|
||||
/// values themselves are *not* exported.
|
||||
pub known_secret_count: usize,
|
||||
/// SDD-116 item 3: Android voice-audio diagnostics YAML
|
||||
/// fragment, or `None` on non-Android targets / before a voice
|
||||
/// session has opened. The producing crate guarantees this
|
||||
/// fragment contains only device-side technical scalars per
|
||||
/// SDD-090 (no PII, no permission state, no server identity).
|
||||
pub android_audio: Option<String>,
|
||||
}
|
||||
|
||||
impl DiagnosticExport {
|
||||
@@ -618,9 +624,20 @@ impl DiagnosticExport {
|
||||
metadata,
|
||||
recent_logs: sink.snapshot(),
|
||||
known_secret_count: sink.redactor().secrets().len(),
|
||||
android_audio: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach an Android voice-audio diagnostics YAML fragment
|
||||
/// (SDD-112 item 10 / SDD-113 item 7 / SDD-116 item 3). The
|
||||
/// caller is responsible for ensuring the fragment is already
|
||||
/// sanitised per SDD-090; the diagnostics bundle does not
|
||||
/// re-redact this string.
|
||||
pub fn with_android_audio(mut self, yaml_fragment: Option<String>) -> Self {
|
||||
self.android_audio = yaml_fragment;
|
||||
self
|
||||
}
|
||||
|
||||
/// Render as a plaintext blob suitable for `Share` / `Copy`.
|
||||
/// The output is multi-line UTF-8, redacted.
|
||||
pub fn to_text(&self) -> String {
|
||||
@@ -634,6 +651,11 @@ impl DiagnosticExport {
|
||||
for (k, v) in &self.metadata {
|
||||
out.push_str(&format!("{k}: {v}\n"));
|
||||
}
|
||||
if let Some(yaml) = &self.android_audio {
|
||||
// SDD-116 item 3 verification-matrix section.
|
||||
out.push_str("\n[audio.android]\n");
|
||||
out.push_str(yaml);
|
||||
}
|
||||
out.push_str("\n[recent logs]\n");
|
||||
for line in &self.recent_logs {
|
||||
out.push_str(line);
|
||||
@@ -873,4 +895,77 @@ mod tests {
|
||||
assert!(!text.contains("u128 banned record must drop"));
|
||||
assert!(text.contains("safe record must pass"));
|
||||
}
|
||||
|
||||
/// SWE4-UV-056 — `DiagnosticExport` with `android_audio` renders the
|
||||
/// `[audio.android]` section AFTER `[metadata]` and BEFORE
|
||||
/// `[recent logs]`, with the YAML fragment immediately following
|
||||
/// the header byte-for-byte (the bundle does not re-redact per
|
||||
/// SDD-090 trust boundary).
|
||||
#[test]
|
||||
fn android_audio_renders_between_metadata_and_logs() {
|
||||
let secrets = KnownSecretRegistry::default();
|
||||
let redactor = Redactor::with_secrets(secrets);
|
||||
let sink = InMemoryLogSink::new(16, redactor);
|
||||
let yaml = "achieved:\n performance_mode: LowLatency\n sample_rate_hz: 48000\n";
|
||||
let exported = DiagnosticExport::from_sink(
|
||||
&sink,
|
||||
vec![("build".into(), "test".into())],
|
||||
)
|
||||
.unwrap()
|
||||
.with_android_audio(Some(yaml.to_string()));
|
||||
let text = exported.to_text();
|
||||
|
||||
let metadata_pos = text.find("[metadata]").expect("metadata section");
|
||||
let android_pos = text.find("[audio.android]").expect("android section");
|
||||
let logs_pos = text.find("[recent logs]").expect("logs section");
|
||||
assert!(
|
||||
metadata_pos < android_pos,
|
||||
"[metadata] must come before [audio.android]"
|
||||
);
|
||||
assert!(
|
||||
android_pos < logs_pos,
|
||||
"[audio.android] must come before [recent logs]"
|
||||
);
|
||||
|
||||
// The YAML fragment must follow the header byte-for-byte
|
||||
// (no re-redaction, no reformatting — SDD-090 trust boundary).
|
||||
let header = "[audio.android]\n";
|
||||
let header_start = text.find(header).expect("audio.android header");
|
||||
let after_header = &text[header_start + header.len()..];
|
||||
assert!(
|
||||
after_header.starts_with(yaml),
|
||||
"YAML fragment must follow [audio.android] header verbatim"
|
||||
);
|
||||
}
|
||||
|
||||
/// SWE4-UV-057 — `DiagnosticExport` with default `android_audio =
|
||||
/// None` omits the `[audio.android]` header entirely (negative
|
||||
/// test). Also verifies idempotence of explicit
|
||||
/// `with_android_audio(None)`.
|
||||
#[test]
|
||||
fn android_audio_absent_omits_section() {
|
||||
let secrets = KnownSecretRegistry::default();
|
||||
let redactor = Redactor::with_secrets(secrets);
|
||||
let sink = InMemoryLogSink::new(16, redactor);
|
||||
|
||||
// Default — never call with_android_audio.
|
||||
let exported_default =
|
||||
DiagnosticExport::from_sink(&sink, vec![("k".into(), "v".into())]).unwrap();
|
||||
let text_default = exported_default.to_text();
|
||||
assert!(
|
||||
!text_default.contains("[audio.android]"),
|
||||
"default export must omit [audio.android] section"
|
||||
);
|
||||
|
||||
// Explicit None — idempotent with default.
|
||||
let exported_explicit =
|
||||
DiagnosticExport::from_sink(&sink, vec![("k".into(), "v".into())])
|
||||
.unwrap()
|
||||
.with_android_audio(None);
|
||||
let text_explicit = exported_explicit.to_text();
|
||||
assert!(
|
||||
!text_explicit.contains("[audio.android]"),
|
||||
"explicit with_android_audio(None) must also omit the section"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user