Author SHA1 Message Date
molluskandClaude Fable 5 7b9cb57003 core: survive a failed net-stack rebuild instead of silently dying
CI / check (push) Failing after 13m37s
The four live-rebuild sites (deferred rebuild on Join/Leave, idle
SetNetworkMode, idle RegenerateIdentity) all did
`net.shutdown().await` then `build_net_stack(...).await?` — a build
failure propagated out of run_core_loop, which its supervisor only
logs. Every subsequent command went nowhere: window alive, app dead,
user told nothing. (The initial startup build already reported.)

New replace_net_stack() helper: tear down the old stack, build for the
requested posture, and on failure fall back to the posture the old
stack was actually running (tracked in the new `net_mode` local; when
the postures are equal the fallback is a plain retry — e.g. identity
regeneration, where reverting the already-persisted key would be
wrong). If the fallback lands, the UI is told the change didn't stick
and `network_mode` reverts so state stays honest and the change stays
re-attemptable. If both builds fail the UI gets a fatal 'Networking
lost … restart' error before the loop exits — informed, not a zombie.

Retry policy isolated in rebuild_with_fallback(), generic over the
builder: 4 new unit tests cover first-try success, fall-back, plain
retry, and double failure without binding sockets.

Fixes finding 2 of the 2026-07-16 full-codebase review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:58:58 -04:00
molluskandClaude Fable 5 af7a42a049 ci: remove zombie workflows from the per-push pipeline
cargo-deny.yml (runs-on: ubuntu-latest) and windows-build.yml (runs-on:
windows-latest) target runner labels no registered runner advertises, so
every push queued two runs Gitea auto-cancelled ~24h later — the Actions
page has shown 2 cancelled runs per push since the runner went live.

- cargo-deny.yml: deleted; redundant with ci.yml's deny step, which now
  runs `cargo deny --locked check` to preserve the locked-tree stance.
- windows-build.yml: kept but workflow_dispatch-only until a Windows
  runner exists; restore instructions in the header comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:46:56 -04:00
molluskandClaude Fable 5 e78e7bc2a5 supply-chain: ignore quick-xml build-time DoS advisories + ttf-parser unmaintained
RUSTSEC-2026-0194/0195 (quick-xml 0.39.4, published 2026-06-29) broke the
deny/audit CI gates on every push since June 29. quick-xml is reached only
via the wayland-scanner proc-macro parsing vendored protocol XML at compile
time — attacker input never touches it and it is absent from the shipped
binary. The fixed 0.41.0 is semver-incompatible with wayland-scanner's
`^0.39` req (no upstream bump yet); documented ignores until one exists.

RUSTSEC-2026-0192 (ttf-parser unmaintained, via iced/cosmic-text) joins the
existing unmaintained ignores (paste, audiopus_sys) — same class, same
lockfile-pinning protection.

New .cargo/audit.toml keeps cargo-audit in sync with deny.toml.

