use lsp_max::{LanguageServer, LspService, Server};
use std::sync::Arc;
use std::time::Duration;
static TEST_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
use lsp_max::jsonrpc::Result as RpcResult;
use lsp_max::lsp_types as lsp;
mod common;
use common::{cleanup_receipts, read_message, wait_for_response, write_msg, RxLog, TxShared};
struct TestBackend;
#[lsp_max::async_trait]
impl LanguageServer for TestBackend {
async fn initialize(&self, _: lsp::InitializeParams) -> RpcResult<lsp::InitializeResult> {
Ok(lsp::InitializeResult::default())
}
async fn shutdown(&self) -> RpcResult<()> {
Ok(())
}
}
type SerialGuard = tokio::sync::MutexGuard<'static, ()>;
async fn boot_server() -> (TxShared, RxLog, tokio::task::JoinHandle<()>, SerialGuard) {
let _guard = TEST_MUTEX.lock().await;
lsp_max::reset_registry_for_tests();
let temp_dir = tempfile::tempdir().unwrap();
let temp_path = temp_dir.path().to_path_buf();
std::boxed::Box::leak(std::boxed::Box::new(temp_dir));
if let Ok(mut reg) = lsp_max::get_registry().lock() {
reg.root_path = temp_path.clone();
}
let _ = std::fs::remove_file(temp_path.join("admission.receipt"));
let _ = std::fs::remove_file(temp_path.join("security.receipt"));
let _ = std::fs::remove_file(temp_path.join("auth.receipt"));
let (service, socket) = LspService::new(|_| TestBackend);
let (client_tx, server_rx) = tokio::io::duplex(1024 * 1024);
let (server_tx, client_rx) = tokio::io::duplex(1024 * 1024);
let server_handle = tokio::spawn(async move {
let _ = Server::new(server_rx, server_tx, socket)
.serve(service)
.await;
});
let client_tx_shared: TxShared = Arc::new(tokio::sync::Mutex::new(Some(client_tx)));
let received: RxLog = Arc::new(std::sync::Mutex::new(Vec::new()));
let received_clone = received.clone();
let mut client_rx_owned = client_rx;
tokio::spawn(async move {
while let Ok(msg) = read_message(&mut client_rx_owned).await {
received_clone.lock().unwrap().push(msg);
}
});
write_msg(
&client_tx_shared,
serde_json::json!({"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{}}}),
)
.await;
wait_for_response(received.clone(), 0, Duration::from_millis(300)).await;
(client_tx_shared, received, server_handle, _guard)
}
fn assert_has_result(resp: &serde_json::Value, method: &str) {
assert!(
resp.get("result").is_some(),
"method {} must return 'result', got: {}",
method,
resp
);
}
async fn test_rpc_method(method: &str, params: serde_json::Value) -> serde_json::Value {
let (tx, rx, _h, _guard) = boot_server().await;
let payload = if params.is_null() {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": method
})
} else {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params
})
};
write_msg(&tx, payload).await;
let resp = wait_for_response(rx, 1, Duration::from_millis(300)).await;
cleanup_receipts();
resp
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_hook_returns_result() {
let resp = test_rpc_method("max/hook", serde_json::Value::Null).await;
assert_has_result(&resp, "max/hook");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_hook_graph_returns_result() {
let resp = test_rpc_method("max/hookGraph", serde_json::Value::Null).await;
assert_has_result(&resp, "max/hookGraph");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_chain_returns_result() {
let resp = test_rpc_method("max/chain", serde_json::Value::Null).await;
assert_has_result(&resp, "max/chain");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_propagate_returns_result() {
let resp = test_rpc_method(
"max/propagate",
serde_json::json!({
"receipt_id": "rcpt-propagate-test",
"hash": "abc123",
"prev_receipt_hash": null
}),
)
.await;
assert_has_result(&resp, "max/propagate");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_autonomic_loop_returns_result() {
let resp = test_rpc_method("max/autonomicLoop", serde_json::Value::Null).await;
assert_has_result(&resp, "max/autonomicLoop");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_manifold_snapshot_returns_result() {
let resp = test_rpc_method("max/manifoldSnapshot", serde_json::Value::Null).await;
assert_has_result(&resp, "max/manifoldSnapshot");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_lawful_transition_returns_result() {
let resp = test_rpc_method("max/lawfulTransition", serde_json::json!("Initializing")).await;
assert_has_result(&resp, "max/lawfulTransition");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_admission_returns_result() {
let resp = test_rpc_method("max/admission", serde_json::Value::Null).await;
assert_has_result(&resp, "max/admission");
let result = resp.get("result").unwrap();
let verdict = result.get("verdict").and_then(|v| v.as_str()).unwrap_or("");
assert!(
["Admitted", "Refused", "Unknown"].contains(&verdict),
"verdict must be Admitted/Refused/Unknown, got: {}",
verdict
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_refusal_returns_result() {
let resp = test_rpc_method("max/refusal", serde_json::json!("diag-test-refusal")).await;
assert_has_result(&resp, "max/refusal");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_replay_returns_result() {
let resp = test_rpc_method("max/replay", serde_json::Value::Null).await;
assert_has_result(&resp, "max/replay");
let result = resp.get("result").unwrap();
assert!(
result.get("receipt_count").is_some(),
"max/replay result must have 'receipt_count' key, got: {}",
result
);
assert!(
result.get("receipts").is_some(),
"max/replay result must have 'receipts' key, got: {}",
result
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_release_actuation_returns_rpc_response() {
let resp = test_rpc_method("max/releaseActuation", serde_json::Value::Null).await;
assert!(
resp.get("result").is_some() || resp.get("error").is_some(),
"max/releaseActuation must return a JSON-RPC response, got: {}",
resp
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_dump_state_returns_result() {
let resp = test_rpc_method("max/dumpState", serde_json::Value::Null).await;
assert_has_result(&resp, "max/dumpState");
let result = resp.get("result").unwrap();
assert!(
result.get("diagnostics").is_some(),
"max/dumpState result must contain 'diagnostics' field (ServerRegistry), got: {}",
result
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_restore_state_returns_result() {
let (tx, rx, _h, _guard) = boot_server().await;
write_msg(
&tx,
serde_json::json!({"jsonrpc":"2.0","id":1,"method":"max/dumpState"}),
)
.await;
let dump_resp = wait_for_response(rx.clone(), 1, Duration::from_millis(300)).await;
let state = dump_resp
.get("result")
.expect("dumpState must return result")
.clone();
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "max/restoreState",
"params": state
}),
)
.await;
let resp = wait_for_response(rx, 2, Duration::from_millis(300)).await;
assert_has_result(&resp, "max/restoreState");
cleanup_receipts();
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_reset_returns_result() {
let resp = test_rpc_method("max/reset", serde_json::Value::Null).await;
assert_has_result(&resp, "max/reset");
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_lawful_transition_has_admitted_field() {
let resp = test_rpc_method("max/lawfulTransition", serde_json::json!("Initializing")).await;
assert_has_result(&resp, "max/lawfulTransition");
let result = resp.get("result").unwrap();
assert!(
result.get("admitted").is_some(),
"max/lawfulTransition result must have 'admitted' field, got: {}",
result
);
let admitted = result.get("admitted").unwrap();
assert!(
admitted.is_boolean(),
"max/lawfulTransition 'admitted' must be a boolean, got: {}",
admitted
);
assert!(
result.get("current_phase").is_some(),
"max/lawfulTransition result must have 'current_phase' field"
);
assert!(
result.get("requested_phase").is_some(),
"max/lawfulTransition result must have 'requested_phase' field"
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_admission_conformance_vector_fields() {
let resp = test_rpc_method("max/admission", serde_json::Value::Null).await;
assert_has_result(&resp, "max/admission");
let result = resp.get("result").unwrap();
let verdict = result.get("verdict").and_then(|v| v.as_str()).unwrap_or("");
assert!(
["Admitted", "Refused", "Unknown"].contains(&verdict),
"max/admission verdict must be Admitted/Refused/Unknown (ConformanceVector states), got: {}",
verdict
);
assert!(
result.get("diagnostic_count").is_some(),
"max/admission result must have 'diagnostic_count' field"
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_refusal_response_shape() {
let resp = test_rpc_method("max/refusal", serde_json::json!("diag-ctq-refusal")).await;
assert_has_result(&resp, "max/refusal");
let result = resp.get("result").unwrap();
assert!(
result.get("refused").is_some(),
"max/refusal result must have 'refused' field, got: {}",
result
);
let refused = result.get("refused").unwrap().as_bool().unwrap_or(false);
assert!(refused, "max/refusal 'refused' must be true");
assert!(
result.get("diagnostic_id").is_some(),
"max/refusal result must have 'diagnostic_id' field"
);
assert!(
result.get("receipt").is_some(),
"max/refusal result must have 'receipt' field"
);
let receipt = result.get("receipt").unwrap();
assert!(
receipt.get("receipt_id").is_some(),
"max/refusal receipt must have 'receipt_id' field"
);
assert!(
receipt.get("hash").is_some(),
"max/refusal receipt must have 'hash' field"
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_max_conformance_delta_returns_result() {
let resp = test_rpc_method(
"max/conformanceDelta",
serde_json::json!({ "since_seq": 0 }),
)
.await;
assert_has_result(&resp, "max/conformanceDelta");
let result = resp.get("result").unwrap();
assert!(
result.get("deltas").is_some(),
"conformanceDelta result must have 'deltas' key"
);
assert!(
result.get("current_seq").is_some(),
"conformanceDelta result must have 'current_seq' key"
);
}
#[tokio::test(flavor = "current_thread")]
async fn test_full_lifecycle_diagnostic_repair_conformance_release() {
let (tx, rx, _h, _guard) = boot_server().await;
write_msg(
&tx,
serde_json::json!({"jsonrpc":"2.0","id":10,"method":"max/snapshot"}),
)
.await;
let snap1_resp = wait_for_response(rx.clone(), 10, Duration::from_millis(300)).await;
assert!(
snap1_resp.get("result").is_some(),
"max/snapshot (step 1) must return 'result', got: {}",
snap1_resp
);
let snapshot_id1 = {
let raw = &snap1_resp["result"];
if let Some(s) = raw.as_str() {
s.to_string()
} else if let Some(s) = raw.get("id").and_then(|v| v.as_str()) {
s.to_string()
} else {
raw.to_string().trim_matches('"').to_string()
}
};
assert!(
snapshot_id1.starts_with("snap-"),
"Expected snapshot_id to start with 'snap-', got: {}",
snapshot_id1
);
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 11,
"method": "max/conformanceVector",
"params": snapshot_id1
}),
)
.await;
let cv1_resp = wait_for_response(rx.clone(), 11, Duration::from_millis(300)).await;
assert!(
cv1_resp.get("result").is_some(),
"max/conformanceVector (initial) must return 'result', got: {}",
cv1_resp
);
assert!(
cv1_resp["result"].get("admitted").is_some(),
"conformanceVector must have 'admitted' field, got cv: {}",
cv1_resp["result"]
);
assert!(
cv1_resp["result"].get("refused").is_some(),
"conformanceVector must have 'refused' field, got cv: {}",
cv1_resp["result"]
);
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 12,
"method": "max/explainDiagnostic",
"params": "diag-auth-generator"
}),
)
.await;
let explain_resp = wait_for_response(rx.clone(), 12, Duration::from_millis(300)).await;
assert!(
explain_resp.get("result").is_some(),
"max/explainDiagnostic must return 'result', got: {}",
explain_resp
);
assert_eq!(
explain_resp["result"]["diagnostic_id"]
.as_str()
.unwrap_or(""),
"diag-auth-generator",
"explainDiagnostic must return the requested diagnostic"
);
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 13,
"method": "max/repairPlan",
"params": "diag-auth-generator"
}),
)
.await;
let plan_resp = wait_for_response(rx.clone(), 13, Duration::from_millis(300)).await;
assert!(
plan_resp.get("result").is_some(),
"max/repairPlan must return 'result', got: {}",
plan_resp
);
let plans = plan_resp["result"]
.as_array()
.expect("repairPlan must return array");
assert!(
!plans.is_empty(),
"repairPlan for diag-auth-generator must return at least one action"
);
let action = plans[0].clone();
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 14,
"method": "max/applyRepairTransaction",
"params": action
}),
)
.await;
let repair_resp = wait_for_response(rx.clone(), 14, Duration::from_millis(300)).await;
assert!(
repair_resp.get("result").is_some(),
"max/applyRepairTransaction must return 'result', got: {}",
repair_resp
);
let receipt_id = repair_resp["result"]["receipt_id"].as_str().unwrap_or("");
assert!(
receipt_id.starts_with("rcpt-"),
"repair receipt_id must start with 'rcpt-', got: {}",
repair_resp["result"]
);
write_msg(
&tx,
serde_json::json!({"jsonrpc":"2.0","id":15,"method":"max/snapshot"}),
)
.await;
let snap2_resp = wait_for_response(rx.clone(), 15, Duration::from_millis(300)).await;
assert!(
snap2_resp.get("result").is_some(),
"max/snapshot (post-repair) must return 'result', got: {}",
snap2_resp
);
let snapshot_id2 = {
let raw = &snap2_resp["result"];
if let Some(s) = raw.as_str() {
s.to_string()
} else if let Some(s) = raw.get("id").and_then(|v| v.as_str()) {
s.to_string()
} else {
raw.to_string().trim_matches('"').to_string()
}
};
assert!(
snapshot_id2.starts_with("snap-"),
"Expected second snapshot_id to start with 'snap-', got: {}",
snapshot_id2
);
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 16,
"method": "max/conformanceVector",
"params": snapshot_id2
}),
)
.await;
let cv2_resp = wait_for_response(rx.clone(), 16, Duration::from_millis(300)).await;
assert!(
cv2_resp.get("result").is_some(),
"max/conformanceVector (post-repair) must return 'result', got: {}",
cv2_resp
);
let cv2 = &cv2_resp["result"];
assert!(
cv2.get("admitted").is_some(),
"post-repair conformanceVector must have 'admitted' field"
);
assert!(
cv2.get("refused").is_some(),
"post-repair conformanceVector must have 'refused' field"
);
write_msg(
&tx,
serde_json::json!({
"jsonrpc": "2.0",
"id": 17,
"method": "max/releaseActuation",
"params": { "instance_id": "lifecycle-test-instance" }
}),
)
.await;
let release_resp = wait_for_response(rx.clone(), 17, Duration::from_millis(300)).await;
assert!(
release_resp.get("result").is_some() || release_resp.get("error").is_some(),
"max/releaseActuation must return a well-formed JSON-RPC response, got: {}",
release_resp
);
cleanup_receipts();
}