Add local spoken passages with Google retrieval and isolated runtime setup

This commit is contained in:
karl committed 2026-10-13 20:18:44 +02:00
1 parent bc9459785d
commit dab703413e
55 files changed
+3633 -70

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
//! Verify every isolated runtime code artifact before starting a worker.
use super::{file_hash, Device, UPSTREAM};
use anyhow::{ensure, Context, Result};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, BTreeSet},
fs,
path::Path,
};
#[derive(Deserialize)]
struct Manifest {
schema: u32,
upstream_commit: String,
device: String,
distribution: bool,
source_hashes: BTreeMap<String, String>,
files: Vec<Entry>,
}
#[derive(Deserialize)]
struct Entry {
path: String,
bytes: u64,
sha256: String,
}
pub(crate) fn verify(worker: &Path, device: Device) -> Result<String> {
let directory = worker
.parent()
.context("Missing speech runtime directory")?;
let path = directory.join("runtime-manifest.json");
ensure!(
fs::symlink_metadata(&path)
.context("Speech runtime inventory missing")?
.file_type()
.is_file(),
"Runtime inventory must be a regular file"
);
ensure!(
fs::metadata(&path)?.len() <= 512 * 1024,
"Runtime inventory exceeds bound"
);
let bytes = fs::read(path)?;
let manifest: Manifest =
serde_json::from_slice(&bytes).context("Invalid speech runtime inventory")?;
ensure!(
manifest.schema == 1 && manifest.upstream_commit == UPSTREAM,
"Stale speech runtime inventory"
);
ensure!(
matches!(manifest.device.as_str(), "auto" | "cpu")
&& manifest.distribution == (manifest.device == "auto"),
"Inconsistent speech distribution inventory"
);
ensure!(
device == Device::Cpu || manifest.distribution,
"CPU-only speech runtime is an internal tool"
);
for name in ["CMakeLists.txt", "worker.cpp", "build.py"] {
ensure!(
manifest.source_hashes.get(name).is_some_and(|s| hash(s)),
"Missing speech build-input provenance"
);
}
ensure!(
!manifest.files.is_empty() && manifest.files.len() <= 128,
"Unbounded speech runtime inventory"
);
let mut seen = BTreeSet::new();
let mut total = 0u64;
for entry in manifest.files {
ensure!(
!entry.path.is_empty()
&& entry.path != "."
&& entry.path != ".."
&& !entry.path.contains(['/', '\\', ':'])
&& seen.insert(entry.path.clone()),
"Invalid speech inventory filename"
);
ensure!(
entry.bytes > 0 && entry.bytes <= 256 * 1024 * 1024 && hash(&entry.sha256),
"Invalid speech inventory artifact"
);
total = total
.checked_add(entry.bytes)
.context("Speech runtime size overflow")?;
ensure!(
total <= 1024 * 1024 * 1024,
"Speech runtime exceeds size budget"
);
let path = directory.join(&entry.path);
ensure!(
fs::symlink_metadata(&path)?.file_type().is_file()
&& fs::metadata(&path)?.len() == entry.bytes
&& file_hash(&path)? == entry.sha256,
"Speech runtime artifact size/SHA changed"
);
}
ensure!(
worker
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|name| seen.contains(name)),
"Worker omitted from speech runtime inventory"
);
Ok(format!("{:x}", Sha256::digest(bytes)))
}
fn hash(value: &str) -> bool {
value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn altered_dependency_or_missing_inventory_fails_before_process_launch() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!(
"searchi-speech-inventory-{}-{unique}",
std::process::id()
));
fs::create_dir(&root).unwrap();
let worker = root.join("worker");
let library = root.join("ggml-cpu.dll");
fs::write(&worker, b"structural worker fixture").unwrap();
fs::write(&library, b"structural library fixture").unwrap();
assert!(verify(&worker, Device::Cpu).is_err());
let artifact = |path: &Path| json!({"path":path.file_name().unwrap().to_str().unwrap(),"bytes":fs::metadata(path).unwrap().len(),"sha256":file_hash(path).unwrap()});
let sources: BTreeMap<_, _> = ["CMakeLists.txt", "worker.cpp", "build.py"]
.into_iter()
.map(|s| (s, "a".repeat(64)))
.collect();
let manifest = json!({"schema":1,"upstream_commit":UPSTREAM,"device":"cpu","distribution":false,"source_hashes":sources,"files":[artifact(&worker),artifact(&library)]});
fs::write(
root.join("runtime-manifest.json"),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
let identity = verify(&worker, Device::Cpu).unwrap();
assert_eq!(identity.len(), 64);
assert!(verify(&worker, Device::Auto).is_err());
fs::write(library, b"same length changed bits").unwrap();
assert!(verify(&worker, Device::Cpu).is_err());
fs::remove_dir_all(root).unwrap();
}
}
+273
View File
@@ -0,0 +1,273 @@
//! Local, isolated speech recognition. Google embeddings remain a separate lane.
//! Calls consume at most thirty seconds of mono 16 kHz PCM. The service admits
//! each call to its background inference budget; this client creates no scheduler.
mod inventory;
mod process;
use anyhow::{ensure, Context, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
fs,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
pub const SAMPLE_RATE: u32 = 16000;
pub const MAX_SAMPLES: usize = 480000;
pub const POLICY: &str = "live-window-vad-dtw-v1;silero-v6.2.0;base;30s;vad-reset;threshold0.5;minspeech250ms;minsilence100ms;maxspeechFLT_MAX;pad30ms;vad-overlap0.1s;source-sample-intersections;beam5;bestof5;temp0;tempinc0.2;entropy2.4;logprob-1;nospeech0.6;suppressblank;no-suppress-nst;auto-language;no-context;dtw-base;no-flash;contiguous-intervals;first-last-token-points;exact-utf8";
pub const UPSTREAM: &str = "d1be6fde11ac6e0407606b4e42fe72d34add8037";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpeechPassage {
pub start_ms: u32,
pub end_ms: u32,
pub text: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SpeechMetrics {
pub vad_ms: f64,
pub asr_ms: f64,
pub load_ms: f64,
pub transport_ms: f64,
#[serde(default)]
pub fallback_ms: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpeechTranscription {
pub passages: Vec<SpeechPassage>,
pub metrics: SpeechMetrics,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Device {
Auto,
Cpu,
}
/// One owned persistent process. Base recognition context loads only on speech.
pub struct SpeechWorker {
process: process::WorkerProcess,
fingerprint: String,
worker_path: PathBuf,
base_path: PathBuf,
vad_path: PathBuf,
device: Device,
}
fn verified_file(path: &Path, bytes: u64, expected: &str) -> Result<String> {
ensure!(
fs::metadata(path)?.is_file() && fs::metadata(path)?.len() == bytes,
"Speech model size differs"
);
let actual = file_hash(path)?;
ensure!(actual == expected, "Speech model SHA-256 differs");
Ok(actual)
}
fn file_hash(path: &Path) -> Result<String> {
let mut file = fs::File::open(path)?;
let mut hash = Sha256::new();
let mut buffer = [0; 65536];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hash.update(&buffer[..n]);
}
Ok(format!("{:x}", hash.finalize()))
}
impl SpeechWorker {
/// Verify exact pinned model bytes before launching the isolated worker.
/// Normal distributions use Auto; Windows VM tests must select Cpu.
pub fn load(worker: &Path, base: &Path, vad: &Path, device: Device) -> Result<Self> {
let device = if std::env::var_os("SEARCHI_FORCE_CPU").is_some_and(|v| v == "1") {
Device::Cpu
} else {
device
};
let inventory_hash = inventory::verify(worker, device)?;
let base_hash = verified_file(
base,
147951465,
"60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe",
)?;
let vad_hash = verified_file(
vad,
885098,
"2aa269b785eeb53a82983a20501ddf7c1d9c48e33ab63a41391ac6c9f7fb6987",
)?;
let meta = fs::metadata(worker)?;
ensure!(
meta.is_file() && meta.len() > 0 && meta.len() <= 64 * 1024 * 1024,
"Invalid speech worker size"
);
let worker_hash = file_hash(worker)?;
let fingerprint = format!(
"{POLICY};runtime={UPSTREAM};inventory={inventory_hash};worker={worker_hash};base={base_hash};vad={vad_hash}"
);
let process = process::WorkerProcess::launch(worker, base, vad, device)?;
Ok(Self {
process,
fingerprint,
worker_path: worker.to_owned(),
base_path: base.to_owned(),
vad_path: vad.to_owned(),
device,
})
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn transcribe(&mut self, pcm: &[f32], sample_rate: u32) -> Result<SpeechTranscription> {
ensure!(
sample_rate == SAMPLE_RATE && !pcm.is_empty() && pcm.len() <= MAX_SAMPLES,
"Speech requires bounded mono 16 kHz PCM"
);
ensure!(pcm.iter().all(|v| v.is_finite()), "Nonfinite speech input");
let fallback_started = Instant::now();
match self.transcribe_once(pcm) {
Ok(value) => Ok(value),
Err(error) if self.device == Device::Auto && process::recoverable(&error) => {
// Retry only initialization/compute/transport failures. Reverify
// assets and all runtime code before a sticky CPU replacement.
let replacement = Self::load(
&self.worker_path,
&self.base_path,
&self.vad_path,
Device::Cpu,
)?;
ensure!(
replacement.fingerprint == self.fingerprint,
"Speech assets changed before CPU fallback"
);
*self = replacement;
let fallback_ms = fallback_started.elapsed().as_secs_f64() * 1000.0;
let mut result = self.transcribe_once(pcm)?;
result.metrics.fallback_ms = fallback_ms;
Ok(result)
}
Err(error) => Err(error),
}
}
fn transcribe_once(&mut self, pcm: &[f32]) -> Result<SpeechTranscription> {
let started = Instant::now();
let detection = self.process.request(1, pcm)?;
let mut metrics = SpeechMetrics::default();
add_metrics(&mut metrics, &detection)?;
let intervals = detection["intervals_samples"]
.as_array()
.context("Missing live VAD intervals")?;
ensure!(intervals.len() <= 128, "Oversized live VAD intervals");
let mut previous = 0;
let mut passages = Vec::new();
for interval in intervals {
let pair = interval.as_array().context("Invalid VAD interval")?;
ensure!(pair.len() == 2, "Invalid VAD interval pair");
let start = usize::try_from(pair[0].as_u64().context("Invalid VAD start")?)?;
let end = usize::try_from(pair[1].as_u64().context("Invalid VAD end")?)?;
ensure!(
start >= previous && start < end && end <= pcm.len(),
"Invalid VAD source bounds"
);
previous = end;
let recognition = self.process.request(2, &pcm[start..end])?;
add_metrics(&mut metrics, &recognition)?;
passages.extend(parse_passages(&recognition, start, end - start)?);
ensure!(
passages.len() <= 64
&& passages.iter().map(|p| p.text.len()).sum::<usize>() <= 64 * 1024,
"Recognized speech output exceeds window budget"
);
}
let native = metrics.vad_ms + metrics.asr_ms + metrics.load_ms;
metrics.transport_ms = (started.elapsed().as_secs_f64() * 1000.0 - native).max(0.0);
Ok(SpeechTranscription { passages, metrics })
}
}
fn add_metrics(metrics: &mut SpeechMetrics, response: &serde_json::Value) -> Result<()> {
for (field, target) in [
("vad_ms", &mut metrics.vad_ms),
("asr_ms", &mut metrics.asr_ms),
("load_ms", &mut metrics.load_ms),
] {
let v = response[field].as_f64().context("Missing speech timing")?;
ensure!(v.is_finite() && v >= 0.0, "Invalid speech timing");
*target += v;
}
Ok(())
}
fn parse_passages(
response: &serde_json::Value,
offset_samples: usize,
samples: usize,
) -> Result<Vec<SpeechPassage>> {
let rows = response["sentences"]
.as_array()
.context("Missing recognized sentences")?;
ensure!(rows.len() <= 64, "Oversized recognized sentences");
let mut previous = 0;
let mut result = Vec::new();
let mut text_bytes = 0;
for row in rows {
let start = row["start_cs"].as_u64().context("Invalid DTW start")?;
let end = row["end_cs"].as_u64().context("Invalid DTW end")?;
ensure!(
start >= previous
&& start <= end
&& end.checked_mul(160).is_some_and(|v| v <= samples as u64),
"Invalid DTW source point span"
);
previous = end;
let text = row["text"]
.as_str()
.context("Invalid recognized UTF-8 text")?;
ensure!(
!text.trim().is_empty() && text.len() <= 4096,
"Invalid recognized text size"
);
text_bytes += text.len();
ensure!(
text_bytes <= 64 * 1024,
"Recognized text exceeds window budget"
);
let origin = offset_samples as u64 / 16;
result.push(SpeechPassage {
start_ms: u32::try_from(origin + start * 10)?,
end_ms: u32::try_from(origin + end * 10)?,
text: text.to_owned(),
});
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn actual_points_may_have_zero_span_and_origin_is_applied_once() {
let response = json!({"sentences":[{"start_cs":2,"end_cs":2,"text":" exact "}]});
assert_eq!(
parse_passages(&response, 16000, 16000).unwrap(),
vec![SpeechPassage {
start_ms: 1020,
end_ms: 1020,
text: " exact ".into()
}]
);
}
#[test]
fn reversed_missing_outside_nonmonotonic_points_and_oversized_text_rejected() {
for row in [
json!({"start_cs":2,"end_cs":1,"text":"x"}),
json!({"start_cs":0,"end_cs":101,"text":"x"}),
json!({"start_cs":1.5,"end_cs":2,"text":"x"}),
json!({"start_cs":0,"end_cs":2,"text":"x".repeat(4097)}),
] {
assert!(parse_passages(&json!({"sentences":[row]}), 0, 16000).is_err());
}
}
}
+289
View File
@@ -0,0 +1,289 @@
//! Bounded binary PCM transport to a single owned native process.
use super::{Device, MAX_SAMPLES, POLICY, UPSTREAM};
use anyhow::{ensure, Context, Result};
use serde_json::Value;
use std::{
io::{Read, Write},
path::Path,
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
sync::mpsc,
thread::{self, JoinHandle},
time::{Duration, Instant},
};
const MAX_OUTPUT: usize = 4 * 1024 * 1024;
const DEADLINE: Duration = Duration::from_secs(600);
#[derive(Debug)]
pub(crate) enum NativeFailure {
Initialization,
Compute,
MalformedText,
InvalidPoints,
Rejected,
}
impl NativeFailure {
pub fn recoverable(&self) -> bool {
matches!(self, Self::Initialization | Self::Compute)
}
}
impl std::fmt::Display for NativeFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Initialization => "Speech native initialization failed",
Self::Compute => "Speech native computation failed",
Self::MalformedText => "Speech recognized invalid UTF-8",
Self::InvalidPoints => "Speech recognized invalid DTW points",
Self::Rejected => "Speech native operation rejected",
})
}
}
impl std::error::Error for NativeFailure {}
#[derive(Debug)]
struct ProtocolFailure;
impl std::fmt::Display for ProtocolFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Invalid speech worker response protocol")
}
}
impl std::error::Error for ProtocolFailure {}
#[derive(Debug)]
struct TransportFailure;
impl std::fmt::Display for TransportFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Speech worker transport exited or timed out")
}
}
impl std::error::Error for TransportFailure {}
pub(crate) fn recoverable(error: &anyhow::Error) -> bool {
error.is::<TransportFailure>()
|| error
.downcast_ref::<NativeFailure>()
.is_some_and(NativeFailure::recoverable)
}
fn read_response<R: Read>(reader: &mut R) -> Result<Value> {
let mut prefix = [0; 4];
reader
.read_exact(&mut prefix)
.context("Speech worker closed response")?;
let bytes = u32::from_le_bytes(prefix) as usize;
if bytes == 0 || bytes > MAX_OUTPUT {
return Err(ProtocolFailure.into());
}
let mut output = vec![0; bytes];
reader.read_exact(&mut output)?;
let value: Value = serde_json::from_slice(&output).map_err(|_| ProtocolFailure)?;
if !value.is_object() {
return Err(ProtocolFailure.into());
}
Ok(value)
}
pub(crate) struct WorkerProcess {
child: Child,
stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>,
stderr: Option<JoinHandle<()>>,
failed: bool,
distribution_auto: bool,
}
impl WorkerProcess {
pub fn launch(worker: &Path, base: &Path, vad: &Path, device: Device) -> Result<Self> {
let mut command = Command::new(worker);
command.arg(base).arg(vad).arg(match device {
Device::Auto => "auto",
Device::Cpu => "cpu",
});
let result = Self::spawn(command, Duration::from_secs(30))?;
ensure!(
device == Device::Cpu || result.distribution_auto,
"CPU-only native speech worker is an internal tool"
);
Ok(result)
}
fn spawn(mut command: Command, startup_deadline: Duration) -> Result<Self> {
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x08000000);
}
let mut child = command.spawn().context("Launch isolated speech worker")?;
let stdin = child.stdin.take();
let stdout = child.stdout.take();
// Drain continuously without retaining transcripts or unbounded log bytes.
let stderr = child.stderr.take().map(|mut pipe| {
thread::spawn(move || {
let mut buffer = [0; 4096];
while pipe.read(&mut buffer).is_ok_and(|n| n > 0) {}
})
});
let mut result = Self {
child,
stdin,
stdout,
stderr,
failed: false,
distribution_auto: false,
};
let ready = result.exchange(Vec::new(), startup_deadline)?;
ensure!(
ready["protocol"] == 1
&& ready["runtime"] == UPSTREAM
&& ready["ready"] == true
&& ready["policy"] == POLICY,
"Legacy or incompatible speech worker"
);
result.distribution_auto = ready["distribution_auto"].as_bool().unwrap_or(false);
Ok(result)
}
pub fn request(&mut self, operation: u8, pcm: &[f32]) -> Result<Value> {
ensure!(
!self.failed && !pcm.is_empty() && pcm.len() <= MAX_SAMPLES,
"Unavailable speech session or unbounded PCM"
);
let mut request = Vec::with_capacity(5 + pcm.len() * 4);
request.push(operation);
request.extend_from_slice(&(pcm.len() as u32).to_le_bytes());
for value in pcm {
request.extend_from_slice(&value.to_le_bytes());
}
let result = self.exchange(request, DEADLINE)?;
if result["ok"] != true {
return Err(match result["error"].as_str() {
Some("base_load_failed" | "vad_load_failed") => NativeFailure::Initialization,
Some("recognition_failed" | "vad_failed" | "vad_intervals_failed") => {
NativeFailure::Compute
}
Some("malformed_recognition_utf8") => NativeFailure::MalformedText,
Some("invalid_dtw_point") => NativeFailure::InvalidPoints,
_ => NativeFailure::Rejected,
}
.into());
}
ensure!(
result["operation"] == operation && result["samples"] == pcm.len(),
"Speech response identity differs"
);
Ok(result)
}
fn exchange(&mut self, request: Vec<u8>, deadline: Duration) -> Result<Value> {
let mut input = self.stdin.take().context("Missing speech input")?;
let mut output = self.stdout.take().context("Missing speech output")?;
let (sender, receiver) = mpsc::channel();
let task = thread::spawn(move || {
let result = (|| {
if !request.is_empty() {
input.write_all(&request)?;
input.flush()?;
}
read_response(&mut output)
})();
let _ = sender.send((input, output, result));
});
let started = Instant::now();
let result = loop {
match receiver.recv_timeout(Duration::from_millis(20)) {
Ok(value) => break Some(value),
Err(mpsc::RecvTimeoutError::Timeout) => {
if started.elapsed() >= deadline || !matches!(self.child.try_wait(), Ok(None)) {
break None;
}
}
Err(mpsc::RecvTimeoutError::Disconnected) => break None,
}
};
if result.is_none() {
self.failed = true;
let _ = self.child.kill();
let _ = self.child.wait();
}
let _ = task.join();
match result {
Some((stdin, stdout, result)) => {
self.stdin = Some(stdin);
self.stdout = Some(stdout);
if result.is_err() {
self.failed = true;
let _ = self.child.kill();
let _ = self.child.wait();
}
result.map_err(|error| {
if error.is::<std::io::Error>() {
TransportFailure.into()
} else {
error
}
})
}
None => Err(TransportFailure.into()),
}
}
}
impl Drop for WorkerProcess {
fn drop(&mut self) {
self.stdin.take();
self.stdout.take();
let _ = self.child.kill();
let _ = self.child.wait();
if let Some(task) = self.stderr.take() {
let _ = task.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn framing_rejects_oversize_truncated_and_invalid_utf8_before_json() {
assert!(read_response(&mut &((MAX_OUTPUT as u32 + 1).to_le_bytes())[..]).is_err());
assert!(read_response(&mut &[5, 0, 0, 0, b'{'][..]).is_err());
assert!(read_response(&mut &[2, 0, 0, 0, 0xff, 0xff][..]).is_err());
let mut frame = (11u32).to_le_bytes().to_vec();
frame.extend_from_slice(b"{\"ok\":true}");
assert_eq!(read_response(&mut frame.as_slice()).unwrap()["ok"], true);
}
#[test]
#[ignore = "Requires Python for actual owned subprocess transport fixtures, no inference"]
fn actual_subprocess_timeout_and_overflow_are_reaped() {
let python = std::env::var_os("SEARCHI_TEST_PYTHON").unwrap_or_else(|| "python".into());
let ready=format!("import sys,struct,json,time; b=json.dumps({{'protocol':1,'ready':True,'runtime':'{UPSTREAM}','policy':'{POLICY}'}}).encode();sys.stdout.buffer.write(struct.pack('<I',len(b))+b);sys.stdout.buffer.flush();");
let mut command = Command::new(&python);
command.arg("-c").arg(format!("{ready}time.sleep(10)"));
let mut process = WorkerProcess::spawn(command, Duration::from_secs(5)).unwrap();
let error = process
.exchange(vec![1], Duration::from_millis(50))
.unwrap_err();
assert!(recoverable(&error));
assert!(process.child.try_wait().unwrap().is_some());
assert!(process.failed);
let mut command = Command::new(&python);
command.arg("-c").arg(format!("{ready}sys.stdin.buffer.read(1);sys.stdout.buffer.write(struct.pack('<I',4194305));sys.stdout.buffer.flush();time.sleep(10)"));
let mut process = WorkerProcess::spawn(command, Duration::from_secs(5)).unwrap();
let error = process
.exchange(vec![1], Duration::from_secs(1))
.unwrap_err();
assert!(!recoverable(&error));
assert!(process.child.try_wait().unwrap().is_some());
assert!(process.failed);
}
#[test]
fn invalid_text_points_and_protocol_never_authorize_cpu_retry() {
for error in [
anyhow::Error::new(NativeFailure::MalformedText),
anyhow::Error::new(NativeFailure::InvalidPoints),
anyhow::Error::new(ProtocolFailure),
] {
assert!(!recoverable(&error));
}
assert!(recoverable(&NativeFailure::Initialization.into()));
assert!(recoverable(&NativeFailure::Compute.into()));
assert!(!recoverable(
&read_response(&mut &[2, 0, 0, 0, 0xff, 0xff][..]).unwrap_err()
));
}
}