#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::uninlined_format_args,
clippy::collapsible_if,
clippy::redundant_clone,
clippy::needless_raw_string_hashes,
clippy::single_match,
clippy::redundant_closure_for_method_calls,
clippy::redundant_pattern_matching,
clippy::ignored_unit_patterns,
clippy::clone_on_copy,
clippy::manual_assert,
clippy::unwrap_in_result,
clippy::useless_vec
)]
use std::{process::Command, time::Duration};
use meerkat_mobkit::{
BaselineRuntimeError, DiscoverySpec, MobKitConfig, ModuleConfig, NormalizationError,
PreSpawnData, ProcessBoundaryError, ProtocolParseError, RestartPolicy, RpcRuntimeError,
RuntimeBoundaryError, UnifiedEvent, parse_module_event_line, run_discovered_module_once,
run_meerkat_baseline_verification_once, run_module_boundary_once,
run_rpc_capabilities_boundary_once,
};
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct ReadyPayload {
version: String,
}
fn shell_module(id: &str, script: &str) -> ModuleConfig {
ModuleConfig {
id: id.to_string(),
command: "sh".to_string(),
args: vec!["-c".to_string(), script.to_string()],
restart_policy: RestartPolicy::Never,
}
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_valid_json_line_from_subprocess_parses() {
let config = MobKitConfig {
modules: vec![shell_module(
"echo",
r#"printf '%s\n' '{"event_id":"evt-1","source":"module","timestamp_ms":42,"event":{"kind":"module","module":"echo","event_type":"ready","payload":{"version":"1.0.0"}}}'"#,
)],
discovery: DiscoverySpec {
namespace: "default".to_string(),
modules: vec!["echo".to_string()],
},
pre_spawn: vec![PreSpawnData {
module_id: "echo".to_string(),
env: vec![("RUST_LOG".to_string(), "debug".to_string())],
}],
};
let envelope = run_discovered_module_once(&config, "echo", Duration::from_secs(1))
.expect("valid process output should parse");
assert_eq!(envelope.event_id, "evt-1");
assert_eq!(envelope.source, "module");
assert_eq!(envelope.timestamp_ms, 42);
assert_eq!(
envelope.event,
UnifiedEvent::Module(meerkat_mobkit::ModuleEvent {
module: "echo".to_string(),
event_type: "ready".to_string(),
payload: json!({"version":"1.0.0"}),
})
);
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_invalid_schema_from_subprocess_rejected() {
let module = shell_module(
"bad",
r#"printf '%s\n' '{"source":"module","timestamp_ms":42,"event":{"kind":"module"}}'"#,
);
let err = run_module_boundary_once(&module, None, Duration::from_secs(1))
.expect_err("schema must reject");
assert_eq!(
err,
RuntimeBoundaryError::Normalize(NormalizationError::MissingField("event_id"))
);
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_timeout_on_never_responding_subprocess() {
let module = shell_module("sleepy", "sleep 5");
let err = run_module_boundary_once(&module, None, Duration::from_millis(25))
.expect_err("must timeout");
assert_eq!(
err,
RuntimeBoundaryError::Process(ProcessBoundaryError::Timeout { timeout_ms: 25 })
);
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_normalization_path_over_mixed_subprocess_outputs() {
let agent = shell_module(
"agent-line",
r#"printf '%s\n' '{"event_id":"evt-agent","source":"agent","timestamp_ms":1,"agent_id":"a-1","event_type":"tick"}'"#,
);
let module = shell_module(
"module-line",
r#"printf '%s\n' '{"event_id":"evt-module","source":"module","timestamp_ms":2,"module":"mod-1","event_type":"ready","payload":{"ok":true}}'"#,
);
let first = run_module_boundary_once(&agent, None, Duration::from_secs(1)).expect("agent line");
let second =
run_module_boundary_once(&module, None, Duration::from_secs(1)).expect("module line");
assert_eq!(first.event_id, "evt-agent");
assert_eq!(first.source, "agent");
assert_eq!(first.timestamp_ms, 1);
assert_eq!(
first.event,
UnifiedEvent::Agent {
agent_id: "a-1".to_string(),
event_type: "tick".to_string(),
payload: None,
}
);
assert_eq!(second.event_id, "evt-module");
assert_eq!(second.source, "module");
assert_eq!(second.timestamp_ms, 2);
assert_eq!(
second.event,
UnifiedEvent::Module(meerkat_mobkit::ModuleEvent {
module: "mod-1".to_string(),
event_type: "ready".to_string(),
payload: json!({"ok":true}),
})
);
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_rpc_capabilities_requires_contract_version_from_process_response() {
let caps = run_rpc_capabilities_boundary_once(
"sh",
&[
"-c".to_string(),
r#"printf '%s\n' '{"contract_version":"0.1.0","transport":"stdio"}'"#.to_string(),
],
&[],
Duration::from_secs(1),
)
.expect("process should return capabilities");
assert_eq!(caps.contract_version, "0.1.0");
let missing = run_rpc_capabilities_boundary_once(
"sh",
&[
"-c".to_string(),
r#"printf '%s\n' '{"transport":"stdio"}'"#.to_string(),
],
&[],
Duration::from_secs(1),
)
.expect_err("missing contract_version must fail");
assert_eq!(
missing,
RpcRuntimeError::Capabilities(meerkat_mobkit::RpcCapabilitiesError::MissingContractVersion)
);
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_meerkat_baseline_symbols_check_against_repo_path() {
let report = run_meerkat_baseline_verification_once(
"sh",
&[
"-c".to_string(),
"printf '%s\\n' \"{\\\"repo_root\\\":\\\"${MEERKAT_REPO:-/Users/luka/src/raik}\\\"}\""
.to_string(),
],
&[],
Duration::from_secs(1),
)
.expect("baseline symbols should exist");
assert!(report.missing_symbols.is_empty());
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_meerkat_baseline_missing_symbols_has_typed_diagnostics() {
let temp = tempfile::tempdir().expect("temp dir");
let err = run_meerkat_baseline_verification_once(
"sh",
&[
"-c".to_string(),
format!(
"printf '%s\\n' '{{\"repo_root\":\"{}\"}}'",
temp.path().display()
),
],
&[],
Duration::from_secs(1),
)
.expect_err("empty repo should fail");
assert!(matches!(err, BaselineRuntimeError::Baseline(_)));
}
#[test]
#[ignore = "external boundary subprocess check"]
fn external_unexpected_payload_from_subprocess_is_rejected() {
let line = run_module_boundary_once(
&shell_module(
"bad-payload",
r#"printf '%s\n' '{"event_id":"evt-x","source":"module","timestamp_ms":42,"event":{"kind":"module","module":"echo","event_type":"ready","payload":{"unexpected":true}}}'"#,
),
None,
Duration::from_secs(1),
)
.expect("line should normalize");
let envelope = serde_json::to_string(&line).expect("serialize normalized envelope");
let err = parse_module_event_line::<ReadyPayload>(&envelope, "ready")
.expect_err("typed payload mismatch should fail");
assert_eq!(err, ProtocolParseError::UnexpectedPayloadType);
}
#[test]
#[ignore = "external baseline subprocess check"]
fn external_phase0_baseline_check_binary_runs() {
let output = Command::new(env!("CARGO_BIN_EXE_baseline_check"))
.env("MEERKAT_REPO", "/Users/luka/src/raik")
.output()
.expect("phase0 baseline binary should run");
assert!(
output.status.success(),
"phase0_baseline_check failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("missing_symbols="));
}