ci(lesavka): repair hygiene gate regressions

This commit is contained in:
Brad Stein 2026-06-05 02:02:17 -03:00
parent 3d32ee0922
commit 92b9aeecbd
16 changed files with 733 additions and 256 deletions

View File

@ -187,6 +187,10 @@ path = "tests/component/client/uplink/client_uplink_component_contract.rs"
name = "client_app_include_contract"
path = "tests/contract/client/app/client_app_include_contract.rs"
[[test]]
name = "client_app_support_contract"
path = "tests/contract/client/app/client_app_support_contract.rs"
[[test]]
name = "client_app_process_contract"
path = "tests/contract/client/app/client_app_process_contract.rs"

View File

@ -2,8 +2,7 @@ impl LesavkaClientApp {
/*──────────────── bundled webcam + mic stream ─────────────────*/
#[cfg(not(coverage))]
#[allow(clippy::too_many_arguments)]
/// Keeps `webcam_media_loop` explicit because it sits on the live uplink path, where stale media must be dropped instead of queued into latency.
/// Inputs are the typed parameters; output is the return value or side effect.
/// Run the live bundled uplink loop, dropping stale media instead of queueing latency.
async fn webcam_media_loop(
ep: Channel,
initial_camera_source: Option<String>,
@ -27,7 +26,8 @@ impl LesavkaClientApp {
continue;
}
let active_camera_source = state.camera_source.resolve(initial_camera_source.as_deref());
let active_camera_source =
state.camera_source.resolve(initial_camera_source.as_deref());
let active_camera_profile =
state.camera_profile.resolve(initial_camera_profile.as_deref());
let active_camera_cfg = camera_config_with_live_codec(camera_cfg, &state.camera_codec);
@ -279,9 +279,9 @@ impl LesavkaClientApp {
let desired_source = state
.microphone_source
.resolve(initial_microphone_source.as_deref());
let desired_audio_codec = state
.audio_codec
.resolve(lesavka_common::audio_transport::UpstreamAudioCodec::Opus);
let desired_audio_codec = state.audio_codec.resolve(
lesavka_common::audio_transport::UpstreamAudioCodec::Opus,
);
let desired_noise_suppression =
state.noise_suppression.resolve(false);
if state.camera != active_camera_requested

View File

@ -74,174 +74,3 @@ fn env_flag_enabled(name: &str) -> bool {
.map(|value| value.trim().to_ascii_lowercase())
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "yes" | "on"))
}
#[cfg(test)]
mod tests {
use super::{
DEFAULT_SERVER_ADDR, camera_config_from_caps, next_delay, resolve_server_addr,
sanitize_video_queue,
};
use crate::handshake::PeerCaps;
use crate::input::camera::CameraCodec;
use serial_test::serial;
use std::time::Duration;
#[test]
fn resolve_server_addr_prefers_cli_then_env_then_default() {
assert_eq!(
resolve_server_addr(&[String::from("http://cli:1")], Some("http://env:2")),
"http://cli:1"
);
assert_eq!(
resolve_server_addr(
&[
String::from("--no-launcher"),
String::from("--server"),
String::from("http://cli-flag:3"),
],
Some("http://env:2"),
),
"http://cli-flag:3"
);
assert_eq!(
resolve_server_addr(&[String::from("--launcher")], Some("http://env:2")),
"http://env:2"
);
assert_eq!(
resolve_server_addr(&[], Some("http://env:2")),
"http://env:2"
);
assert_eq!(resolve_server_addr(&[], None), DEFAULT_SERVER_ADDR);
}
#[test]
#[serial]
fn camera_config_from_caps_requires_complete_profile() {
temp_env::with_var("LESAVKA_CAM_CODEC", None::<&str>, || {
let mut caps = PeerCaps {
camera: true,
microphone: false,
bundled_webcam_media: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(String::from("mjpeg")),
camera_width: Some(1280),
camera_height: Some(720),
camera_fps: Some(25),
eye_width: None,
eye_height: None,
eye_fps: None,
};
let config = camera_config_from_caps(&caps).expect("complete caps should map");
assert!(matches!(config.codec, CameraCodec::Mjpeg));
assert_eq!(config.width, 1280);
caps.camera_codec = Some(String::from("h265"));
let config = camera_config_from_caps(&caps).expect("h265 alias should map");
assert!(matches!(config.codec, CameraCodec::Hevc));
caps.camera_codec = Some(String::from("vp9"));
assert!(camera_config_from_caps(&caps).is_none());
});
}
#[test]
#[serial]
fn camera_config_from_caps_uses_negotiated_codec_over_launcher_default() {
let caps = PeerCaps {
camera: true,
microphone: false,
bundled_webcam_media: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(String::from("hevc")),
camera_width: Some(1280),
camera_height: Some(720),
camera_fps: Some(25),
eye_width: None,
eye_height: None,
eye_fps: None,
};
temp_env::with_var("LESAVKA_CAM_CODEC", Some("mjpeg"), || {
let config = camera_config_from_caps(&caps).expect("caps codec should map");
assert!(matches!(config.codec, CameraCodec::Hevc));
});
}
#[test]
#[serial]
fn camera_config_from_caps_allows_explicit_forced_codec_override() {
let caps = PeerCaps {
camera: true,
microphone: false,
bundled_webcam_media: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(String::from("hevc")),
camera_width: Some(1280),
camera_height: Some(720),
camera_fps: Some(25),
eye_width: None,
eye_height: None,
eye_fps: None,
};
temp_env::with_vars(
[
("LESAVKA_CAM_CODEC", Some("mjpeg")),
("LESAVKA_CAM_CODEC_FORCE", Some("1")),
],
|| {
let config = camera_config_from_caps(&caps).expect("forced override should map");
assert!(matches!(config.codec, CameraCodec::Mjpeg));
},
);
}
#[test]
#[serial]
fn camera_config_force_flag_ignores_unknown_override() {
let caps = PeerCaps {
camera: true,
microphone: false,
bundled_webcam_media: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(String::from("hevc")),
camera_width: Some(1280),
camera_height: Some(720),
camera_fps: Some(25),
eye_width: None,
eye_height: None,
eye_fps: None,
};
temp_env::with_vars(
[
("LESAVKA_CAM_CODEC", Some("vp9")),
("LESAVKA_CAM_CODEC_FORCE", Some("1")),
],
|| {
let config =
camera_config_from_caps(&caps).expect("bad forced override should fall back");
assert!(matches!(config.codec, CameraCodec::Hevc));
},
);
}
#[test]
fn sanitize_video_queue_enforces_floor() {
assert_eq!(sanitize_video_queue(None), 8);
assert_eq!(sanitize_video_queue(Some(1)), 4);
assert_eq!(sanitize_video_queue(Some(32)), 32);
}
#[test]
fn next_delay_doubles_until_capped() {
assert_eq!(next_delay(Duration::from_secs(1)), Duration::from_secs(2));
assert_eq!(next_delay(Duration::from_secs(15)), Duration::from_secs(30));
assert_eq!(next_delay(Duration::from_secs(31)), Duration::from_secs(30));
}
}

View File

@ -12,14 +12,7 @@ impl LauncherState {
}
pub fn set_server_version(&mut self, version: Option<String>) {
self.server_version = version.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
self.server_version = normalize_optional_text(version);
}
pub fn set_server_media_caps(
@ -31,22 +24,8 @@ impl LauncherState {
) {
self.server_camera = camera;
self.server_microphone = microphone;
self.server_camera_output = camera_output.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
self.server_camera_codec = camera_codec.and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
self.server_camera_output = normalize_optional_text(camera_output);
self.server_camera_codec = normalize_optional_text(camera_codec);
}
pub fn set_view_mode(&mut self, view_mode: ViewMode) {
@ -481,3 +460,10 @@ impl LauncherState {
self.upstream_sync = upstream_sync;
}
}
fn normalize_optional_text(value: Option<String>) -> Option<String> {
value.and_then(|value| {
let trimmed = value.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
})
}

View File

@ -340,6 +340,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
| `LESAVKA_UVC_EXTERNAL` | server hardware/device override |
| `LESAVKA_UVC_FALLBACK` | server hardware/device override |
| `LESAVKA_UVC_FPS` | server hardware/device override |
| `LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH` | UVC helper boundary-audit runtime control path; defaults to `/tmp/lesavka-uvc-frame-audit.control`, where a path value enables exact-frame audit capture and `off`/`0` disables it without restarting the server |
| `LESAVKA_UVC_FRAME_AUDIT_DIR` | UVC helper boundary-audit directory; when set, the server saves exact MJPEG frames published to the UVC helper plus a JSONL index so recordings can be compared against pre-UVC payloads |
| `LESAVKA_UVC_FRAME_AUDIT_EVERY` | UVC helper boundary-audit sampling interval; saves every Nth spooled frame, defaults to `1` for short repros |
| `LESAVKA_UVC_FRAME_AUDIT_LOG_PATH` | UVC helper boundary-audit JSONL path override; defaults to `spool-audit.jsonl` inside `LESAVKA_UVC_FRAME_AUDIT_DIR` |

View File

@ -68,12 +68,12 @@
"client/src/app/uplink_media/webcam_media_loop.rs": {
"clippy_warnings": 0,
"doc_debt": 3,
"loc": 476
"loc": 492
},
"client/src/app_support.rs": {
"clippy_warnings": 0,
"doc_debt": 3,
"loc": 174
"doc_debt": 0,
"loc": 76
},
"client/src/bin/lesavka-relayctl.rs": {
"clippy_warnings": 0,
@ -362,8 +362,8 @@
},
"client/src/launcher/state/launcher_state_impl.rs": {
"clippy_warnings": 0,
"doc_debt": 17,
"loc": 475
"doc_debt": 16,
"loc": 469
},
"client/src/launcher/state/launcher_status_line.rs": {
"clippy_warnings": 0,
@ -1328,32 +1328,32 @@
"server/src/video_sinks/mjpeg_spool.rs": {
"clippy_warnings": 0,
"doc_debt": 4,
"loc": 443
"loc": 468
},
"server/src/video_sinks/mjpeg_spool/audit.rs": {
"clippy_warnings": 0,
"doc_debt": 0,
"loc": 287
"loc": 338
},
"server/src/video_sinks/mjpeg_spool/tests.rs": {
"clippy_warnings": 0,
"doc_debt": 2,
"loc": 439
"loc": 489
},
"server/src/video_sinks/webcam_sink.rs": {
"clippy_warnings": 0,
"doc_debt": 7,
"loc": 479
"loc": 480
},
"server/src/video_sinks/webcam_sink/constructor.rs": {
"clippy_warnings": 0,
"doc_debt": 1,
"loc": 382
"loc": 384
},
"server/src/video_sinks/webcam_sink/frame_handoff.rs": {
"clippy_warnings": 0,
"doc_debt": 4,
"loc": 357
"loc": 364
},
"server/src/video_sinks/webcam_sink/tests.rs": {
"clippy_warnings": 0,

View File

@ -22,6 +22,7 @@ HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC = 8000
DEFAULT_ISOCHRONOUS_LIMIT_PCT = 85
DEFAULT_UVC_MAX_PACKET = 1024
DEFAULT_MEDIA_CONTROL_PATH = "/tmp/lesavka-media.control"
DEFAULT_SERVER_UVC_AUDIT_CONTROL_PATH = "/tmp/lesavka-uvc-frame-audit.control"
MARKER_BITS = 32
MARKER_COLUMNS = 16
CADENCE_REASONS = {"frame_repeat", "frame_gap", "frame_backwards"}
@ -235,6 +236,32 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--max-reference-artifacts", type=int, default=12)
parser.add_argument("--reference-every", type=int, default=900)
parser.add_argument("--progress-every", type=int, default=150)
parser.add_argument(
"--server-uvc-audit",
action="store_true",
help="enable exact server-side UVC-bound MJPEG audit evidence for this run",
)
parser.add_argument(
"--server-uvc-audit-host",
default="",
help="SSH host running the Lesavka server; defaults to --inject-host when set",
)
parser.add_argument(
"--server-uvc-audit-control-path",
default=DEFAULT_SERVER_UVC_AUDIT_CONTROL_PATH,
help="runtime control file read by the server to enable UVC-bound frame auditing",
)
parser.add_argument(
"--server-uvc-audit-dir",
default="",
help="remote audit directory; default uses a unique /tmp path on the server host",
)
parser.add_argument(
"--server-uvc-audit-sample-frames",
type=int,
default=30,
help="number of audited MJPEG frames to copy/decode for boundary classification",
)
parser.add_argument(
"--stream-analyze",
action="store_true",
@ -417,6 +444,263 @@ def restore_remote_live_upstream(host: str, state: dict[str, Any]) -> None:
)
def resolve_server_uvc_audit_host(args: argparse.Namespace) -> str:
if args.server_uvc_audit_host:
return args.server_uvc_audit_host
if args.inject_host:
return args.inject_host
return ""
def setup_server_uvc_audit(args: argparse.Namespace, artifact_stamp: str) -> tuple[str, str] | None:
if not args.server_uvc_audit:
return None
host = resolve_server_uvc_audit_host(args)
if not host:
raise SystemExit("--server-uvc-audit requires --server-uvc-audit-host when --local-inject is used")
remote_dir = args.server_uvc_audit_dir or f"/tmp/lesavka-synthetic-rct-uvc-audit-{artifact_stamp}"
command = (
f"rm -rf {shlex.quote(remote_dir)} && "
f"mkdir -p {shlex.quote(remote_dir)} && "
f"printf '%s\\n' {shlex.quote(remote_dir)} > {shlex.quote(args.server_uvc_audit_control_path)}"
)
subprocess.run(["ssh", host, command], check=True)
print(
f"enabled server UVC-bound frame audit on {host}: {remote_dir}",
file=sys.stderr,
)
return host, remote_dir
def cleanup_server_uvc_audit(args: argparse.Namespace, state: tuple[str, str] | None) -> None:
if state is None:
return
host, _remote_dir = state
command = f"rm -f {shlex.quote(args.server_uvc_audit_control_path)}"
subprocess.run(["ssh", host, command], check=False)
print(
f"disabled server UVC-bound frame audit on {host}",
file=sys.stderr,
)
def read_jsonl(path: pathlib.Path) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
if not path.exists():
return records
for line in path.read_text(errors="replace").splitlines():
try:
value = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(value, dict):
records.append(value)
return records
def sample_records(records: list[dict[str, Any]], limit: int) -> list[dict[str, Any]]:
if limit <= 0 or len(records) <= limit:
return records
if limit == 1:
return [records[-1]]
indexes = {
round(idx * (len(records) - 1) / (limit - 1))
for idx in range(limit)
}
return [records[idx] for idx in sorted(indexes)]
def copy_server_uvc_audit(
args: argparse.Namespace,
state: tuple[str, str] | None,
local_dir: pathlib.Path,
) -> pathlib.Path | None:
if state is None:
return None
host, remote_dir = state
local_dir.mkdir(parents=True, exist_ok=True)
remote_log = f"{remote_dir.rstrip('/')}/spool-audit.jsonl"
local_log = local_dir / "spool-audit.jsonl"
subprocess.run(["scp", f"{host}:{remote_log}", str(local_log)], check=False)
records = read_jsonl(local_log)
for record in sample_records(records, args.server_uvc_audit_sample_frames):
frame_file = str(record.get("file") or "")
if not frame_file or "/" in frame_file:
continue
subprocess.run(
[
"scp",
f"{host}:{remote_dir.rstrip('/')}/{frame_file}",
str(local_dir / frame_file),
],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return local_dir
def decode_mjpeg_to_gray(path: pathlib.Path, width: int, height: int) -> bytes | None:
if width <= 0 or height <= 0 or not path.exists():
return None
proc = subprocess.run(
[
"ffmpeg",
"-hide_banner",
"-loglevel",
"error",
"-i",
str(path),
"-an",
"-pix_fmt",
"gray",
"-f",
"rawvideo",
"-",
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
)
expected = width * height
if proc.returncode != 0 or len(proc.stdout) < expected:
return None
return proc.stdout[:expected]
def summarize_server_uvc_audit(
local_dir: pathlib.Path | None,
mode_width: int,
mode_height: int,
mode_fps: int,
capture_data: dict[str, Any] | None,
args: argparse.Namespace,
) -> dict[str, Any] | None:
if local_dir is None:
return None
log_path = local_dir / "spool-audit.jsonl"
records = read_jsonl(log_path)
frame_size_counts: collections.Counter[str] = collections.Counter()
uvc_mode_counts: collections.Counter[str] = collections.Counter()
complete_count = 0
rejected_count = 0
decoded_sample_count = 0
marker_sample_count = 0
visual_sample_count = 0
sample_reason_counts: collections.Counter[str] = collections.Counter()
previous_seq: int | None = None
for record in records:
width = record.get("frame_width")
height = record.get("frame_height")
frame_size_counts[f"{width}x{height}"] += 1
uvc_width = record.get("uvc_width")
uvc_height = record.get("uvc_height")
uvc_fps = record.get("uvc_fps")
uvc_mode_counts[f"{uvc_width}x{uvc_height}@{uvc_fps}"] += 1
complete_count += int(bool(record.get("jpeg_complete")))
rejected_count += int(bool(record.get("rejected")))
for record in sample_records(records, args.server_uvc_audit_sample_frames):
frame_file = str(record.get("file") or "")
width = int(record.get("frame_width") or 0)
height = int(record.get("frame_height") or 0)
if not frame_file or width <= 0 or height <= 0:
continue
raw = decode_mjpeg_to_gray(local_dir / frame_file, width, height)
if raw is None:
continue
decoded_sample_count += 1
result = analyze_frame(raw, width, height, args, previous_seq)
comparison_seq = result.get("comparison_sequence")
if comparison_seq is not None:
previous_seq = int(comparison_seq)
if result.get("decoded_sequence") is not None:
marker_sample_count += 1
if result.get("visual_suspicious"):
visual_sample_count += 1
sample_reason_counts.update(result.get("visual_reasons") or [])
matching_records = [
record
for record in records
if int(record.get("frame_width") or 0) == mode_width
and int(record.get("frame_height") or 0) == mode_height
]
matching_uvc_records = [
record
for record in records
if int(record.get("uvc_width") or 0) == mode_width
and int(record.get("uvc_height") or 0) == mode_height
and int(record.get("uvc_fps") or 0) == mode_fps
]
capture_visual_frames = int((capture_data or {}).get("visual_suspicious_frames") or 0)
capture_frames = int((capture_data or {}).get("frames") or 0)
status = "inconclusive"
diagnosis: list[str] = []
if not records:
status = "server_boundary_missing"
diagnosis.append(
"server UVC-bound audit recorded no frames; the software output path did not prove it produced fresh webcam frames during the probe"
)
elif rejected_count and rejected_count == len(records):
status = "server_boundary_rejected"
diagnosis.append(
"every audited UVC-bound frame was rejected before handoff; corruption or profile trouble is before the browser-facing UVC path"
)
elif complete_count < len(records):
status = "server_boundary_incomplete_jpeg"
diagnosis.append(
"the server UVC-bound audit contains incomplete JPEG payloads, so corruption exists before or at the server handoff"
)
elif not matching_records:
status = "server_boundary_frame_mode_mismatch"
diagnosis.append(
f"server UVC-bound frames did not match requested {mode_width}x{mode_height}; observed frame sizes {dict(frame_size_counts)}"
)
elif not matching_uvc_records:
status = "server_boundary_uvc_mode_mismatch"
diagnosis.append(
f"server UVC-bound records did not advertise requested {mode_width}x{mode_height}@{mode_fps}; observed UVC modes {dict(uvc_mode_counts)}"
)
elif visual_sample_count:
status = "server_boundary_visual_corruption"
diagnosis.append(
"decoded server UVC-bound audit samples were already visually suspicious before reaching the host/browser"
)
elif capture_visual_frames:
status = "downstream_uvc_or_browser_corruption"
diagnosis.append(
"server UVC-bound samples were clean and mode-matched, but receiver capture showed visual corruption; the software UVC gadget/browser leg is implicated"
)
elif capture_frames:
status = "no_visual_corruption_observed"
diagnosis.append(
"server UVC-bound samples and receiver capture had no visual corruption in this run"
)
summary = {
"schema": "lesavka.server-uvc-boundary-summary.v1",
"status": status,
"diagnosis": diagnosis,
"record_count": len(records),
"complete_count": complete_count,
"rejected_count": rejected_count,
"frame_size_counts": dict(frame_size_counts),
"uvc_mode_counts": dict(uvc_mode_counts),
"matching_frame_records": len(matching_records),
"matching_uvc_mode_records": len(matching_uvc_records),
"decoded_sample_count": decoded_sample_count,
"marker_sample_count": marker_sample_count,
"visual_sample_count": visual_sample_count,
"sample_visual_reason_counts": dict(sample_reason_counts),
"artifact_dir": str(local_dir),
"log_path": str(log_path),
}
(local_dir / "boundary-summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
return summary
def run_remote_orchestrated(args: argparse.Namespace) -> int:
if (not args.inject_host and not args.local_inject) or not args.rct_host:
raise SystemExit(
@ -425,11 +709,16 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
if not shutil.which("ssh") or not shutil.which("scp"):
raise SystemExit("ssh and scp are required for the remote synthetic probe")
width, height, fps = mode_dimensions(args)
run_stamp = timestamp()
inject_max_frame_bytes = args.inject_max_frame_bytes or default_inject_max_frame_bytes(fps)
artifact_dir = pathlib.Path(args.artifact_dir) if args.artifact_dir else default_artifact_dir(args.mode)
artifact_dir = (
pathlib.Path(args.artifact_dir)
if args.artifact_dir
else pathlib.Path("artifacts/synthetic-rct") / f"{args.mode.replace('@', '-').replace('x', 'x')}-{run_stamp}"
)
artifact_dir.mkdir(parents=True, exist_ok=True)
remote_rct_dir = args.remote_rct_dir or f"/tmp/lesavka-synthetic-rct-capture-{timestamp()}"
remote_inject_dir = args.remote_inject_dir or f"/tmp/lesavka-synthetic-uplink-{timestamp()}"
remote_rct_dir = args.remote_rct_dir or f"/tmp/lesavka-synthetic-rct-capture-{run_stamp}"
remote_inject_dir = args.remote_inject_dir or f"/tmp/lesavka-synthetic-uplink-{run_stamp}"
remote_script = f"/tmp/lesavka-synthetic-rct-probe-{os.getpid()}.py"
script_text = pathlib.Path(__file__).read_text()
subprocess.run(
@ -537,6 +826,10 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
"rct_host": args.rct_host,
"pause_local_live_upstream": args.pause_local_live_upstream,
"media_control_path": args.media_control_path,
"server_uvc_audit": args.server_uvc_audit,
"server_uvc_audit_host": resolve_server_uvc_audit_host(args),
"server_uvc_audit_control_path": args.server_uvc_audit_control_path,
"server_uvc_audit_sample_frames": args.server_uvc_audit_sample_frames,
},
indent=2,
sort_keys=True,
@ -598,7 +891,9 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
diagnosis: list[str] = []
paused_control: tuple[pathlib.Path, bytes | None] | None = None
paused_remote_control: tuple[str, dict[str, Any]] | None = None
server_audit_state: tuple[str, str] | None = None
try:
server_audit_state = setup_server_uvc_audit(args, run_stamp)
if args.pause_local_live_upstream:
if args.local_inject:
paused_control = pause_local_live_upstream(args)
@ -624,12 +919,14 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
capture = start_capture()
capture_rc, inject_rc = wait_capture_or_inject_exit(capture, inject)
finally:
cleanup_server_uvc_audit(args, server_audit_state)
if paused_remote_control is not None:
restore_remote_live_upstream(*paused_remote_control)
if paused_control is not None:
restore_local_live_upstream(*paused_control)
local_capture = artifact_dir / "capture"
local_inject = artifact_dir / "inject"
local_server_audit = artifact_dir / "server-uvc-audit"
if capture is not None:
subprocess.run(["scp", "-r", f"{args.rct_host}:{remote_rct_dir}", str(local_capture)], check=False)
if args.local_inject:
@ -639,7 +936,9 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
shutil.copytree(remote_inject_dir, local_inject)
else:
subprocess.run(["scp", "-r", f"{args.inject_host}:{remote_inject_dir}", str(local_inject)], check=False)
copied_server_audit = copy_server_uvc_audit(args, server_audit_state, local_server_audit)
capture_summary = local_capture / "summary.json"
capture_data: dict[str, Any] | None = None
if capture_summary.exists():
try:
capture_data = json.loads(capture_summary.read_text())
@ -695,6 +994,17 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
)
except Exception:
pass
server_boundary_summary = summarize_server_uvc_audit(
copied_server_audit,
width,
height,
fps,
capture_data,
args,
)
if server_boundary_summary:
for item in server_boundary_summary.get("diagnosis") or []:
diagnosis.append(str(item))
summary = {
"schema": "lesavka.synthetic-rct-probe.orchestrator.v1",
"mode": args.mode,
@ -704,6 +1014,8 @@ def run_remote_orchestrated(args: argparse.Namespace) -> int:
"artifact_dir": str(artifact_dir),
"capture_artifacts": str(local_capture),
"inject_artifacts": str(local_inject),
"server_uvc_boundary": server_boundary_summary,
"server_uvc_audit_artifacts": str(local_server_audit) if copied_server_audit else None,
}
(artifact_dir / "run-summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
print(json.dumps(summary, indent=2, sort_keys=True))

View File

@ -23,6 +23,9 @@ pub(super) struct MjpegSpoolTiming {
pub profile: &'static str,
pub source_pts_us: Option<u64>,
pub decoded_pts_us: Option<u64>,
pub uvc_width: Option<u16>,
pub uvc_height: Option<u16>,
pub uvc_fps: Option<u32>,
}
impl MjpegSpoolTiming {
@ -36,6 +39,9 @@ impl MjpegSpoolTiming {
profile: "mjpeg-passthrough",
source_pts_us: Some(source_pts_us),
decoded_pts_us: None,
uvc_width: None,
uvc_height: None,
uvc_fps: None,
}
}
@ -50,6 +56,9 @@ impl MjpegSpoolTiming {
profile: "mjpeg-normalized",
source_pts_us: Some(source_pts_us),
decoded_pts_us: None,
uvc_width: None,
uvc_height: None,
uvc_fps: None,
}
}
@ -64,6 +73,9 @@ impl MjpegSpoolTiming {
profile: "hevc-decoded-mjpeg",
source_pts_us: Some(source_pts_us),
decoded_pts_us,
uvc_width: None,
uvc_height: None,
uvc_fps: None,
}
}
@ -81,8 +93,18 @@ impl MjpegSpoolTiming {
profile: "hevc-decoded-mjpeg-rejected",
source_pts_us: Some(source_pts_us),
decoded_pts_us,
uvc_width: None,
uvc_height: None,
uvc_fps: None,
}
}
pub(super) fn with_uvc_mode(mut self, width: u16, height: u16, fps: u32) -> Self {
self.uvc_width = Some(width);
self.uvc_height = Some(height);
self.uvc_fps = Some(fps);
self
}
}
/// Decide whether the UVC helper file-spool path should own MJPEG emission.
@ -345,12 +367,15 @@ pub(super) fn format_mjpeg_spool_metadata(
timing: MjpegSpoolTiming,
) -> String {
format!(
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-meta.v1\",\"sequence\":{},\"profile\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"spool_unix_ns\":{}}}\n",
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-meta.v1\",\"sequence\":{},\"profile\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"uvc_width\":{},\"uvc_height\":{},\"uvc_fps\":{},\"spool_unix_ns\":{}}}\n",
sequence,
timing.profile,
bytes,
json_number_or_null(timing.source_pts_us),
json_number_or_null(timing.decoded_pts_us),
json_number_or_null(timing.uvc_width.map(u64::from)),
json_number_or_null(timing.uvc_height.map(u64::from)),
json_number_or_null(timing.uvc_fps.map(u64::from)),
unix_now_ns()
)
}

View File

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::super::hevc_mjpeg_guard;
use super::MjpegSpoolTiming;
static AUDIT_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
@ -11,6 +12,7 @@ static AUDIT_SAVED_FRAMES: AtomicU64 = AtomicU64::new(0);
const DEFAULT_UVC_FRAME_AUDIT_EVERY: u32 = 1;
const DEFAULT_UVC_FRAME_AUDIT_MAX_FRAMES: u32 = 1800;
const DEFAULT_UVC_FRAME_AUDIT_ROLLING: bool = true;
const DEFAULT_UVC_FRAME_AUDIT_CONTROL_PATH: &str = "/tmp/lesavka-uvc-frame-audit.control";
const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;
const FNV1A64_PRIME: u64 = 0x100000001b3;
@ -71,6 +73,30 @@ pub(super) fn mjpeg_spool_audit_dir() -> Option<PathBuf> {
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(mjpeg_spool_runtime_audit_dir)
}
/// Resolve the runtime control-file audit directory.
///
/// Inputs: `LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH` or the default control path.
/// Output: an audit directory, unless the file is absent or contains an off
/// value. Why: lab probes need to enable exact UVC-bound captures around one
/// run without restarting or making all installed calls write MJPEG dumps.
fn mjpeg_spool_runtime_audit_dir() -> Option<PathBuf> {
let control_path = std::env::var("LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(DEFAULT_UVC_FRAME_AUDIT_CONTROL_PATH));
let value = fs::read_to_string(control_path).ok()?;
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed.eq_ignore_ascii_case("0")
|| trimmed.eq_ignore_ascii_case("false")
|| trimmed.eq_ignore_ascii_case("no")
|| trimmed.eq_ignore_ascii_case("off")
{
return None;
}
Some(PathBuf::from(trimmed))
}
/// Resolve the UVC-bound audit sampling interval.
@ -189,14 +215,28 @@ fn format_audit_record(
let profile = timing.map(|value| value.profile).unwrap_or("unknown");
let source_pts_us = timing.and_then(|value| value.source_pts_us);
let decoded_pts_us = timing.and_then(|value| value.decoded_pts_us);
let uvc_width = timing.and_then(|value| value.uvc_width.map(u64::from));
let uvc_height = timing.and_then(|value| value.uvc_height.map(u64::from));
let uvc_fps = timing.and_then(|value| value.uvc_fps.map(u64::from));
let inspection = hevc_mjpeg_guard::inspect_mjpeg_frame(data);
format!(
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"slot\":{},\"profile\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"slot\":{},\"profile\":\"{}\",\"bytes\":{},\"jpeg_complete\":{},\"frame_width\":{},\"frame_height\":{},\"entropy_bytes\":{},\"entropy_distinct_bytes\":{},\"entropy_dominant_pct\":{},\"entropy_max_run\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"uvc_width\":{},\"uvc_height\":{},\"uvc_fps\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
sequence,
slot,
json_escape(profile),
data.len(),
inspection.complete,
json_number_or_null(inspection.width.map(u64::from)),
json_number_or_null(inspection.height.map(u64::from)),
inspection.entropy_bytes,
inspection.entropy_distinct_bytes,
inspection.entropy_dominant_pct,
inspection.entropy_max_run,
json_number_or_null(source_pts_us),
json_number_or_null(decoded_pts_us),
json_number_or_null(uvc_width),
json_number_or_null(uvc_height),
json_number_or_null(uvc_fps),
unix_now_ns(),
fnv1a64_hex(data),
json_escape(frame_file)
@ -218,15 +258,26 @@ fn format_rejected_audit_record(
) -> String {
let source_pts_us = timing.source_pts_us;
let decoded_pts_us = timing.decoded_pts_us;
let inspection = hevc_mjpeg_guard::inspect_mjpeg_frame(data);
format!(
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"slot\":{},\"profile\":\"{}\",\"rejected\":true,\"reason\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"slot\":{},\"profile\":\"{}\",\"rejected\":true,\"reason\":\"{}\",\"bytes\":{},\"jpeg_complete\":{},\"frame_width\":{},\"frame_height\":{},\"entropy_bytes\":{},\"entropy_distinct_bytes\":{},\"entropy_dominant_pct\":{},\"entropy_max_run\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"uvc_width\":{},\"uvc_height\":{},\"uvc_fps\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
sequence,
slot,
json_escape(timing.profile),
json_escape(reason),
data.len(),
inspection.complete,
json_number_or_null(inspection.width.map(u64::from)),
json_number_or_null(inspection.height.map(u64::from)),
inspection.entropy_bytes,
inspection.entropy_distinct_bytes,
inspection.entropy_dominant_pct,
inspection.entropy_max_run,
json_number_or_null(source_pts_us),
json_number_or_null(decoded_pts_us),
json_number_or_null(timing.uvc_width.map(u64::from)),
json_number_or_null(timing.uvc_height.map(u64::from)),
json_number_or_null(timing.uvc_fps.map(u64::from)),
unix_now_ns(),
fnv1a64_hex(data),
json_escape(frame_file)

View File

@ -184,6 +184,48 @@ fn mjpeg_spool_audit_knobs_are_opt_in_and_bounded() {
assert!(super::audit::should_sample_spool_audit_frame(4, 3));
}
/// Verifies runtime audit control can enable boundary evidence without a
/// server restart.
///
/// Input: a temporary control file containing an audit directory. Output:
/// audit dir resolution follows the control file when the env dir is unset.
/// Why: hardware/lab probes need to turn on exact UVC-bound frame evidence
/// around one run without making every installed call write MJPEG dumps.
#[test]
fn mjpeg_spool_audit_can_be_enabled_by_runtime_control_file() {
let dir = tempfile::tempdir().expect("tempdir");
let control = dir.path().join("uvc-audit.control");
let audit = dir.path().join("audit");
std::fs::write(&control, format!("{}\n", audit.display())).expect("write control");
temp_env::with_vars(
[
("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
(
"LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH",
Some(control.to_str().expect("utf8 path")),
),
],
|| {
assert_eq!(super::audit::mjpeg_spool_audit_dir(), Some(audit));
},
);
std::fs::write(&control, "off\n").expect("disable control");
temp_env::with_vars(
[
("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
(
"LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH",
Some(control.to_str().expect("utf8 path")),
),
],
|| {
assert_eq!(super::audit::mjpeg_spool_audit_dir(), None);
},
);
}
/// Verifies metadata records carry enough timing evidence for RCT analysis.
///
/// Input: HEVC-decoded spool timing. Output: JSON fields for source and
@ -350,7 +392,10 @@ fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
super::spool_mjpeg_frame_with_timing(
&frame,
b"audit-jpeg",
Some(super::MjpegSpoolTiming::mjpeg_passthrough(222)),
Some(
super::MjpegSpoolTiming::mjpeg_passthrough(222)
.with_uvc_mode(1280, 720, 30),
),
)
.expect("spool audited frame");
super::spool_mjpeg_frame_with_timing(
@ -377,6 +422,11 @@ fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
assert!(index.contains("\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\""));
assert!(index.contains("\"profile\":\"mjpeg-passthrough\""));
assert!(index.contains("\"source_pts_us\":222"));
assert!(index.contains("\"jpeg_complete\":false"));
assert!(index.contains("\"frame_width\":null"));
assert!(index.contains("\"uvc_width\":1280"));
assert!(index.contains("\"uvc_height\":720"));
assert!(index.contains("\"uvc_fps\":30"));
assert!(index.contains("\"file\":\"frame-"));
}

View File

@ -46,6 +46,7 @@ pub struct WebcamSink {
direct_mjpeg_max_bytes: usize,
uvc_width: u16,
uvc_height: u16,
uvc_fps: u32,
direct_mjpeg_profile_mismatch_seen: AtomicBool,
unexpected_mjpeg_in_hevc_seen: AtomicBool,
last_decoded_mjpeg_bytes: AtomicU64,

View File

@ -61,6 +61,7 @@ impl WebcamSink {
direct_mjpeg_max_bytes: mjpeg_spool_frame_max_bytes(cfg.fps),
uvc_width: cfg.width.min(u32::from(u16::MAX)) as u16,
uvc_height: cfg.height.min(u32::from(u16::MAX)) as u16,
uvc_fps: cfg.fps.max(1),
direct_mjpeg_profile_mismatch_seen: AtomicBool::new(false),
unexpected_mjpeg_in_hevc_seen: AtomicBool::new(false),
last_decoded_mjpeg_bytes: AtomicU64::new(0),
@ -368,6 +369,7 @@ impl WebcamSink {
direct_mjpeg_max_bytes: mjpeg_spool_frame_max_bytes(cfg.fps),
uvc_width: cfg.width.min(u32::from(u16::MAX)) as u16,
uvc_height: cfg.height.min(u32::from(u16::MAX)) as u16,
uvc_fps: cfg.fps.max(1),
direct_mjpeg_profile_mismatch_seen: AtomicBool::new(false),
unexpected_mjpeg_in_hevc_seen: AtomicBool::new(false),
last_decoded_mjpeg_bytes: AtomicU64::new(0),

View File

@ -107,14 +107,16 @@ impl WebcamSink {
{
self.decoded_mjpeg_miss_count.store(0, Ordering::Relaxed);
let decoded_pts_us = buffer.pts().map(|pts| pts.nseconds() / 1_000);
let timing = MjpegSpoolTiming::hevc_decoded_mjpeg(pkt.pts, decoded_pts_us);
let timing = MjpegSpoolTiming::hevc_decoded_mjpeg(pkt.pts, decoded_pts_us)
.with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
let previous_bytes = self.last_decoded_mjpeg_bytes.load(Ordering::Relaxed);
let decoded_bytes = map.as_slice().len();
if let Some(reason) =
hevc_mjpeg_guard::decoded_mjpeg_reject_reason(previous_bytes, map.as_slice())
{
let rejected_timing =
MjpegSpoolTiming::rejected_hevc_decoded_mjpeg(pkt.pts, decoded_pts_us);
MjpegSpoolTiming::rejected_hevc_decoded_mjpeg(pkt.pts, decoded_pts_us)
.with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
let reason_text = format!("{reason:?}");
super::mjpeg_spool::audit_rejected_mjpeg_frame(
map.as_slice(),
@ -218,7 +220,8 @@ impl WebcamSink {
#[cfg(not(coverage))]
fn spool_passthrough_direct_mjpeg_frame(&self, path: &Path, pkt: &VideoPacket) {
let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts);
let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts)
.with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
if let Err(err) = spool_mjpeg_frame_with_timing(path, &pkt.data, Some(timing)) {
warn!(target:"lesavka_server::video", %err, "failed to spool MJPEG frame for UVC helper");
} else {
@ -347,7 +350,8 @@ impl WebcamSink {
return;
}
let timing = MjpegSpoolTiming::mjpeg_normalized(pkt.pts);
let timing = MjpegSpoolTiming::mjpeg_normalized(pkt.pts)
.with_uvc_mode(self.uvc_width, self.uvc_height, self.uvc_fps);
if let Err(err) = spool_mjpeg_frame_with_timing(path, normalized, Some(timing)) {
warn!(target:"lesavka_server::video", %err, "failed to spool normalized direct MJPEG frame for UVC helper");
} else {

View File

@ -0,0 +1,173 @@
// Integration coverage for client app support helpers.
//
// Scope: exercise server-address, camera-capability, and retry-delay helpers
// against deterministic local types.
// Targets: `client/src/app_support.rs`.
// Why: these startup decisions shape live camera codec negotiation and should
// stay covered without adding source-file LOC to the hygiene ratchet.
mod handshake {
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
pub bundled_webcam_media: bool,
pub server_version: Option<String>,
pub camera_output: Option<String>,
pub camera_codec: Option<String>,
pub camera_width: Option<u32>,
pub camera_height: Option<u32>,
pub camera_fps: Option<u32>,
pub eye_width: Option<u32>,
pub eye_height: Option<u32>,
pub eye_fps: Option<u32>,
}
}
mod input {
pub mod camera {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CameraCodec {
Mjpeg,
H264,
Hevc,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CameraConfig {
pub codec: CameraCodec,
pub width: u32,
pub height: u32,
pub fps: u32,
}
}
}
#[path = "../../../../client/src/app_support.rs"]
mod app_support;
use app_support::{
DEFAULT_SERVER_ADDR, camera_config_from_caps, next_delay, resolve_server_addr,
sanitize_video_queue,
};
use handshake::PeerCaps;
use input::camera::CameraCodec;
use serial_test::serial;
use std::time::Duration;
fn peer_caps(codec: &str) -> PeerCaps {
PeerCaps {
camera: true,
microphone: false,
bundled_webcam_media: false,
server_version: None,
camera_output: Some(String::from("uvc")),
camera_codec: Some(codec.to_string()),
camera_width: Some(1280),
camera_height: Some(720),
camera_fps: Some(25),
eye_width: None,
eye_height: None,
eye_fps: None,
}
}
#[test]
fn resolve_server_addr_prefers_cli_then_env_then_default() {
assert_eq!(
resolve_server_addr(&[String::from("http://cli:1")], Some("http://env:2")),
"http://cli:1"
);
assert_eq!(
resolve_server_addr(
&[
String::from("--no-launcher"),
String::from("--server"),
String::from("http://cli-flag:3"),
],
Some("http://env:2"),
),
"http://cli-flag:3"
);
assert_eq!(
resolve_server_addr(&[String::from("--launcher")], Some("http://env:2")),
"http://env:2"
);
assert_eq!(
resolve_server_addr(&[], Some("http://env:2")),
"http://env:2"
);
assert_eq!(resolve_server_addr(&[], None), DEFAULT_SERVER_ADDR);
}
#[test]
#[serial]
fn camera_config_from_caps_requires_complete_profile() {
temp_env::with_var("LESAVKA_CAM_CODEC", None::<&str>, || {
let mut caps = peer_caps("mjpeg");
let config = camera_config_from_caps(&caps).expect("complete caps should map");
assert!(matches!(config.codec, CameraCodec::Mjpeg));
assert_eq!(config.width, 1280);
caps.camera_codec = Some(String::from("h265"));
let config = camera_config_from_caps(&caps).expect("h265 alias should map");
assert!(matches!(config.codec, CameraCodec::Hevc));
caps.camera_codec = Some(String::from("vp9"));
assert!(camera_config_from_caps(&caps).is_none());
});
}
#[test]
#[serial]
fn camera_config_from_caps_uses_negotiated_codec_over_launcher_default() {
temp_env::with_var("LESAVKA_CAM_CODEC", Some("mjpeg"), || {
let config = camera_config_from_caps(&peer_caps("hevc")).expect("caps codec should map");
assert!(matches!(config.codec, CameraCodec::Hevc));
});
}
#[test]
#[serial]
fn camera_config_from_caps_allows_explicit_forced_codec_override() {
temp_env::with_vars(
[
("LESAVKA_CAM_CODEC", Some("mjpeg")),
("LESAVKA_CAM_CODEC_FORCE", Some("1")),
],
|| {
let config = camera_config_from_caps(&peer_caps("hevc")).expect("forced override");
assert!(matches!(config.codec, CameraCodec::Mjpeg));
},
);
}
#[test]
#[serial]
fn camera_config_force_flag_ignores_unknown_override() {
temp_env::with_vars(
[
("LESAVKA_CAM_CODEC", Some("vp9")),
("LESAVKA_CAM_CODEC_FORCE", Some("1")),
],
|| {
let config = camera_config_from_caps(&peer_caps("hevc")).expect("fallback codec");
assert!(matches!(config.codec, CameraCodec::Hevc));
},
);
}
#[test]
fn sanitize_video_queue_enforces_floor() {
assert_eq!(sanitize_video_queue(None), 8);
assert_eq!(sanitize_video_queue(Some(1)), 4);
assert_eq!(sanitize_video_queue(Some(32)), 32);
}
#[test]
fn next_delay_doubles_until_capped() {
assert_eq!(next_delay(Duration::from_secs(1)), Duration::from_secs(2));
assert_eq!(next_delay(Duration::from_secs(15)), Duration::from_secs(30));
assert_eq!(next_delay(Duration::from_secs(31)), Duration::from_secs(30));
}

View File

@ -10,8 +10,26 @@ use std::fs;
use serial_test::serial;
#[allow(dead_code, clippy::items_after_test_module)]
mod spool {
mod video_support {
pub fn env_u32(name: &str, default: u32) -> u32 {
std::env::var(name)
.ok()
.and_then(|value| value.trim().parse().ok())
.unwrap_or(default)
}
}
mod video_sinks {
#[allow(clippy::items_after_test_module)]
pub mod hevc_mjpeg_guard {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/video_sinks/hevc_mjpeg_guard.rs"
));
}
#[allow(dead_code, clippy::items_after_test_module)]
pub mod spool {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/server/src/video_sinks/mjpeg_spool.rs"
@ -44,7 +62,9 @@ mod spool {
Some(MjpegSpoolTiming::mjpeg_passthrough(source_pts_us)),
)
}
}
}
use video_sinks::spool;
#[test]
#[serial]

View File

@ -52,6 +52,11 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"--media-control-path",
"--jpeg-quality",
"--inject-max-frame-bytes",
"--server-uvc-audit",
"--server-uvc-audit-host",
"--server-uvc-audit-control-path",
"--server-uvc-audit-dir",
"--server-uvc-audit-sample-frames",
"--stream-analyze",
"--sequence-window",
"--mix-mae-threshold",
@ -94,6 +99,20 @@ fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
"lower_dominant_sequence",
"max_mixed_band_count",
"max_sequence_boundary_count",
"lesavka.server-uvc-boundary-summary.v1",
"server_uvc_boundary",
"server_uvc_audit_artifacts",
"server_boundary_missing",
"server_boundary_frame_mode_mismatch",
"server_boundary_uvc_mode_mismatch",
"server_boundary_visual_corruption",
"downstream_uvc_or_browser_corruption",
"no_visual_corruption_observed",
"spool-audit.jsonl",
"matching_frame_records",
"matching_uvc_mode_records",
"decoded_sample_count",
"marker_sample_count",
"diagnosis",
"encoded_oversize_frames",
"sent_frames",