176 lines
7.4 KiB
Rust
176 lines
7.4 KiB
Rust
//! Contract tests for the client-origin transport-to-RCT probe.
|
|
//!
|
|
//! Scope: statically guard the black-box transport harness and client timeline
|
|
//! artifacts used for client->server tuning.
|
|
//! Targets: `scripts/manual/run_client_to_rct_transport_probe.sh`,
|
|
//! `scripts/manual/client_rct_transport_summary.py`, and
|
|
//! `scripts/manual/client_rct_uvc_frame_meta_fetch.sh`, and
|
|
//! `client/src/sync_probe/`.
|
|
//! Why: this probe is the bridge between synthetic bundled client media and the
|
|
//! final RCT sync/freshness/smoothness evidence, so it must not drift into
|
|
//! split-stream or sudo-mutating behavior.
|
|
|
|
const CLIENT_RCT_SCRIPT: &str =
|
|
include_str!("../../scripts/manual/run_client_to_rct_transport_probe.sh");
|
|
const CLIENT_RCT_SUMMARY: &str =
|
|
include_str!("../../scripts/manual/client_rct_transport_summary.py");
|
|
const CLIENT_RCT_LAYERS: &str = include_str!("../../scripts/manual/client_rct_transport_layers.py");
|
|
const UVC_FRAME_META_FETCH: &str =
|
|
include_str!("../../scripts/manual/client_rct_uvc_frame_meta_fetch.sh");
|
|
const UVC_FRAME_META_SUMMARY: &str =
|
|
include_str!("../../scripts/manual/summarize_uvc_frame_meta_log.py");
|
|
const SYNC_PROBE_CONFIG: &str = include_str!("../../client/src/sync_probe/config.rs");
|
|
const SYNC_PROBE_RUNNER: &str = include_str!("../../client/src/sync_probe/runner.rs");
|
|
const SYNC_PROBE_BUNDLED_TRANSPORT: &str =
|
|
include_str!("../../client/src/sync_probe/runner/bundled_transport.rs");
|
|
const SYNC_PROBE_TIMELINE: &str = include_str!("../../client/src/sync_probe/timeline.rs");
|
|
|
|
#[test]
|
|
fn client_rct_probe_injects_synthetic_media_through_bundled_transport() {
|
|
for expected in [
|
|
"lesavka-sync-probe",
|
|
"--event-width-codes \"${PROBE_EVENT_WIDTH_CODES}\"",
|
|
"PROBE_EVENT_WIDTH_CODES=${PROBE_EVENT_WIDTH_CODES:-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}",
|
|
"--timeline-json \"${LOCAL_CLIENT_TIMELINE_JSON}\"",
|
|
"stream_webcam_media",
|
|
"UpstreamMediaBundle",
|
|
"server does not advertise bundled webcam media",
|
|
"refusing to measure split upstream",
|
|
] {
|
|
assert!(
|
|
CLIENT_RCT_SCRIPT.contains(expected)
|
|
|| SYNC_PROBE_RUNNER.contains(expected)
|
|
|| SYNC_PROBE_BUNDLED_TRANSPORT.contains(expected)
|
|
|| SYNC_PROBE_CONFIG.contains(expected),
|
|
"client-to-RCT transport probe should contain {expected}"
|
|
);
|
|
}
|
|
for forbidden in [
|
|
".stream_camera(Request::new(outbound))",
|
|
".stream_microphone(Request::new(outbound))",
|
|
] {
|
|
assert!(
|
|
!SYNC_PROBE_RUNNER.contains(forbidden)
|
|
&& !SYNC_PROBE_BUNDLED_TRANSPORT.contains(forbidden),
|
|
"synthetic transport probe must not use split upstream RPC {forbidden}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn client_rct_probe_preserves_black_box_rct_measurement_artifacts() {
|
|
for expected in [
|
|
"LOCAL_CAPTURE=\"${LOCAL_REPORT_DIR}/capture.mkv\"",
|
|
"LOCAL_REPORT_JSON=\"${LOCAL_REPORT_DIR}/report.json\"",
|
|
"LOCAL_EVENTS_CSV=\"${LOCAL_REPORT_DIR}/events.csv\"",
|
|
"LOCAL_CLIENT_TIMELINE_JSON=\"${LOCAL_REPORT_DIR}/client-transport-timeline.json\"",
|
|
"LOCAL_TRANSPORT_SUMMARY_JSON=\"${LOCAL_REPORT_DIR}/client-rct-transport-summary.json\"",
|
|
"LOCAL_RUN_LOG=\"${LOCAL_REPORT_DIR}/client-rct-run.log\"",
|
|
"capture_start_unix_ns=",
|
|
"lesavka-sync-analyze",
|
|
"client_rct_transport_summary.py",
|
|
"smoothness",
|
|
"freshness_budget_ms",
|
|
"freshness_bottleneck",
|
|
"client_send_summary",
|
|
"post_client_send_worst_p95_ms",
|
|
"LESAVKA_CLIENT_RCT_UVC_FRAME_META_LOG_REMOTE",
|
|
"uvc_frame_meta_log_remote=${LESAVKA_CLIENT_RCT_UVC_FRAME_META_LOG_REMOTE:-disabled}",
|
|
"client_rct_uvc_frame_meta_fetch.sh",
|
|
"summarize_uvc_frame_meta_log.py",
|
|
"required UVC frame metadata log could not be summarized",
|
|
"optional UVC frame metadata log could not be summarized",
|
|
"uvc_frame_meta_summary_json",
|
|
"lesavka.uvc-mjpeg-spool-summary.v1",
|
|
"uvc_spool",
|
|
"UVC spool boundary",
|
|
"synthetic evidence",
|
|
"video_age_p95_ms",
|
|
"audio_age_p95_ms",
|
|
"artifact_dir: ${LOCAL_REPORT_DIR}",
|
|
] {
|
|
assert!(
|
|
CLIENT_RCT_SCRIPT.contains(expected)
|
|
|| CLIENT_RCT_SUMMARY.contains(expected)
|
|
|| CLIENT_RCT_LAYERS.contains(expected)
|
|
|| UVC_FRAME_META_FETCH.contains(expected)
|
|
|| UVC_FRAME_META_SUMMARY.contains(expected),
|
|
"client-to-RCT probe should preserve artifact/summary marker {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn client_rct_probe_keeps_shell_harness_focused_under_loc_limit() {
|
|
for (name, contents) in [
|
|
("run_client_to_rct_transport_probe.sh", CLIENT_RCT_SCRIPT),
|
|
("client_rct_uvc_frame_meta_fetch.sh", UVC_FRAME_META_FETCH),
|
|
("client_rct_transport_summary.py", CLIENT_RCT_SUMMARY),
|
|
] {
|
|
let loc = contents.lines().count();
|
|
assert!(
|
|
loc <= 500,
|
|
"{name} should stay under the 500 LOC refactor guard; saw {loc}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn client_rct_probe_is_non_mutating_and_passwordless_by_default() {
|
|
for expected in [
|
|
"SSH_OPTS=${SSH_OPTS:-\"-o BatchMode=yes -o ConnectTimeout=5\"}",
|
|
"no remote sudo/reconfigure will be attempted by this script",
|
|
"LESAVKA_SERVER_ADDR=${LESAVKA_SERVER_ADDR:-auto}",
|
|
"LESAVKA_CLIENT_RCT_MODE=${LESAVKA_CLIENT_RCT_MODE:-auto}",
|
|
"LESAVKA_CLIENT_RCT_START_DELAY_SECONDS=${LESAVKA_CLIENT_RCT_START_DELAY_SECONDS:-0}",
|
|
"LESAVKA_CLIENT_RCT_START_DELAY_SECONDS must be a non-negative number",
|
|
"start_delay=${LESAVKA_CLIENT_RCT_START_DELAY_SECONDS}s",
|
|
"ExitOnForwardFailure=yes",
|
|
"127.0.0.1:${local_port}:127.0.0.1:${SERVER_TUNNEL_REMOTE_PORT}",
|
|
] {
|
|
assert!(
|
|
CLIENT_RCT_SCRIPT.contains(expected),
|
|
"client-to-RCT probe should keep unattended marker {expected}"
|
|
);
|
|
}
|
|
for forbidden in ["sudo -S", "read -s", "PASSWORD", "VAULT", "vault"] {
|
|
assert!(
|
|
!CLIENT_RCT_SCRIPT.contains(forbidden) && !UVC_FRAME_META_FETCH.contains(forbidden),
|
|
"client-to-RCT probe should not prompt for or retrieve secrets: {forbidden}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn client_timeline_schema_captures_origin_and_event_timing() {
|
|
for expected in [
|
|
"lesavka.client-transport-probe-timeline.v1",
|
|
"client-generated",
|
|
"client-post-capture-uplink-bundle",
|
|
"client_uplink_included: true",
|
|
"client_start_unix_ns",
|
|
"client_capture_unix_ns",
|
|
"planned_start_us",
|
|
"planned_end_us",
|
|
"marker_tick_period",
|
|
"let client_start_unix_ns = capture.start_unix_ns();",
|
|
"ProbeTimeline::new(camera, &schedule, config.duration, client_start_unix_ns)",
|
|
] {
|
|
assert!(
|
|
SYNC_PROBE_TIMELINE.contains(expected) || SYNC_PROBE_RUNNER.contains(expected),
|
|
"client timeline should contain {expected}"
|
|
);
|
|
}
|
|
assert!(
|
|
SYNC_PROBE_RUNNER.find("ProbeTimeline::new").unwrap()
|
|
< SYNC_PROBE_RUNNER
|
|
.rfind("run_bundled_probe_stream(")
|
|
.unwrap(),
|
|
"timeline should be written before bundled transport starts so origin evidence matches generated media"
|
|
);
|
|
assert!(
|
|
SYNC_PROBE_BUNDLED_TRANSPORT.contains("let bundled_task = tokio::spawn"),
|
|
"bundled transport module should own the spawned streaming task"
|
|
);
|
|
}
|