Known leftover warning (allowed, non-failing): spin 0.10.0 is yanked but
futures-buffered (via iroh) requires ^0.10 and no unyanked 0.10.x exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:46:13 -04:00
molluskandClaude Fable 5 52ab374b74 style: cargo fmt under rustfmt 1.9.0 (toolchain update 2026-07-08)
Six diffs across four files: the 2026-07-08 stable toolchain update
(rustc 1.96.1 / rustfmt 1.9.0) re-flags code that was fmt-clean when
committed under the previous rustfmt. No semantic change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:45:18 -04:00
mollusk 8825707c17 chore: patch crossbeam-epoch RustSec advisory
CI / check (push) Failing after 7s
cargo-deny / cargo-deny (push) Has been cancelled
windows-build / windows-build (push) Has been cancelled
2026-07-15 06:31:09 -04:00
11 changed files with 314 additions and 84 deletions
+11
View File
@@ -0,0 +1,11 @@
# cargo-audit configuration. Keep the ignore list in sync with deny.toml,
# which carries the full justification for each entry.
[advisories]
ignore = [
# quick-xml DoS advisories: build-time only, reached solely via the
# wayland-scanner proc-macro parsing trusted vendored protocol XML.
# Fix (0.41.0) is semver-incompatible with wayland-scanner's `^0.39`;
# drop once wayland-scanner bumps. See deny.toml.
"RUSTSEC-2026-0194",
"RUSTSEC-2026-0195",
]
-34
View File
@@ -1,34 +0,0 @@
name: cargo-deny
# Enforce the supply-chain policy in deny.toml (advisories / bans / licenses /
# sources) on every push to main and every PR. Runs on a *locked* tree so the
# pinned, vetted versions in Cargo.lock are exactly what get audited — see the
# deny.toml header and VERSIONING.md. A new poisoned release of a dependency
# cannot reach CI until Cargo.lock is deliberately updated.
on:
push:
branches: [main]
pull_request:
jobs:
cargo-deny:
runs-on: ubuntu-latest
# rust:1 provides the cargo toolchain that cargo-deny shells out to for
# `cargo metadata`. Adjust the runner label if your act_runner uses a
# different one.
container: rust:1
steps:
- uses: actions/checkout@v4
- name: Install cargo-deny (pinned prebuilt)
run: |
set -euo pipefail
version=0.19.9
curl -sSfL \
"https://github.com/EmbarkStudios/cargo-deny/releases/download/${version}/cargo-deny-${version}-x86_64-unknown-linux-musl.tar.gz" \
| tar -xz -C /usr/local/bin --strip-components=1 --wildcards '*/cargo-deny'
cargo-deny --version
- name: cargo deny check
run: cargo deny --locked check
+3 -1
View File
@@ -36,7 +36,9 @@ jobs:
run: cargo test --doc run: cargo test --doc
- name: cargo-deny (advisories, bans, licenses, sources) - name: cargo-deny (advisories, bans, licenses, sources)
run: cargo deny check # --locked so the pinned, vetted versions in Cargo.lock are exactly
# what get audited (the lockfile-as-review-checkpoint model).
run: cargo deny --locked check
- name: cargo-audit - name: cargo-audit
run: cargo audit run: cargo audit
+15 -11
View File
@@ -7,11 +7,20 @@ name: windows-build
# alias) so a Unix-only assumption can't sneak back in and break Windows. # alias) so a Unix-only assumption can't sneak back in and break Windows.
# #
# RUNNER REQUIREMENT: this needs a Windows act_runner registered with the # RUNNER REQUIREMENT: this needs a Windows act_runner registered with the
# `windows-latest` label (the Linux `cargo-deny` job's container approach does # `windows-latest` label (a Linux-container approach does NOT apply here —
# NOT apply here — Windows jobs run on the host, not a Linux container). If your # Windows jobs run on the host, not a Linux container). If your runner
# runner advertises a different label, change `runs-on` below. Until a Windows # advertises a different label, change `runs-on` below.
# runner exists this workflow is simply skipped/queued, not a failure of the #
# Linux CI. # MANUAL-ONLY until that runner exists: with push/PR triggers enabled, every
# push queued a run no runner could claim and Gitea auto-cancelled it ~24h
# later, littering the Actions page with cancelled runs. Restore the push/PR
# triggers when a Windows runner is registered:
#
# on:
# push:
# branches: [main, "windows-port-**"]
# pull_request:
# workflow_dispatch:
# #
# BUILD-HOST REQUIREMENTS (validated by the opus spike, see # BUILD-HOST REQUIREMENTS (validated by the opus spike, see
# peerspeak-windows-opus-spike.md): # peerspeak-windows-opus-spike.md):
@@ -23,12 +32,7 @@ name: windows-build
# must provide both. # must provide both.
on: on:
push: # Manual runs from the Gitea Actions UI only — see the header comment.
# `main` plus the in-progress port branches, so the Windows path is exercised
# before merge rather than only after.
branches: [main, "windows-port-**"]
pull_request:
# Allow manual runs from the Gitea Actions UI.
workflow_dispatch: workflow_dispatch:
permissions: permissions:
Generated
+2 -2
View File
@@ -1207,9 +1207,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.18" version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
+13
View File
@@ -24,6 +24,19 @@ ignore = [
# audiopus_sys: unmaintained FFI bindings to the stable libopus C library, # audiopus_sys: unmaintained FFI bindings to the stable libopus C library,
# pulled in via our direct `opus 0.3.1` dep. No drop-in replacement. # pulled in via our direct `opus 0.3.1` dep. No drop-in replacement.
"RUSTSEC-2026-0150", "RUSTSEC-2026-0150",
# ttf-parser: unmaintained, transitive via iced/cosmic-text (font parsing
# for the GUI). Inputs are system + embedded fonts, not network data. No
# upstream migration yet; revisit when iced moves off it.
"RUSTSEC-2026-0192",
# quick-xml 0.39.4 DoS advisories (quadratic dup-attr check; unbounded
# namespace allocation). Build-time only: quick-xml is reached solely via
# the wayland-scanner PROC-MACRO, which parses the wayland protocol XML
# files vendored inside the wayland-* crates at compile time. Attacker
# input never reaches it and it is not in the shipped binary. The fix
# (0.41.0) is semver-incompatible with wayland-scanner 0.31.x's `^0.39`
# requirement; drop both ignores once wayland-scanner releases a bump.
"RUSTSEC-2026-0194",
"RUSTSEC-2026-0195",
] ]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+13 -5
View File
@@ -6239,7 +6239,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
// Rendered only while the audio link is live — the // Rendered only while the audio link is live — the
// Connecting/Reconnecting indicator covers the rest. // Connecting/Reconnecting indicator covers the rest.
if let Some(info) = state.conn_stats.get(peer_id) { if let Some(info) = state.conn_stats.get(peer_id) {
let dot_color = if info.relay { color_yellow } else { color_green }; let dot_color = if info.relay {
color_yellow
} else {
color_green
};
let badge = row![ let badge = row![
text("").size(9).color(dot_color), text("").size(9).color(dot_color),
text(conn_badge_label(info)).size(11).color(color_subtext), text(conn_badge_label(info)).size(11).color(color_subtext),
@@ -6248,7 +6252,9 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
.align_y(iced::alignment::Vertical::Center); .align_y(iced::alignment::Vertical::Center);
let detail = column![ let detail = column![
text(conn_tooltip_path(info)).size(11).color(color_text), text(conn_tooltip_path(info)).size(11).color(color_text),
text(conn_loss_label(info.loss_pct)).size(11).color(color_subtext), text(conn_loss_label(info.loss_pct))
.size(11)
.color(color_subtext),
text(format!( text(format!(
"↑ {} ↓ {}", "↑ {} ↓ {}",
conn_rate_label(info.up_kbps), conn_rate_label(info.up_kbps),
@@ -6261,9 +6267,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
name_col = name_col.push( name_col = name_col.push(
tooltip( tooltip(
badge, badge,
container(detail) container(detail).padding(8).style(c_style(
.padding(8) color_crust,
.style(c_style(color_crust, color_surface, 6.0)), color_surface,
6.0,
)),
iced::widget::tooltip::Position::Bottom, iced::widget::tooltip::Position::Bottom,
) )
.gap(6), .gap(6),
+244 -28
View File
@@ -888,6 +888,103 @@ async fn build_net_stack(
}) })
} }
/// Retry policy for a live net-stack replacement, generic over the builder so
/// it is unit-testable without binding sockets: build for `requested`; if that
/// fails, build for `live` (the posture the old stack was actually running) so
/// a bad posture change degrades to the previous posture instead of leaving no
/// stack at all. When `requested == live` the second attempt is a plain retry.
///
/// `Ok((stack, mode, primary_err))` — a stack is up on `mode`; `primary_err`
/// is `Some` when the first attempt failed. `Err((primary, fallback))` — both
/// attempts failed and networking is gone.
async fn rebuild_with_fallback<T, E, F, Fut>(
mut build: F,
requested: NetworkMode,
live: NetworkMode,
) -> Result<(T, NetworkMode, Option<E>), (E, E)>
where
F: FnMut(NetworkMode) -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
{
match build(requested).await {
Ok(stack) => Ok((stack, requested, None)),
Err(primary) => match build(live).await {
Ok(stack) => Ok((stack, live, Some(primary))),
Err(fallback) => Err((primary, fallback)),
},
}
}
/// Tear down `old` and stand up a replacement stack for `requested_mode`.
///
/// A build failure here is rare (only the local socket bind can fail; the
/// relay handshake is backgrounded), but it used to propagate straight out of
/// `run_core_loop` with no `UiEvent`, silently killing every future command —
/// the app looked alive and did nothing. Instead, fall back to `live_mode`
/// via `rebuild_with_fallback`, tell the UI when the requested change did not
/// stick, and return the mode the new stack actually runs so the caller can
/// keep its state honest. `Err` only when both builds fail: networking is
/// gone (already reported to the UI as fatal) and the caller should exit.
#[allow(clippy::too_many_arguments)]
async fn replace_net_stack(
old: NetStack,
what: &str,
secret_key: &SecretKey,
requested_mode: NetworkMode,
live_mode: NetworkMode,
friends_handler: &crate::presence_net::Handler,
publish: bool,
ui_tx: &mpsc::Sender<UiEvent>,
) -> Result<(NetStack, NetworkMode), anyhow::Error> {
let lookup = old.memory_lookup.clone();
old.shutdown().await;
let outcome = rebuild_with_fallback(
|mode| {
build_net_stack(
secret_key.clone(),
mode,
lookup.clone(),
friends_handler.clone(),
publish,
)
},
requested_mode,
live_mode,
)
.await;
match outcome {
Ok((stack, mode, None)) => Ok((stack, mode)),
Ok((stack, mode, Some(primary))) => {
if mode == requested_mode {
// Same-posture retry succeeded — everything the user asked for
// is in effect, so log it rather than raising a UI error.
crate::log_msg(&format!(
"{what}: net stack build failed once ({primary:#}); retry succeeded"
));
} else {
let _ = ui_tx
.send(UiEvent::Error(format!(
"{what} failed ({primary:#}); staying on the previous \
network mode for this session"
)))
.await;
}
Ok((stack, mode))
}
Err((primary, fallback)) => {
let _ = ui_tx
.send(UiEvent::Error(format!(
"Networking lost: {primary:#} (recovery attempt also failed: \
{fallback:#}). Restart PeerSpeak to reconnect."
)))
.await;
Err(anyhow::anyhow!(
"net stack rebuild failed: {primary:#}; fallback: {fallback:#}"
))
}
}
}
/// Maximum number of *automatic* chat-attachment fetches in flight at once. /// Maximum number of *automatic* chat-attachment fetches in flight at once.
/// ///
/// Auto-fetch (inline image preview) is triggered by an untrusted peer's chat /// Auto-fetch (inline image preview) is triggered by an untrusted peer's chat
@@ -1335,6 +1432,10 @@ async fn run_core_loop(
// rebuilt on the next Leave (or before the next Join), preserving the old // rebuilt on the next Leave (or before the next Join), preserving the old
// "applies on next join" semantics while keeping the endpoint up while idle. // "applies on next join" semantics while keeping the endpoint up while idle.
let mut net_rebuild_pending = false; let mut net_rebuild_pending = false;
// The posture the live stack was actually built with. Trails `network_mode`
// while a rebuild is pending, and is the fallback posture when a rebuild
// fails (see `replace_net_stack`).
let mut net_mode = network_mode;
// When Discoverable is on, the instant it auto-reverts to Normal (W7 P6 time-box). // When Discoverable is on, the instant it auto-reverts to Normal (W7 P6 time-box).
// `None` = not Discoverable, no pending revert. Set on SetPresenceMode(Discoverable), // `None` = not Discoverable, no pending revert. Set on SetPresenceMode(Discoverable),
@@ -1552,17 +1653,24 @@ async fn run_core_loop(
// active, rebuild the persistent stack now — after the old session is // active, rebuild the persistent stack now — after the old session is
// gone, before the new one binds — so this join uses the new posture. // gone, before the new one binds — so this join uses the new posture.
if net_rebuild_pending { if net_rebuild_pending {
let lookup = net.memory_lookup.clone();
net.shutdown().await;
let publish = presence_mode.lock().unwrap().publishes_to_discovery(); let publish = presence_mode.lock().unwrap().publishes_to_discovery();
net = build_net_stack( let (stack, live) = replace_net_stack(
secret_key.clone(), net,
"Applying deferred network settings",
&secret_key,
network_mode, network_mode,
lookup, net_mode,
friends_handler.clone(), &friends_handler,
publish, publish,
&ui_tx,
) )
.await?; .await?;
net = stack;
net_mode = live;
// If the new posture failed and we fell back, keep the mode
// state honest (and re-attemptable) rather than pretending
// the change applied. The join proceeds on the live stack.
network_mode = live;
net_rebuild_pending = false; net_rebuild_pending = false;
} }
@@ -2594,17 +2702,21 @@ async fn run_core_loop(
// Apply any network-mode / identity change that was deferred while we // Apply any network-mode / identity change that was deferred while we
// were in the call (rebuild while idle keeps the endpoint reachable). // were in the call (rebuild while idle keeps the endpoint reachable).
if net_rebuild_pending { if net_rebuild_pending {
let lookup = net.memory_lookup.clone();
net.shutdown().await;
let publish = presence_mode.lock().unwrap().publishes_to_discovery(); let publish = presence_mode.lock().unwrap().publishes_to_discovery();
net = build_net_stack( let (stack, live) = replace_net_stack(
secret_key.clone(), net,
"Applying deferred network settings",
&secret_key,
network_mode, network_mode,
lookup, net_mode,
friends_handler.clone(), &friends_handler,
publish, publish,
&ui_tx,
) )
.await?; .await?;
net = stack;
net_mode = live;
network_mode = live;
net_rebuild_pending = false; net_rebuild_pending = false;
} }
} }
@@ -2750,17 +2862,21 @@ async fn run_core_loop(
// idle; if a call is active, defer to the next Leave/Join so the // idle; if a call is active, defer to the next Leave/Join so the
// live call isn't disrupted (preserves "applies on next join"). // live call isn't disrupted (preserves "applies on next join").
if active_session.is_none() { if active_session.is_none() {
let lookup = net.memory_lookup.clone();
net.shutdown().await;
let publish = presence_mode.lock().unwrap().publishes_to_discovery(); let publish = presence_mode.lock().unwrap().publishes_to_discovery();
net = build_net_stack( let (stack, live) = replace_net_stack(
secret_key.clone(), net,
"Network mode change",
&secret_key,
network_mode, network_mode,
lookup, net_mode,
friends_handler.clone(), &friends_handler,
publish, publish,
&ui_tx,
) )
.await?; .await?;
net = stack;
net_mode = live;
network_mode = live;
} else { } else {
net_rebuild_pending = true; net_rebuild_pending = true;
} }
@@ -2798,17 +2914,22 @@ async fn run_core_loop(
// key unchanged, so a rebuild would be pointless churn). // key unchanged, so a rebuild would be pointless churn).
if regenerated { if regenerated {
if active_session.is_none() { if active_session.is_none() {
let lookup = net.memory_lookup.clone();
net.shutdown().await;
let publish = presence_mode.lock().unwrap().publishes_to_discovery(); let publish = presence_mode.lock().unwrap().publishes_to_discovery();
net = build_net_stack( // Same mode both attempts — the fallback is a plain
secret_key.clone(), // retry under the (already persisted) new key.
let (stack, live) = replace_net_stack(
net,
"Endpoint restart after identity change",
&secret_key,
network_mode, network_mode,
lookup, net_mode,
friends_handler.clone(), &friends_handler,
publish, publish,
&ui_tx,
) )
.await?; .await?;
net = stack;
net_mode = live;
} else { } else {
net_rebuild_pending = true; net_rebuild_pending = true;
} }
@@ -3322,10 +3443,10 @@ fn replace_viewer_index<T>(viewers: &[(String, T)], ticket: &str) -> Option<usiz
mod tests { mod tests {
use super::{ use super::{
KnownPeers, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS, MIC_LEVEL_REPORT_SAMPLES, MicLevelMeter, KnownPeers, MAX_OPUS_PAYLOAD, MAX_RETAINED_PEERS, MIC_LEVEL_REPORT_SAMPLES, MicLevelMeter,
PLAYBACK_HANDOFF_QUEUE_FRAMES, PeerSpeakTicket, admit_retained, apply_peer_volume, NetworkMode, PLAYBACK_HANDOFF_QUEUE_FRAMES, PeerSpeakTicket, admit_retained,
apply_volume, audio_datagram_len_ok, coalesce_insert, coalesce_pop, frame_level, apply_peer_volume, apply_volume, audio_datagram_len_ok, coalesce_insert, coalesce_pop,
mix_frames, mix_stereo_frames, next_game_change, replace_viewer_index, send_playback_frame, frame_level, mix_frames, mix_stereo_frames, next_game_change, rebuild_with_fallback,
should_auto_fetch, stereo_to_mono, replace_viewer_index, send_playback_frame, should_auto_fetch, stereo_to_mono,
}; };
use crate::core::messages::{CoalesceKey, CoreCommand, coalesce_key}; use crate::core::messages::{CoalesceKey, CoreCommand, coalesce_key};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -3349,6 +3470,101 @@ mod tests {
assert_eq!(replace_viewer_index::<u8>(&[], "ticket-A"), None); assert_eq!(replace_viewer_index::<u8>(&[], "ticket-A"), None);
} }
// --- rebuild_with_fallback: the retry policy behind replace_net_stack ---
// The builder is injected, so these cover the policy without sockets. The
// closure does its bookkeeping synchronously and returns a ready future.
#[tokio::test]
async fn rebuild_keeps_requested_posture_on_first_success() {
let calls = std::cell::RefCell::new(Vec::new());
let out = rebuild_with_fallback(
|mode| {
calls.borrow_mut().push(mode);
std::future::ready(Ok::<u8, String>(7))
},
NetworkMode::DirectOnly,
NetworkMode::N0Full,
)
.await;
assert_eq!(out, Ok((7, NetworkMode::DirectOnly, None)));
// No second build: the live posture is only a fallback.
assert_eq!(*calls.borrow(), vec![NetworkMode::DirectOnly]);
}
#[tokio::test]
async fn rebuild_falls_back_to_the_live_posture_when_the_requested_one_fails() {
let calls = std::cell::RefCell::new(Vec::new());
let out = rebuild_with_fallback(
|mode| {
calls.borrow_mut().push(mode);
std::future::ready(if mode == NetworkMode::DirectOnly {
Err("bind failed".to_string())
} else {
Ok(7u8)
})
},
NetworkMode::DirectOnly,
NetworkMode::N0Full,
)
.await;
// A stack is up on the OLD posture and the caller learns both that it
// fell back (mode) and why (the primary error) — no silent zombie.
assert_eq!(
out,
Ok((7, NetworkMode::N0Full, Some("bind failed".to_string())))
);
assert_eq!(
*calls.borrow(),
vec![NetworkMode::DirectOnly, NetworkMode::N0Full]
);
}
#[tokio::test]
async fn rebuild_reports_both_errors_when_networking_is_gone() {
let out = rebuild_with_fallback(
|_| std::future::ready(Err::<u8, String>("bind failed".to_string())),
NetworkMode::DirectOnly,
NetworkMode::N0Full,
)
.await;
assert_eq!(
out,
Err(("bind failed".to_string(), "bind failed".to_string()))
);
}
#[tokio::test]
async fn rebuild_with_equal_postures_is_a_plain_retry() {
// RegenerateIdentity rebuilds under the same mode: the fallback is a
// second attempt with identical parameters, not a posture change.
let calls = std::cell::Cell::new(0u8);
let out = rebuild_with_fallback(
|mode| {
calls.set(calls.get() + 1);
assert_eq!(mode, NetworkMode::RelayNoDiscovery);
std::future::ready(if calls.get() == 1 {
Err("transient".to_string())
} else {
Ok(7u8)
})
},
NetworkMode::RelayNoDiscovery,
NetworkMode::RelayNoDiscovery,
)
.await;
// Succeeded on the requested posture, so the caller treats the change
// as applied (the Some(err) is logged, not surfaced as a UI error).
assert_eq!(
out,
Ok((
7,
NetworkMode::RelayNoDiscovery,
Some("transient".to_string())
))
);
assert_eq!(calls.get(), 2);
}
#[test] #[test]
fn admit_retained_rejects_only_new_ids_at_the_cap() { fn admit_retained_rejects_only_new_ids_at_the_cap() {
// Below the cap, a brand-new identity is retained. // Below the cap, a brand-new identity is retained.
+5 -1
View File
@@ -374,7 +374,11 @@ pub async fn spawn_host(
// Log the exact argv we hand pixelpass so a field log can confirm which // Log the exact argv we hand pixelpass so a field log can confirm which
// encode/quality flags (e.g. --bitrate) actually reached the host — these // encode/quality flags (e.g. --bitrate) actually reached the host — these
// are local flags with no ticket/secret, so logging them verbatim is safe. // are local flags with no ticket/secret, so logging them verbatim is safe.
crate::log_msg(&format!("pixelpass host spawn: {} {}", bin.display(), args.join(" "))); crate::log_msg(&format!(
"pixelpass host spawn: {} {}",
bin.display(),
args.join(" ")
));
let mut child = Command::new(bin) let mut child = Command::new(bin)
.args(&args) .args(&args)
.stdin(Stdio::null()) .stdin(Stdio::null())
+5 -1
View File
@@ -766,7 +766,11 @@ where
} }
} }
MenuAction::Paste => { MenuAction::Paste => {
let clip = sanitize_clip(&clipboard.read(clipboard::Kind::Standard).unwrap_or_default()); let clip = sanitize_clip(
&clipboard
.read(clipboard::Kind::Standard)
.unwrap_or_default(),
);
let edit = paste(self.value, start, end, &clip); let edit = paste(self.value, start, end, &clip);
self.publish_paste(edit, shell); self.publish_paste(edit, shell);
+3 -1
View File
@@ -304,7 +304,9 @@ async fn connection_stats_report_a_direct_path_with_live_counters() {
assert_eq!(info.remote_addr, s2.remote_addr); assert_eq!(info.remote_addr, s2.remote_addr);
assert!(info.rtt_ms < 1000, "localhost RTT should be sane"); assert!(info.rtt_ms < 1000, "localhost RTT should be sane");
assert!( assert!(
info.up_kbps.expect("same path + positive window has a rate") > 0.0, info.up_kbps
.expect("same path + positive window has a rate")
> 0.0,
"audio was flowing, so the upstream rate must be non-zero" "audio was flowing, so the upstream rate must be non-zero"
); );
} }