use super::handle_lsp_message;
use crate::tide::TideRepublishDemandV0;
use crate::{
LspShellState, apply_tide_workspace_republish_item, collect_tide_workspace_republish_streaming,
complete_tide_workspace_republish, enable_deferred_external_sif_refresh,
prepare_tide_workspace_republish_job,
};
use serde_json::json;
fn open_document(state: &mut LspShellState, uri: &str, language_id: &str, text: &str) {
handle_lsp_message(
state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": uri,
"languageId": language_id,
"version": 1,
"text": text,
},
},
}),
);
}
fn republish_fixture_state() -> LspShellState {
let mut state = LspShellState::default();
enable_deferred_external_sif_refresh(&mut state);
open_document(
&mut state,
"file:///workspace/src/Alpha.module.scss",
"scss",
".alpha { color: red; }",
);
open_document(
&mut state,
"file:///workspace/src/Beta.module.scss",
"scss",
".beta { color: blue; }",
);
state
}
fn settle_sif_lane(state: &mut LspShellState) -> Result<(), &'static str> {
let sif_job = crate::prepare_deferred_external_sif_refresh_job(state)
.ok_or("startup SIF demand must flush")?;
let sif_result = crate::collect_deferred_external_sif_refresh(sif_job);
crate::apply_deferred_external_sif_refresh_result(state, sif_result);
Ok(())
}
#[test]
fn republish_tide_round_trip_covers_the_corpus() -> Result<(), &'static str> {
let mut state = republish_fixture_state();
let tick = 0;
state
.tide_republish_lane
.deposit(TideRepublishDemandV0::All, tick);
assert!(
prepare_tide_workspace_republish_job(&mut state, true).is_none(),
"republish must wait for the SIF lane to settle"
);
settle_sif_lane(&mut state)?;
let job = prepare_tide_workspace_republish_job(&mut state, true)
.ok_or("settled frontier + idle courtesy must flush")?;
let generation = job.generation;
assert!(
prepare_tide_workspace_republish_job(&mut state, true).is_none(),
"one in-flight tide per lane"
);
let chunks = std::sync::Mutex::new(Vec::new());
collect_tide_workspace_republish_streaming(job, &|result| {
let Ok(mut chunks) = chunks.lock() else {
return false;
};
chunks.push(result);
true
});
let chunks = chunks
.into_inner()
.map_err(|_| "streaming chunks mutex should not be poisoned")?;
assert!(
chunks.last().is_some_and(|chunk| chunk.final_chunk),
"the stream must terminate with a final chunk"
);
let mut items = Vec::new();
let mut uncovered = Vec::new();
for chunk in chunks {
assert_eq!(chunk.generation, generation);
items.extend(chunk.items);
uncovered.extend(chunk.uncovered_uris);
}
assert_eq!(
items.len() + uncovered.len(),
2,
"every corpus target is either covered or reported uncovered"
);
let mut published = 0usize;
for item in items {
let outputs = apply_tide_workspace_republish_item(&mut state, item);
assert!(!outputs.is_empty(), "an applied item must publish");
published += 1;
}
let effects = complete_tide_workspace_republish(&mut state, generation, uncovered.clone());
assert!(
published > 0 || !effects.deferred_diagnostics.is_empty() || !effects.outputs.is_empty(),
"the tide must reach every target through the wave or the fallback arm"
);
assert!(
!state.tide_republish_lane.in_flight(),
"completion re-arms the lane"
);
Ok(())
}
#[test]
fn cone_flush_targets_only_the_seed_closure() -> Result<(), &'static str> {
let mut state = LspShellState::default();
enable_deferred_external_sif_refresh(&mut state);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{"uri": "file:///workspace", "name": "workspace"},
],
},
}),
);
open_document(
&mut state,
"file:///workspace/src/Tokens.module.scss",
"scss",
"$brand: red;\n.token { color: $brand; }",
);
open_document(
&mut state,
"file:///workspace/src/Importer.module.scss",
"scss",
"@use \"./Tokens.module.scss\" as tokens;\n.importer { color: red; }",
);
open_document(
&mut state,
"file:///workspace/src/Bystander.module.scss",
"scss",
".bystander { color: green; }",
);
open_document(
&mut state,
"file:///workspace/src/App.tsx",
"typescriptreact",
"import styles from './Importer.module.scss';\nexport const a = styles.importer;",
);
settle_sif_lane(&mut state)?;
if let Some(job) = prepare_tide_workspace_republish_job(&mut state, true) {
let generation = job.generation;
collect_tide_workspace_republish_streaming(job, &|_| true);
let _ = complete_tide_workspace_republish(&mut state, generation, Vec::new());
}
let _ = crate::resolve_style_diagnostics_for_uri(
&state,
"file:///workspace/src/Tokens.module.scss",
);
let token_id = state
.document_file_id("file:///workspace/src/Tokens.module.scss")
.ok_or("token document must be interned")?;
state
.tide_republish_lane
.deposit(TideRepublishDemandV0::cone([token_id]), 1);
let job = prepare_tide_workspace_republish_job(&mut state, true).ok_or("cone must flush")?;
let uris = job.target_uris_for_test();
assert!(
uris.iter().any(|uri| uri.ends_with("Tokens.module.scss")),
"the seed itself is a target: {uris:?}"
);
assert!(
!uris
.iter()
.any(|uri| uri.ends_with("Bystander.module.scss")),
"a file outside the seed's reverse closure must NOT be a target: {uris:?}"
);
let generation = job.generation;
collect_tide_workspace_republish_streaming(job, &|_| true);
let effects = complete_tide_workspace_republish(&mut state, generation, Vec::new());
let touches = |uri: &str| {
effects
.deferred_diagnostics
.iter()
.any(|dispatch| dispatch.uri == uri)
|| effects.outputs.iter().any(|output| {
output
.value
.pointer("/params/uri")
.and_then(serde_json::Value::as_str)
== Some(uri)
})
};
assert!(
touches("file:///workspace/src/App.tsx"),
"a cone completion refreshes the cone's dependent sources"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn file_id_cone_matches_alias_heavy_path_closure_without_per_hop_derivation()
-> Result<(), Box<dyn std::error::Error>> {
use std::collections::{BTreeMap, BTreeSet};
let workspace_path = std::env::temp_dir().join(format!(
"omena-tide-file-id-alias-{}-{}",
std::process::id(),
crate::current_time_millis()
));
let real_src = workspace_path.join("real-src");
let alias_src = workspace_path.join("alias-src");
std::fs::create_dir_all(real_src.as_path())?;
std::os::unix::fs::symlink(real_src.as_path(), alias_src.as_path())?;
let token_path = real_src.join("Tokens.module.scss");
let importer_path = real_src.join("Importer.module.scss");
let bystander_path = real_src.join("Bystander.module.scss");
for (path, text) in [
(&token_path, ".token { color: red; }"),
(&importer_path, ".importer { color: blue; }"),
(&bystander_path, ".bystander { color: green; }"),
] {
std::fs::write(path, text)?;
}
let token_uri = crate::protocol::path_to_file_uri(token_path.as_path());
let token_alias_uri =
crate::protocol::path_to_file_uri(alias_src.join("Tokens.module.scss").as_path());
let importer_uri = crate::protocol::path_to_file_uri(importer_path.as_path());
let bystander_uri = crate::protocol::path_to_file_uri(bystander_path.as_path());
let mut state = LspShellState::default();
for (uri, text) in [
(token_uri.as_str(), ".token { color: red; }"),
(importer_uri.as_str(), ".importer { color: blue; }"),
(bystander_uri.as_str(), ".bystander { color: green; }"),
] {
open_document(&mut state, uri, "scss", text);
}
let token_id = state
.document_file_id(token_alias_uri.as_str())
.ok_or("symlink alias must resolve to the admitted token id")?;
let importer_id = state
.document_file_id(importer_uri.as_str())
.ok_or("importer id")?;
let raw_index = omena_query::ReverseDependencyIndexV0 {
rev: BTreeMap::from([(
token_alias_uri.clone(),
BTreeSet::from([importer_uri.clone()]),
)]),
edges_by_from: BTreeMap::new(),
};
let file_id_rev =
crate::diagnostics_scheduler::reverse_dependency_file_id_mirror(&state, &raw_index);
assert_eq!(
file_id_rev,
BTreeMap::from([(token_id, BTreeSet::from([importer_id]))]),
"the production mirror builder must collapse the symlink alias onto the admitted ids"
);
*state.reverse_dependency_index_memo.borrow_mut() =
Some(crate::state::LspReverseDependencyIndexMemo {
revision: 1,
summary_hash: "alias-heavy-fixture".to_string(),
ledger_epoch: 0,
index: raw_index.clone(),
file_id_rev,
});
let raw_seeds = BTreeSet::from([token_alias_uri]);
let mut previous_paths = crate::diagnostics_scheduler::reverse_dependency_closure_for_lsp_paths(
&raw_index, &raw_seeds,
);
previous_paths.extend(raw_seeds);
let mut previous = previous_paths
.iter()
.filter_map(|uri| state.document_file_id(uri))
.filter_map(|file_id| state.document_for_file_id(file_id))
.map(|document| document.uri.clone())
.collect::<Vec<_>>();
previous.sort();
previous.dedup();
crate::diagnostics_scheduler::republish_alias_derivation_probe::reset();
let mut interned = crate::diagnostics_follow_up::tide_republish_target_uris(
&state,
&TideRepublishDemandV0::cone([token_id]),
);
interned.sort();
assert_eq!(
interned, previous,
"the interned cone must preserve the alias-heavy target set byte-for-byte"
);
assert_eq!(
crate::diagnostics_scheduler::republish_alias_derivation_probe::read(),
0,
"the file-id closure must not derive URI aliases at each graph hop"
);
assert!(
!interned.iter().any(|uri| uri == &bystander_uri),
"the unrelated document stays outside both closures"
);
let _ = std::fs::remove_dir_all(workspace_path);
Ok(())
}
#[test]
fn disowned_republish_tide_drops_leftovers_and_rearms() -> Result<(), &'static str> {
let mut state = republish_fixture_state();
settle_sif_lane(&mut state)?;
state
.tide_republish_lane
.deposit(TideRepublishDemandV0::All, 0);
let job = prepare_tide_workspace_republish_job(&mut state, true).ok_or("gate must open")?;
let generation = job.generation;
state.tide_reopen_republish_window(crate::tide::TideDisownCauseV0::all(
crate::tide::TideInputKindV0::DocumentSet,
));
assert!(state.tide_republish_lane_generation() > generation);
assert!(
state.tide_republish_lane.has_demand(),
"the disowned tide's coverage carries over into the reopened window"
);
let chunks = std::sync::Mutex::new(Vec::new());
collect_tide_workspace_republish_streaming(job, &|result| {
let Ok(mut chunks) = chunks.lock() else {
return false;
};
chunks.push(result);
true
});
let chunks = chunks
.into_inner()
.map_err(|_| "streaming chunks mutex should not be poisoned")?;
assert!(
chunks.iter().all(|chunk| chunk.items.is_empty()),
"an aborted wave covers nothing"
);
assert!(chunks.last().is_some_and(|chunk| chunk.final_chunk));
let uncovered: Vec<String> = chunks
.into_iter()
.flat_map(|chunk| chunk.uncovered_uris)
.collect();
let effects = complete_tide_workspace_republish(&mut state, generation, uncovered);
assert!(
effects.outputs.is_empty() && effects.deferred_diagnostics.is_empty(),
"a disowned tide must not schedule fallback work"
);
assert!(!state.tide_republish_lane.in_flight());
Ok(())
}
#[test]
fn disown_collision_census_attributes_in_cone_and_out_of_cone_drivers()
-> Result<(), Box<dyn std::error::Error>> {
let workspace_path = std::env::temp_dir().join(format!(
"omena-tide-disown-census-{}-{}",
std::process::id(),
crate::current_time_millis()
));
let src_dir = workspace_path.join("src");
std::fs::create_dir_all(src_dir.as_path())?;
let alpha_path = src_dir.join("Alpha.module.scss");
let bystander_path = src_dir.join("Bystander.module.scss");
let external_path = src_dir.join("_External.scss");
let app_path = src_dir.join("App.tsx");
std::fs::write(alpha_path.as_path(), ".alpha { color: red; }\n")?;
std::fs::write(bystander_path.as_path(), ".bystander { color: green; }\n")?;
std::fs::write(external_path.as_path(), "$brand: blue;\n")?;
std::fs::write(
app_path.as_path(),
"import styles from './Alpha.module.scss';\nconst view = styles.al;\n",
)?;
let workspace_uri = crate::protocol::path_to_file_uri(workspace_path.as_path());
let alpha_uri = crate::protocol::path_to_file_uri(alpha_path.as_path());
let bystander_uri = crate::protocol::path_to_file_uri(bystander_path.as_path());
let external_uri = crate::protocol::path_to_file_uri(external_path.as_path());
let app_uri = crate::protocol::path_to_file_uri(app_path.as_path());
let mut state = LspShellState::default();
enable_deferred_external_sif_refresh(&mut state);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { "workspaceFolders": [{
"uri": workspace_uri,
"name": "workspace",
}] },
}),
);
open_document(
&mut state,
alpha_uri.as_str(),
"scss",
".alpha { color: red; }",
);
open_document(
&mut state,
bystander_uri.as_str(),
"scss",
".bystander { color: green; }",
);
open_document(
&mut state,
app_uri.as_str(),
"typescriptreact",
"import styles from './Alpha.module.scss';\nconst view = styles.al;",
);
settle_sif_lane(&mut state)?;
if let Some(startup) = prepare_tide_workspace_republish_job(&mut state, true) {
let generation = startup.generation;
collect_tide_workspace_republish_streaming(startup, &|_| true);
let _ = complete_tide_workspace_republish(&mut state, generation, Vec::new());
}
let _ = crate::resolve_style_diagnostics_for_uri(&state, alpha_uri.as_str());
let alpha_id = state
.document_file_id(alpha_uri.as_str())
.ok_or("alpha document must be interned")?;
state
.tide_republish_lane
.deposit(TideRepublishDemandV0::cone([alpha_id]), state.tide_tick);
let first = prepare_tide_workspace_republish_job(&mut state, true)
.ok_or("the alpha cone must enter flight")?;
assert!(
!first
.target_uris_for_test()
.iter()
.any(|uri| crate::protocol::file_uri_equivalent(uri, bystander_uri.as_str())),
"the seeded edit must be outside the frozen alpha cone"
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": { "uri": bystander_uri, "version": 2 },
"contentChanges": [{
"text": format!(
"@use \"{}\" as external;\n.bystander {{ color: external.$brand; }}",
external_uri
),
}],
},
}),
);
let hover = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": alpha_uri },
"position": { "line": 0, "character": 2 },
},
}),
);
assert!(
hover
.as_ref()
.and_then(|value| value.pointer("/result/contents"))
.is_some(),
"the scripted hover must execute against the measured workspace"
);
let completion = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 3,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": app_uri },
"position": { "line": 1, "character": 22 },
},
}),
);
assert!(
completion
.as_ref()
.and_then(|value| value.pointer("/result/items"))
.and_then(serde_json::Value::as_array)
.is_some(),
"the scripted completion must execute against the measured workspace"
);
settle_sif_lane(&mut state)?;
let second = prepare_tide_workspace_republish_job(&mut state, true)
.ok_or("carried demand must enter a second flight")?;
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "workspace/didChangeConfiguration",
"params": { "settings": { "omena": { "diagnostics": {
"severity": "error",
"deepAnalysis": true,
} } } },
}),
);
let snapshot = state.snapshot();
let total: u64 = snapshot.tide_disowns_total.values().sum();
let out_of_cone: u64 = snapshot.tide_disowns_out_of_cone.values().sum();
assert_eq!(
snapshot.tide_disowns_total.get("documentSet"),
Some(&1),
"the unrelated document-set edit must disown one tide"
);
assert_eq!(
snapshot.tide_disowns_out_of_cone.get("documentSet"),
Some(&1),
"the deliberately seeded unrelated edit must be classified out-of-cone"
);
assert_eq!(
snapshot.tide_disowns_total.get("diagnosticSettings"),
Some(&1),
"the global diagnostic-settings driver must disown the second tide"
);
assert_eq!(
snapshot.tide_disowns_out_of_cone.get("diagnosticSettings"),
Some(&0),
"a global setting overlaps every in-flight cone"
);
assert_eq!((total, out_of_cone), (2, 1));
println!(
"tide-disown-census total={total} out_of_cone={out_of_cone} ratio={:.2}",
out_of_cone as f64 / total as f64
);
drop(first);
drop(second);
let _ = std::fs::remove_dir_all(workspace_path);
Ok(())
}
#[cfg(feature = "salsa-style-diagnostics")]
mod sif_delta_seeding {
use crate::LspShellState;
use crate::external_sif_loader::republish_demand_for_external_sif_delta;
use crate::state::LspReverseDependencyIndexMemo;
use crate::tide::TideRepublishDemandV0;
use omena_query::{OmenaQueryExternalSifInputV0, ReverseDependencyIndexV0};
use std::collections::{BTreeMap, BTreeSet};
fn external_sif(url: &str, content: &[u8]) -> Option<OmenaQueryExternalSifInputV0> {
let sif = omena_sif::OmenaSifV1::from_static_exports(
url,
omena_sif::OmenaSifGeneratorV1 {
name: "fixture".to_string(),
version: "0.1.0".to_string(),
toolchain_id: "fixture@0.1.0".to_string(),
},
omena_sif::OmenaSifSourceV1 {
syntax: omena_sif::OmenaSifSourceSyntaxV1::Scss,
},
omena_sif::OmenaSifExportsV1 {
variables: Vec::new(),
mixins: Vec::new(),
functions: Vec::new(),
placeholders: Vec::new(),
forwards: Vec::new(),
},
Vec::new(),
content,
)
.ok()?;
Some(OmenaQueryExternalSifInputV0 {
canonical_url: url.to_string(),
sif,
})
}
fn state_with_reverse_index(edges: &[(&str, &str)]) -> LspShellState {
let mut rev: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut state = LspShellState::default();
let mut file_id_rev: BTreeMap<crate::LspFileId, BTreeSet<crate::LspFileId>> =
BTreeMap::new();
for (target, dependent) in edges {
rev.entry(target.to_string())
.or_default()
.insert(dependent.to_string());
let target_id = state.intern_file_uri(target);
let dependent_id = state.intern_file_uri(dependent);
file_id_rev
.entry(target_id)
.or_default()
.insert(dependent_id);
}
*state.reverse_dependency_index_memo.borrow_mut() = Some(LspReverseDependencyIndexMemo {
revision: 1,
summary_hash: "fixture".to_string(),
ledger_epoch: 0,
index: ReverseDependencyIndexV0 {
rev,
edges_by_from: BTreeMap::new(),
},
file_id_rev,
});
state
}
#[test]
fn changed_sif_with_attributed_importers_seeds_a_cone() -> Result<(), &'static str> {
let url = "https://cdn.example/tokens.scss";
let importer = "file:///workspace/src/User.module.scss";
let mut state = state_with_reverse_index(&[(url, importer)]);
let importer_id = state
.document_file_id(importer)
.ok_or("importer must be interned")?;
state.resolution.external_sifs = vec![external_sif(url, b"$brand: red;").ok_or("old sif")?];
let next = vec![external_sif(url, b"$brand: blue;").ok_or("new sif")?];
assert_eq!(
republish_demand_for_external_sif_delta(&state, next.as_slice()),
TideRepublishDemandV0::cone([importer_id]),
);
Ok(())
}
#[test]
fn unattributed_url_and_missing_index_widen_to_all() -> Result<(), &'static str> {
let url = "https://cdn.example/tokens.scss";
let mut state = state_with_reverse_index(&[("https://other.example/x.scss", "file:///a")]);
state.resolution.external_sifs = Vec::new();
let next = vec![external_sif(url, b"$brand: red;").ok_or("sif")?];
assert_eq!(
republish_demand_for_external_sif_delta(&state, next.as_slice()),
TideRepublishDemandV0::All,
"an unattributable changed url must widen"
);
let mut cold = LspShellState::default();
cold.resolution.external_sifs = Vec::new();
assert_eq!(
republish_demand_for_external_sif_delta(&cold, next.as_slice()),
TideRepublishDemandV0::All,
"no reverse index (cold start) must widen"
);
Ok(())
}
#[test]
fn stale_reverse_index_widens_to_all() -> Result<(), &'static str> {
let url = "https://cdn.example/tokens.scss";
let importer = "file:///workspace/src/User.module.scss";
let mut state = state_with_reverse_index(&[(url, importer)]);
state.resolution.external_sifs = vec![external_sif(url, b"$brand: red;").ok_or("old sif")?];
state
.tide_ledger
.advance(&[crate::tide::TideInputKindV0::DocumentText]);
let next = vec![external_sif(url, b"$brand: blue;").ok_or("new sif")?];
assert_eq!(
republish_demand_for_external_sif_delta(&state, next.as_slice()),
TideRepublishDemandV0::All,
"a stale reverse index must widen, never guess"
);
Ok(())
}
#[test]
fn unchanged_sif_set_deposits_nothing() -> Result<(), &'static str> {
let url = "https://cdn.example/tokens.scss";
let mut state = state_with_reverse_index(&[(url, "file:///a")]);
let sif = external_sif(url, b"$brand: red;").ok_or("sif")?;
state.resolution.external_sifs = vec![sif.clone()];
assert_eq!(
republish_demand_for_external_sif_delta(&state, std::slice::from_ref(&sif)),
TideRepublishDemandV0::None,
);
Ok(())
}
}
#[test]
fn completion_refreshes_open_source_documents_against_the_settled_corpus()
-> Result<(), &'static str> {
let mut state = republish_fixture_state();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace/src/App.tsx",
"languageId": "typescriptreact",
"version": 1,
"text": "import styles from \"./Alpha.module.scss\";\nconst view = <div className={styles.alpha} />;",
},
},
}),
);
settle_sif_lane(&mut state)?;
state
.tide_republish_lane
.deposit(TideRepublishDemandV0::All, 0);
let job = prepare_tide_workspace_republish_job(&mut state, true).ok_or("gate must open")?;
let generation = job.generation;
collect_tide_workspace_republish_streaming(job, &|_| true);
let effects = complete_tide_workspace_republish(&mut state, generation, Vec::new());
let refreshes_source = effects
.deferred_diagnostics
.iter()
.any(|dispatch| dispatch.uri == "file:///workspace/src/App.tsx")
|| effects.outputs.iter().any(|output| {
output
.value
.pointer("/params/uri")
.and_then(serde_json::Value::as_str)
== Some("file:///workspace/src/App.tsx")
});
assert!(
refreshes_source,
"a current-generation completion must re-enter open SOURCE documents through the per-file arm — their diagnostics were computed against a pre-settle corpus"
);
Ok(())
}