2026-07-01 08:44:25 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use datastream::{DatastreamEndpoint, Record};
|
|
|
|
|
use iroh::{EndpointAddr, SecretKey};
|
|
|
|
|
use mvp_system::bootstrap_datastream::{BootstrapDatastreamBridge, node_stream_id};
|
|
|
|
|
use mvp_system::provisioning::{
|
|
|
|
|
NodeProvisionSpec, PluginObservation, PluginObservationSink, PluginSink, ProvisionLogStream,
|
|
|
|
|
};
|
|
|
|
|
use mvp_system::telemetry::MvpProvisionLogRecord;
|
|
|
|
|
use parking_lot::Mutex;
|
|
|
|
|
use serde_json::json;
|
|
|
|
|
use swactor::actor::ActorAddress;
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
struct RecordingSink {
|
|
|
|
|
observations: Mutex<Vec<PluginObservation>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RecordingSink {
|
|
|
|
|
fn observations(&self) -> Vec<PluginObservation> {
|
|
|
|
|
self.observations.lock().clone()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PluginObservationSink for RecordingSink {
|
|
|
|
|
fn observe(&self, observation: PluginObservation) {
|
|
|
|
|
self.observations.lock().push(observation);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn recording_sink() -> (Arc<RecordingSink>, PluginSink) {
|
|
|
|
|
let recording = Arc::new(RecordingSink::default());
|
|
|
|
|
(recording.clone(), PluginSink::new(recording))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spec() -> NodeProvisionSpec {
|
|
|
|
|
NodeProvisionSpec {
|
|
|
|
|
run_id: 7,
|
|
|
|
|
node_id: 42,
|
|
|
|
|
stage_index: Some(3),
|
|
|
|
|
image: "worker:latest".to_owned(),
|
|
|
|
|
env: Vec::new(),
|
|
|
|
|
args: Vec::new(),
|
2026-07-05 09:59:51 +00:00
|
|
|
mounts: Vec::new(),
|
2026-07-01 08:44:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bootstrap_bridge_writes_node_stream_and_forwards_plugin_observations() {
|
|
|
|
|
let endpoint = DatastreamEndpoint::new(node_stream_id(7, 42));
|
|
|
|
|
let subscription = endpoint.subscribe_all("test");
|
|
|
|
|
let (recording, sink) = recording_sink();
|
|
|
|
|
let bridge = BootstrapDatastreamBridge::new(spec(), sink, Some(endpoint.producer()));
|
|
|
|
|
|
2026-07-07 10:40:02 +00:00
|
|
|
bridge.observe_stdout_line("ssh stdout diagnostic");
|
|
|
|
|
bridge.observe_stderr_line("debug1: ssh stderr diagnostic");
|
2026-07-01 08:44:25 +00:00
|
|
|
endpoint.tick();
|
|
|
|
|
|
|
|
|
|
let observations = recording.observations();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
observations,
|
|
|
|
|
vec![
|
|
|
|
|
PluginObservation::StdoutLine {
|
|
|
|
|
run_id: 7,
|
|
|
|
|
node_id: 42,
|
2026-07-07 10:40:02 +00:00
|
|
|
line: "ssh stdout diagnostic".to_owned(),
|
2026-07-01 08:44:25 +00:00
|
|
|
},
|
|
|
|
|
PluginObservation::StderrLine {
|
|
|
|
|
run_id: 7,
|
|
|
|
|
node_id: 42,
|
2026-07-07 10:40:02 +00:00
|
|
|
line: "debug1: ssh stderr diagnostic".to_owned(),
|
2026-07-01 08:44:25 +00:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let deliveries = subscription.drain_available();
|
2026-07-07 10:40:02 +00:00
|
|
|
let logs = deliveries
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|delivery| delivery.stream == node_stream_id(7, 42))
|
|
|
|
|
.filter_map(|delivery| MvpProvisionLogRecord::decode(&delivery.frame.payload).ok())
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
assert_eq!(logs.len(), 2, "{deliveries:?}");
|
2026-07-01 08:44:25 +00:00
|
|
|
|
2026-07-07 10:40:02 +00:00
|
|
|
let stdout = &logs[0];
|
|
|
|
|
let stderr = &logs[1];
|
2026-07-01 08:44:25 +00:00
|
|
|
assert_eq!(stdout.line.stream, ProvisionLogStream::Stdout);
|
2026-07-07 10:40:02 +00:00
|
|
|
assert_eq!(stdout.line.line, "ssh stdout diagnostic");
|
2026-07-01 08:44:25 +00:00
|
|
|
assert_eq!(stderr.line.stream, ProvisionLogStream::Stderr);
|
2026-07-07 10:40:02 +00:00
|
|
|
assert_eq!(stderr.line.line, "debug1: ssh stderr diagnostic");
|
2026-07-01 08:44:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ready_json_on_stdout_emits_runtime_ready_through_plugin_sink() {
|
|
|
|
|
let (recording, sink) = recording_sink();
|
|
|
|
|
let bridge = BootstrapDatastreamBridge::new(spec(), sink, None);
|
|
|
|
|
let endpoint = EndpointAddr::new(SecretKey::from_bytes(&[7; 32]).public());
|
|
|
|
|
let node_actor = ActorAddress::new_random();
|
|
|
|
|
let line = serde_json::to_string(&json!({
|
|
|
|
|
"type": "ready",
|
|
|
|
|
"endpoint": endpoint,
|
|
|
|
|
"node_actor": node_actor,
|
|
|
|
|
"logical_node_id": 42,
|
|
|
|
|
"stage_index": 3,
|
|
|
|
|
}))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
bridge.observe_stdout_line(line);
|
|
|
|
|
|
|
|
|
|
assert!(recording.observations().iter().any(|observation| matches!(
|
|
|
|
|
observation,
|
|
|
|
|
PluginObservation::RuntimeReady {
|
|
|
|
|
run_id: 7,
|
|
|
|
|
node_id: 42,
|
|
|
|
|
stage_index: Some(3),
|
|
|
|
|
endpoint: observed_endpoint,
|
|
|
|
|
node_actor: observed_actor,
|
|
|
|
|
} if observed_endpoint == &endpoint && observed_actor == &node_actor
|
|
|
|
|
)));
|
|
|
|
|
}
|