use super::*;
use std::sync::{Arc, mpsc};
const APP_STYLE_URI: &str = "file:///workspace-q/src/App.module.scss";
const THEME_STYLE_URI: &str = "file:///workspace-q/src/_theme.scss";
fn open_query_dispatch_workspace(state: &mut LspShellState) {
handle_lsp_message(
state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{
"uri": "file:///workspace-q",
"name": "workspace-q",
},
],
},
}),
);
for (uri, text) in [
(
APP_STYLE_URI,
"@use \"./theme\";\n.btn { color: red; }\n.btn { color: green; }",
),
(THEME_STYLE_URI, ".btn { color: blue; }"),
] {
handle_lsp_message(
state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": uri,
"languageId": "scss",
"version": 1,
"text": text,
},
},
}),
);
}
}
fn hover_btn_request(id: u64) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": APP_STYLE_URI,
},
"position": {
"line": 1,
"character": 2,
},
},
})
}
fn change_theme_color(state: &mut LspShellState, version: i64, text: &str) {
handle_lsp_message(
state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": THEME_STYLE_URI,
"version": version,
},
"contentChanges": [
{
"text": text,
},
],
},
}),
);
}
fn dispatched_hover_markdown(
snapshot: &LspQuerySnapshotV0,
id: u64,
) -> Result<String, std::io::Error> {
let dispatch = LspQueryDispatchV0 {
snapshot: snapshot_clone_for_test(snapshot),
message: hover_btn_request(id),
completion: None,
};
resolve_dispatched_query_response(&dispatch)
.as_ref()
.and_then(|value| value.pointer("/result/contents/value"))
.and_then(Value::as_str)
.map(ToString::to_string)
.ok_or_else(|| std::io::Error::other("dispatched hover should render markdown"))
}
fn snapshot_clone_for_test(snapshot: &LspQuerySnapshotV0) -> LspQuerySnapshotV0 {
snapshot.shell_state_for_test().query_snapshot()
}
#[test]
fn query_snapshot_clones_document_pointers_and_isolates_later_edits() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let snapshot = state.query_snapshot();
for (file_id, document) in &state.documents {
let snapshot_document = snapshot
.shell_state_for_test()
.documents
.get(file_id)
.ok_or_else(|| std::io::Error::other("snapshot must carry every document"))?;
assert!(
Arc::ptr_eq(document, snapshot_document),
"snapshot must clone document pointers, not the corpus: {}",
document.uri
);
}
change_theme_color(&mut state, 2, ".btn { color: purple; }");
let theme_storage_uri = LspShellState::document_storage_uri(THEME_STYLE_URI);
let app_storage_uri = LspShellState::document_storage_uri(APP_STYLE_URI);
let theme_file_id = state
.document_file_id(THEME_STYLE_URI)
.ok_or_else(|| std::io::Error::other("theme document must be interned"))?;
let app_file_id = state
.document_file_id(APP_STYLE_URI)
.ok_or_else(|| std::io::Error::other("app document must be interned"))?;
assert!(
!Arc::ptr_eq(
&state.documents[&theme_file_id],
&snapshot.shell_state_for_test().documents[&theme_file_id],
),
"edited document must be copy-on-write detached from the snapshot: {theme_storage_uri}"
);
assert!(
Arc::ptr_eq(
&state.documents[&app_file_id],
&snapshot.shell_state_for_test().documents[&app_file_id],
),
"untouched document must still share storage with the snapshot: {app_storage_uri}"
);
let stale_markdown = dispatched_hover_markdown(&snapshot, 11)?;
assert!(
stale_markdown.contains("`blue`") && !stale_markdown.contains("`purple`"),
"stale snapshot must answer from its dispatch-time corpus: {stale_markdown}"
);
let fresh_markdown = dispatched_hover_markdown(&state.query_snapshot(), 12)?;
assert!(
fresh_markdown.contains("`purple`") && !fresh_markdown.contains("`blue`"),
"fresh snapshot must reflect the edit: {fresh_markdown}"
);
Ok(())
}
#[test]
fn dispatched_query_response_on_worker_thread_matches_synchronous_handler() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let dispatch = LspQueryDispatchV0 {
snapshot: state.query_snapshot(),
message: hover_btn_request(21),
completion: None,
};
let worker_response = std::thread::spawn(move || resolve_dispatched_query_response(&dispatch))
.join()
.map_err(|_| std::io::Error::other("query worker test thread panicked"))?;
let synchronous_response = handle_lsp_message(&mut state, hover_btn_request(21));
assert_eq!(
worker_response, synchronous_response,
"worker resolution from the snapshot must match the synchronous handler"
);
assert!(
worker_response.is_some(),
"hover request must produce a response"
);
Ok(())
}
#[test]
fn dispatched_query_shares_cascade_narrowing_memo_with_loop() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let snapshot = state.query_snapshot();
assert!(
Arc::ptr_eq(
&state.cascade_narrowing_substrate_memo,
&snapshot
.shell_state_for_test()
.cascade_narrowing_substrate_memo,
),
"snapshot must share the memo handle, not copy it"
);
assert!(
state.cascade_narrowing_substrate_memo_lock().is_none(),
"no hover ran yet, the memo must be empty"
);
let _ = dispatched_hover_markdown(&snapshot, 31)?;
assert!(
state.cascade_narrowing_substrate_memo_lock().is_some(),
"a substrate built on the dispatched lane must be visible to the loop"
);
Ok(())
}
#[test]
fn dispatch_gate_classifies_requests_and_honors_loop_side_cancellation() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let LspLoopTurnV0::DispatchQuery(dispatch) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, hover_btn_request(41))
else {
return Err(
std::io::Error::other("hover request must dispatch, not resolve inline").into(),
);
};
assert_eq!(
dispatch.message.get("id"),
Some(&json!(41)),
"dispatch must carry the original request"
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "$/cancelRequest",
"params": {
"id": 42,
},
}),
);
let LspLoopTurnV0::Outputs(outputs) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, hover_btn_request(42))
else {
return Err(std::io::Error::other("pre-cancelled request must never be dispatched").into());
};
let codes = outputs
.iter()
.map(|output| output.value.pointer("/error/code").cloned())
.collect::<Vec<_>>();
assert_eq!(
codes,
vec![Some(json!(REQUEST_CANCELLED_ERROR_CODE))],
"pre-cancelled request must get the cancelled error on the loop"
);
let notification = json!({
"jsonrpc": "2.0",
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": APP_STYLE_URI,
},
"position": {
"line": 1,
"character": 2,
},
},
});
let LspLoopTurnV0::Outputs(outputs) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, notification)
else {
return Err(std::io::Error::other("a notification must never be dispatched").into());
};
assert_eq!(outputs, Vec::new());
Ok(())
}
#[test]
fn dispatched_query_cancellation_replaces_the_computed_payload_once() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let LspLoopTurnV0::DispatchQuery(dispatch) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, hover_btn_request(51))
else {
return Err(std::io::Error::other("hover request must dispatch").into());
};
let (computed_tx, computed_rx) = mpsc::sync_channel(0);
let (release_tx, release_rx) = mpsc::sync_channel(0);
let worker = std::thread::spawn(move || {
let computed = resolve_dispatched_query_response(&dispatch);
computed_tx
.send(())
.map_err(|_| std::io::Error::other("test coordinator dropped computed signal"))?;
release_rx
.recv()
.map_err(|_| std::io::Error::other("test coordinator dropped release signal"))?;
let first = complete_dispatched_query_response(&dispatch, computed.clone());
let duplicate = complete_dispatched_query_response(&dispatch, computed);
Ok::<_, std::io::Error>((first, duplicate))
});
computed_rx
.recv()
.map_err(|_| std::io::Error::other("worker did not reach the completion boundary"))?;
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": CANCEL_REQUEST_METHOD,
"params": { "id": 51 },
}),
);
release_tx
.send(())
.map_err(|_| std::io::Error::other("worker dropped the completion boundary"))?;
let (response, duplicate) = worker
.join()
.map_err(|_| std::io::Error::other("query worker test thread panicked"))??;
let response = response
.ok_or_else(|| std::io::Error::other("cancelled request must receive a response"))?;
assert_eq!(
response.pointer("/error/code"),
Some(&json!(REQUEST_CANCELLED_ERROR_CODE))
);
assert!(
response.get("result").is_none(),
"computed result must be suppressed"
);
assert!(duplicate.is_none(), "one dispatch must never emit twice");
assert_eq!(state.snapshot().suppressed_dispatched_result_count, 1);
assert_eq!(state.snapshot().cancelled_request_count, 0);
Ok(())
}
#[test]
fn reused_request_id_keeps_the_new_generation_registered() -> TestResult {
let mut state = LspShellState::default();
open_query_dispatch_workspace(&mut state);
let LspLoopTurnV0::DispatchQuery(first) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, hover_btn_request(61))
else {
return Err(std::io::Error::other("first hover request must dispatch").into());
};
let first_response = resolve_dispatched_query_response(&first);
assert!(complete_dispatched_query_response(&first, first_response).is_some());
let LspLoopTurnV0::DispatchQuery(second) =
handle_lsp_message_scheduled_outputs_or_dispatch(&mut state, hover_btn_request(61))
else {
return Err(std::io::Error::other("reused hover request id must dispatch").into());
};
assert!(
complete_dispatched_query_response(&first, None).is_none(),
"an old generation cannot complete twice"
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": CANCEL_REQUEST_METHOD,
"params": { "id": 61 },
}),
);
let second_response = resolve_dispatched_query_response(&second);
let response = complete_dispatched_query_response(&second, second_response)
.ok_or_else(|| std::io::Error::other("current generation must receive a response"))?;
assert_eq!(
response.pointer("/error/code"),
Some(&json!(REQUEST_CANCELLED_ERROR_CODE)),
"the old completion must not remove the current generation"
);
assert_eq!(state.snapshot().suppressed_dispatched_result_count, 1);
Ok(())
}