use super::super::test_helpers::{EnvVarGuard, isolated_cache_dir, lock_env};
use super::*;
use crate::assert::DetailKind;
#[cfg(feature = "llm")]
fn llm_metric(name: &str, value: f64) -> crate::test_support::Metric {
crate::test_support::Metric {
name: name.to_owned(),
value,
polarity: crate::test_support::Polarity::Unknown,
unit: String::new(),
source: crate::test_support::MetricSource::LlmExtract,
stream: crate::test_support::MetricStream::Stdout,
}
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_duplicate_name_rejects() {
let metrics = vec![
llm_metric("latency.p99", 1.0),
llm_metric("latency.p99", 2.0),
];
let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one duplicate-name violation expected, got {violations:?}",
);
assert!(
violations[0].contains("duplicate metric name"),
"diagnostic must mention 'duplicate metric name': {}",
violations[0],
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_nan_rejects() {
let metrics = vec![llm_metric("latency.p99", f64::NAN)];
let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one non-finite violation expected, got {violations:?}",
);
assert!(
violations[0].contains("non-finite"),
"diagnostic must mention 'non-finite': {}",
violations[0],
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_wrong_source_rejects() {
let mut metrics = vec![llm_metric("latency.p99", 1.0)];
metrics[0].source = crate::test_support::MetricSource::Json;
let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
1,
"exactly one wrong-source violation expected, got {violations:?}",
);
assert!(
violations[0].contains("MetricSource::LlmExtract"),
"diagnostic must mention 'MetricSource::LlmExtract': {}",
violations[0],
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_clean_input_passes() {
let metrics = vec![
llm_metric("latency.p50", 1.0),
llm_metric("latency.p99", 2.0),
llm_metric("rps", 1000.0),
];
assert!(
super::llm_extract::validate_llm_extraction(&metrics).is_empty(),
"clean input must produce an empty violations Vec",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_single_metric_multiple_violations() {
let mut metrics = vec![llm_metric("latency.p99", f64::INFINITY)];
metrics[0].source = crate::test_support::MetricSource::Json;
let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
2,
"non-finite + wrong-source on the same metric must produce 2 violations, got {violations:?}",
);
let messages: Vec<&str> = violations.iter().map(String::as_str).collect();
assert!(
messages.iter().any(|m| m.contains("non-finite")),
"non-finite violation must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("MetricSource::LlmExtract")),
"wrong-source violation must appear: {messages:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_multiple_duplicates_each_surface() {
let metrics = vec![
llm_metric("rps", 1.0),
llm_metric("rps", 2.0),
llm_metric("rps", 3.0),
];
let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
2,
"three same-name metrics → two duplicate-name violations, got {violations:?}",
);
for v in &violations {
assert!(
v.contains("duplicate metric name"),
"every violation must call out duplicate name: {v}",
);
}
}
#[cfg(feature = "llm")]
#[test]
fn validate_llm_extraction_heterogeneous_violations_across_metrics() {
let mut metrics = vec![
llm_metric("rps", 1.0),
llm_metric("rps", 2.0), llm_metric("latency.p99", f64::NAN), llm_metric("p50", 1.0),
];
metrics[3].source = crate::test_support::MetricSource::Json; let violations = super::llm_extract::validate_llm_extraction(&metrics);
assert_eq!(
violations.len(),
3,
"three independent violations expected, got {violations:?}",
);
let messages: Vec<&str> = violations.iter().map(String::as_str).collect();
assert!(
messages
.iter()
.any(|m| m.contains("duplicate metric name") && m.contains("'rps'")),
"duplicate-name on 'rps' must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("non-finite") && m.contains("'latency.p99'")),
"non-finite on 'latency.p99' must appear: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("MetricSource::LlmExtract") && m.contains("'p50'")),
"wrong-source on 'p50' must appear: {messages:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_none_produces_no_violations() {
let metrics = vec![
llm_metric("rps", -42.0), llm_metric("latency", 1e15), ];
let bounds = crate::test_support::MetricBounds::default();
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"MetricBounds::default() must produce zero violations regardless of input; \
got: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_min_count_rejects_short_set() {
let metrics = vec![llm_metric("a", 1.0), llm_metric("b", 2.0)];
let bounds = crate::test_support::MetricBounds {
min_count: Some(5),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
1,
"short set must produce exactly one min_count violation; got: {violations:?}",
);
assert!(
violations[0].contains("extracted 2 metric(s)"),
"diagnostic must name actual count: {}",
violations[0],
);
assert!(
violations[0].contains("at least 5"),
"diagnostic must name required minimum: {}",
violations[0],
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_min_count_accepts_at_threshold() {
let metrics = vec![
llm_metric("a", 1.0),
llm_metric("b", 2.0),
llm_metric("c", 3.0),
];
let bounds = crate::test_support::MetricBounds {
min_count: Some(3),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"metric count == min_count is acceptable (>= semantics); got: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_value_min_rejects_each_below_floor() {
let metrics = vec![
llm_metric("p50", -1.0),
llm_metric("p99", -2.0),
llm_metric("rps", 100.0), llm_metric("delta", -5.0),
];
let bounds = crate::test_support::MetricBounds {
value_min: Some(0.0),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
3,
"every below-floor metric must surface its own violation; got: {violations:?}",
);
assert!(
violations
.iter()
.all(|v| v.contains("below payload's declared lower bound")),
"every diagnostic must name the lower-bound class: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'p50'")),
"p50 violation must surface: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'delta'")),
"delta violation must surface: {violations:?}",
);
assert!(
!violations.iter().any(|v| v.contains("'rps'")),
"rps must NOT trigger a value_min violation (100 > 0); got: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_value_min_accepts_at_threshold() {
let metrics = vec![llm_metric("zero", 0.0)];
let bounds = crate::test_support::MetricBounds {
value_min: Some(0.0),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert!(
violations.is_empty(),
"value at exactly value_min is acceptable (strict-less-than semantics); \
got: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_value_max_rejects_each_above_ceiling() {
let metrics = vec![
llm_metric("rss_huge", 1e16),
llm_metric("rss_normal", 1e6),
llm_metric("latency_runaway", 1e15),
];
let bounds = crate::test_support::MetricBounds {
value_max: Some(1e12),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
2,
"two above-ceiling metrics must surface; got: {violations:?}",
);
assert!(
violations
.iter()
.all(|v| v.contains("above payload's declared upper bound")),
"every diagnostic must name the upper-bound class: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("'rss_huge'")),
"rss_huge must trigger: {violations:?}",
);
assert!(
!violations.iter().any(|v| v.contains("'rss_normal'")),
"rss_normal (1e6) must NOT trigger value_max=1e12: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_combined_bounds_each_violation_independent() {
let metrics = vec![llm_metric("low", -1.0), llm_metric("high", 1e15)];
let bounds = crate::test_support::MetricBounds {
min_count: Some(5),
value_min: Some(0.0),
value_max: Some(1e12),
};
let violations = super::llm_extract::validate_metric_bounds(&metrics, &bounds);
assert_eq!(
violations.len(),
3,
"combined: 1 min_count + 1 value_min + 1 value_max violation; got: {violations:?}",
);
assert!(
violations.iter().any(|v| v.contains("at least 5")),
"min_count violation must surface: {violations:?}",
);
assert!(
violations
.iter()
.any(|v| v.contains("'low'") && v.contains("below")),
"value_min on 'low' must surface: {violations:?}",
);
assert!(
violations
.iter()
.any(|v| v.contains("'high'") && v.contains("above")),
"value_max on 'high' must surface: {violations:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn validate_metric_bounds_empty_metrics_with_min_count_violates() {
let bounds = crate::test_support::MetricBounds {
min_count: Some(1),
..crate::test_support::MetricBounds::default()
};
let violations = super::llm_extract::validate_metric_bounds(&[], &bounds);
assert_eq!(
violations.len(),
1,
"empty input + min_count=1 must produce one violation; got: {violations:?}",
);
assert!(
violations[0].contains("extracted 0 metric(s)"),
"diagnostic must name 0 as actual count: {}",
violations[0],
);
}
#[test]
fn payload_metric_bounds_defaults_to_none_via_payload_binary_constructor() {
const P: crate::test_support::Payload =
crate::test_support::Payload::binary("test", "test_bin");
assert!(
P.metric_bounds.is_none(),
"Payload::binary must initialize metric_bounds to None",
);
}
#[test]
fn payload_metric_bounds_carries_static_reference() {
const SCHBENCH_BOUNDS: crate::test_support::MetricBounds = crate::test_support::MetricBounds {
min_count: Some(5),
value_min: Some(0.0),
value_max: Some(1e12),
};
const P: crate::test_support::Payload = crate::test_support::Payload {
name: "schbench_test",
kind: crate::test_support::PayloadKind::Binary("schbench"),
output: crate::test_support::OutputFormat::LlmExtract(None),
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: Some(&SCHBENCH_BOUNDS),
};
assert!(P.metric_bounds.is_some());
let b = P.metric_bounds.unwrap();
assert_eq!(b.min_count, Some(5));
assert_eq!(b.value_min, Some(0.0));
assert_eq!(b.value_max, Some(1e12));
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_offline_gate_skips_bounds_check() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "irrelevant under offline gate".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: Some(crate::test_support::MetricBounds {
min_count: Some(1),
..crate::test_support::MetricBounds::default()
}),
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"offline-gated extraction must produce only the load-failure detail, \
not a spurious bounds violation; got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"the lone failure must be the load-failure: {}",
failures[0].message,
);
}
#[cfg(feature = "llm")]
fn empty_raw(payload_index: usize) -> crate::test_support::RawPayloadOutput {
crate::test_support::RawPayloadOutput {
payload_index,
stdout: String::new(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}
}
#[cfg(feature = "llm")]
fn empty_pm(payload_index: usize) -> crate::test_support::PayloadMetrics {
crate::test_support::PayloadMetrics {
payload_index,
metrics: Vec::new(),
exit_code: 0,
}
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_empty_raw_outputs_returns_no_failures() {
let mut pm = vec![empty_pm(0), empty_pm(1)];
let failures = host_side_llm_extract(&mut pm, &[]);
assert!(failures.is_empty(), "empty raw outputs → no failures");
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_orphan_raw_output_surfaces_pairing_failure() {
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(42)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("payload_index=42")),
"orphan-raw detail naming index 42 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("[0]")),
"orphan-PM scan must surface the empty-metrics PM at index 0: {messages:?}",
);
assert!(
pm[0].metrics.is_empty(),
"no extraction should have run on the orphan path",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_multiple_orphans_each_surface() {
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(10), empty_raw(20), empty_raw(30)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages.iter().any(|m| m.contains("payload_index=10")),
"orphan raw at 10 must surface: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("payload_index=20")),
"orphan raw at 20 must surface: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("payload_index=30")),
"orphan raw at 30 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("[0]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must surface the empty PM at index 0: {messages:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_json_zero_leaves_not_conflated_with_llm_placeholder() {
let mut pm = vec![empty_pm(5)];
let raws = vec![empty_raw(99)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages.iter().any(|m| m.contains("payload_index=99")),
"orphan raw at 99 must surface: {messages:?}",
);
assert!(
pm[0].metrics.is_empty(),
"Json empty-metrics slot must not be written by LlmExtract pairing",
);
assert_eq!(
pm[0].payload_index, 5,
"Json slot's payload_index must be untouched",
);
assert!(
messages
.iter()
.any(|m| m.contains("[5]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must include the Json slot at index 5 in its \
candidate list (false positive disclosed in the diagnostic): {messages:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_orphan_pm_with_no_matching_raw_surfaces() {
let mut pm = vec![empty_pm(7), empty_pm(99)];
let raws = vec![empty_raw(10), empty_raw(20)];
let failures = host_side_llm_extract(&mut pm, &raws);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("[7, 99]") && m.contains("no matching RawPayloadOutput")),
"orphan-PM scan must list both unmatched PM indices [7, 99]: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("CRC mismatch")),
"orphan-PM diagnostic must surface the CRC-bad cause: {messages:?}",
);
assert!(
messages.iter().any(|m| m.contains("False-positive case")),
"orphan-PM diagnostic must disclose the false-positive case for \
mixed-format tests: {messages:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_no_orphan_pm_when_all_pms_have_matching_raws() {
let mut pm = vec![empty_pm(0), empty_pm(1)];
let raws: Vec<crate::test_support::RawPayloadOutput> = Vec::new();
let failures = host_side_llm_extract(&mut pm, &raws);
assert!(
failures.is_empty(),
"with no LlmExtract raws, orphan-PM scan must not fire (test is \
not exercising LlmExtract): {failures:?}",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_with_empty_streams_no_panic_no_metrics() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![empty_raw(0)];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"empty streams under offline gate must produce exactly one load-failed detail, \
got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"load-failure detail must surface the diagnostic prefix; got: {}",
failures[0].message,
);
assert!(
pm[0].metrics.is_empty(),
"PM slot must remain empty when extraction failed; got: {:?}",
pm[0].metrics,
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_under_offline_gate_surfaces_actionable_detail() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "arbitrary stdout content for the model".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"offline gate must produce exactly one load-failed detail, got: {failures:?}",
);
let detail = &failures[0];
assert_eq!(
detail.kind,
DetailKind::Other,
"load-failure detail kind must be `Other` (the framework's bucket \
for infrastructure failures); got: {:?}",
detail.kind,
);
let msg = &detail.message;
assert!(
msg.starts_with("LlmExtract model load failed:"),
"diagnostic must BEGIN WITH 'LlmExtract model load failed:' \
— a substring-only match would let a regression bury the prefix \
behind banner noise. got: {msg:?}",
);
assert!(
msg.contains(crate::test_support::OFFLINE_ENV),
"actionable diagnostic must name the offline env var so the operator \
knows to unset KTSTR_MODEL_OFFLINE or pre-seed the cache; got: {msg}",
);
assert!(
pm[0].metrics.is_empty(),
"load failure must leave the PM slot empty; got: {:?}",
pm[0].metrics,
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_offline_gate_skips_stderr_fallback() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: String::new(),
stderr: "stderr body that the fallback would reach if not gated".to_string(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
}];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
1,
"stderr fallback must be skipped when stdout's call already returned Err; \
a second 'model load failed' detail would mean the gate regressed. \
got: {failures:?}",
);
assert!(
failures[0].message.contains("LlmExtract model load failed"),
"the lone surfaced detail must be the load-failure: {}",
failures[0].message,
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_offline_gate_per_pair_failure_detail() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0), empty_pm(1)];
let raws = vec![
crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "first pair stdout".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
crate::test_support::RawPayloadOutput {
payload_index: 1,
stdout: "second pair stdout".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
2,
"two matched pairs under offline gate must each surface their own load-failure \
detail; a regression that bailed after the first failure would surface only one. \
got: {failures:?}",
);
for f in &failures {
assert!(
f.message.contains("LlmExtract model load failed"),
"every detail must be a load-failure: {}",
f.message,
);
}
assert!(
pm[0].metrics.is_empty() && pm[1].metrics.is_empty(),
"both PM slots must remain empty under the offline gate",
);
}
#[cfg(feature = "llm")]
#[test]
fn host_side_llm_extract_orphan_and_load_failure_both_surface() {
let _env_lock = lock_env();
super::super::model::reset();
let _cache = isolated_cache_dir();
let _offline = EnvVarGuard::set(crate::test_support::OFFLINE_ENV, "1");
let mut pm = vec![empty_pm(0)];
let raws = vec![
crate::test_support::RawPayloadOutput {
payload_index: 0,
stdout: "matched pair".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
crate::test_support::RawPayloadOutput {
payload_index: 99,
stdout: "orphan".to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
},
];
let failures = host_side_llm_extract(&mut pm, &raws);
assert_eq!(
failures.len(),
2,
"mixed orphan + matched-but-load-failing must surface both details independently; \
got: {failures:?}",
);
let messages: Vec<&str> = failures.iter().map(|d| d.message.as_str()).collect();
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract host pairing") && m.contains("payload_index=99")),
"orphan detail naming index 99 must surface: {messages:?}",
);
assert!(
messages
.iter()
.any(|m| m.contains("LlmExtract model load failed")),
"load-failure detail must surface: {messages:?}",
);
}
#[test]
fn raw_payload_output_bulk_wire_round_trip_preserves_both_streams() {
use crate::vmm::wire;
const STDOUT_MARKER: &str = "STDOUT_MARKER_BULK_E2E_a1b2c3";
const STDERR_MARKER: &str = "STDERR_MARKER_BULK_E2E_x9y8z7";
let original = crate::test_support::RawPayloadOutput {
payload_index: 21,
stdout: STDOUT_MARKER.to_string(),
stderr: STDERR_MARKER.to_string(),
hint: Some("bulk-focus".to_string()),
metric_hints: Vec::new(),
metric_bounds: None,
};
let payload = postcard::to_stdvec(&original).expect("postcard-encode RawPayloadOutput");
use zerocopy::IntoBytes;
let hdr = wire::ShmMessage {
msg_type: wire::MSG_TYPE_RAW_PAYLOAD_OUTPUT,
length: payload.len() as u32,
crc32: crc32fast::hash(&payload),
_pad: 0,
};
let mut frame: Vec<u8> = Vec::with_capacity(wire::FRAME_HEADER_SIZE + payload.len());
frame.extend_from_slice(hdr.as_bytes());
frame.extend_from_slice(&payload);
let drained = crate::vmm::host_comms::parse_tlv_stream(&frame);
assert_eq!(
drained.entries.len(),
1,
"exactly one entry expected from bulk parse",
);
let entry = &drained.entries[0];
assert_eq!(entry.msg_type, wire::MSG_TYPE_RAW_PAYLOAD_OUTPUT,);
assert!(entry.crc_ok, "bulk CRC must match");
let restored: crate::test_support::RawPayloadOutput =
postcard::from_bytes(&entry.payload).expect("decode RawPayloadOutput from bulk");
assert_eq!(restored.stdout, STDOUT_MARKER);
assert_eq!(restored.stderr, STDERR_MARKER);
assert!(!restored.stdout.contains(STDERR_MARKER));
assert!(!restored.stderr.contains(STDOUT_MARKER));
assert_eq!(restored.payload_index, original.payload_index);
assert_eq!(restored.hint.as_deref(), Some("bulk-focus"));
}