use std::sync::Arc;
use std::time::Duration;
use tower_lsp_server::ls_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position};
use crate::deps_dev::deps_dev_system;
use crate::osv::ScanOutcome;
use crate::{
ConcreteVersion, Dependency, DependencySource, Deprecation, ParseResult, ProvenanceStatus,
PublishTime, Registry, SupplyChainTrustSignal, Version, VersionReq, format_relative_age,
is_within_cooldown,
};
use super::{
EcosystemFormatter, HOVER_RECENT_VERSIONS, VersionData, escape_markdown, in_use_version,
markdown_code_span, position_in_range,
};
const DEPS_DEV_WAIT_BUDGET: Duration = Duration::from_millis(700);
fn version_age_suffix(version: &dyn Version, now: PublishTime) -> String {
version
.published_at()
.map(|published| format!(" — {}", format_relative_age(published.age_secs_from(now))))
.unwrap_or_default()
}
const HOVER_FALLBACK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
pub const CMD_DOT_FOOTER: &str = "\n---\n⌨️ **Press `Cmd+.` to update version**";
pub async fn generate_hover<R: Registry + ?Sized>(
parse_result: &dyn ParseResult,
position: Position,
versions: VersionData<'_>,
registry: &R,
formatter: &dyn EcosystemFormatter,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
) -> Option<Hover> {
let dep = parse_result.dependencies().into_iter().find(|d| {
let on_name = position_in_range(position, d.name_range());
let on_version = d
.version_range()
.is_some_and(|r| position_in_range(position, r));
on_name || on_version
})?;
let dep_source = dep.source();
let resolvable = formatter.can_resolve_source(&dep_source);
let normalized_name = formatter.normalize_package_name(dep.name());
let trust_handle = spawn_trust_signal_fetch(
dep,
&dep_source,
&versions,
formatter,
normalized_name.as_str(),
);
let available_versions = if resolvable {
registry
.get_versions_from(dep.name(), &dep_source, freshness)
.await
.ok()
} else {
None
};
let url = (!formatter.suppress_package_url(&dep_source))
.then(|| formatter.package_url(dep.name()))
.filter(|u| !u.is_empty());
let mut markdown = String::with_capacity(512);
push_header_hover_section(&mut markdown, dep, url.as_deref());
let resolved: Option<&str> = if formatter.manifest_requirement_is_resolved_version(dep) {
dep.version_requirement().map(VersionReq::as_str)
} else {
versions
.resolved
.get(normalized_name.as_str())
.or_else(|| versions.resolved.get(dep.name()))
.map(ConcreteVersion::as_str)
};
push_current_or_requirement_hover_section(&mut markdown, dep, resolved);
push_markers_hover_section(&mut markdown, dep);
let wildcard_req = VersionReq::new("*");
let live_latest_idx = available_versions
.as_ref()
.and_then(|v| registry.select_latest_matching(v, &wildcard_req));
let list_fallback_latest = if available_versions.as_ref().is_some_and(|v| !v.is_empty())
&& live_latest_idx.is_none()
{
match tokio::time::timeout(
HOVER_FALLBACK_TIMEOUT,
registry.get_latest_matching_from(dep.name(), &dep_source, &wildcard_req, None),
)
.await
{
Ok(Ok(found)) => {
tracing::debug!(package = %dep.name(), found = found.is_some(), "hover latest fallback (get_latest_matching) resolved");
found
}
Ok(Err(error)) => {
tracing::warn!(package = %dep.name(), %error, "hover latest fallback (get_latest_matching) failed");
None
}
Err(_) => {
tracing::warn!(
package = %dep.name(),
timeout_secs = HOVER_FALLBACK_TIMEOUT.as_secs(),
"hover latest fallback (get_latest_matching) timed out"
);
None
}
}
} else {
None
};
let cached_latest = resolvable
.then(|| {
versions
.cached
.get(normalized_name.as_str())
.or_else(|| versions.cached.get(dep.name()))
})
.flatten();
let latest_line: Option<(&str, Option<PublishTime>)> = match &available_versions {
Some(v) if !v.is_empty() => live_latest_idx
.map(|idx| &v[idx])
.map(|live| (live.version_string().as_str(), live.published_at()))
.or_else(|| {
list_fallback_latest
.as_deref()
.map(|live| (live.version_string().as_str(), live.published_at()))
}),
_ => cached_latest.map(|v| (v.latest.as_str(), v.published_at)),
};
push_latest_hover_section(&mut markdown, latest_line, freshness, now);
let vuln_key = versions.ecosystem.and_then(|ecosystem| {
crate::osv::vulnerability_keys(parse_result, versions.resolved, formatter, ecosystem)
.remove(&dep.name_range())
});
let vuln_outcome = versions.vulnerabilities.and_then(|m| {
vuln_key
.as_deref()
.and_then(|key| m.get(key))
.or_else(|| m.get(&normalized_name))
.or_else(|| m.get(dep.name().as_str()))
});
let deprecation = versions
.outcomes
.and_then(|o| o.deprecation(&normalized_name));
push_deprecation_hover_section(&mut markdown, formatter, deprecation);
push_vulnerability_hover_section(&mut markdown, vuln_outcome);
let trust_signal = match trust_handle {
Some(handle) => match tokio::time::timeout(DEPS_DEV_WAIT_BUDGET, handle).await {
Ok(Ok(signal)) => signal,
Ok(Err(_)) | Err(_) => None,
},
None => None,
};
push_trust_signal_hover_section(&mut markdown, trust_signal.as_ref());
if let Some(available_versions) = available_versions.as_ref().filter(|v| !v.is_empty()) {
push_recent_versions_hover_section(
&mut markdown,
available_versions,
live_latest_idx,
freshness,
now,
formatter,
);
}
push_cmd_dot_footer_hover_section(
&mut markdown,
resolvable,
available_versions.as_deref(),
cached_latest,
vuln_outcome,
deprecation,
versions.offline,
);
push_offline_footer_hover_section(&mut markdown, resolvable, versions.offline);
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: markdown,
}),
range: Some(dep.name_range()),
})
}
fn spawn_trust_signal_fetch(
dep: &dyn Dependency,
dep_source: &DependencySource,
versions: &VersionData<'_>,
formatter: &dyn EcosystemFormatter,
normalized_name: &str,
) -> Option<tokio::task::JoinHandle<Option<SupplyChainTrustSignal>>> {
versions.trust.and_then(|client| {
let ecosystem = versions.ecosystem?;
let system = deps_dev_system(ecosystem)?;
if versions.offline || !formatter.source_is_public_registry_content(dep_source) {
return None;
}
let version = in_use_version(
dep,
normalized_name,
versions.resolved,
formatter,
ecosystem,
)?;
let client = Arc::clone(client);
let name = dep.name().to_string();
Some(tokio::spawn(async move {
client.trust_signal(system, &name, &version).await
}))
})
}
fn push_header_hover_section(markdown: &mut String, dep: &dyn Dependency, url: Option<&str>) {
use std::fmt::Write as _;
match url {
Some(url) => write!(
markdown,
"# [{}]({})\n\n",
escape_markdown(dep.name().as_str()),
url
),
None => write!(markdown, "# {}\n\n", escape_markdown(dep.name().as_str())),
}
.unwrap();
}
fn push_current_or_requirement_hover_section(
markdown: &mut String,
dep: &dyn Dependency,
resolved: Option<&str>,
) {
use std::fmt::Write as _;
if let Some(resolved_ver) = resolved {
write!(
markdown,
"**Current**: {}\n\n",
markdown_code_span(resolved_ver)
)
.unwrap();
} else if let Some(version_req) = dep.version_requirement() {
write!(
markdown,
"**Requirement**: {}\n\n",
markdown_code_span(version_req.as_str())
)
.unwrap();
}
}
fn push_markers_hover_section(markdown: &mut String, dep: &dyn Dependency) {
use std::fmt::Write as _;
if let Some(marker_expr) = dep.markers() {
write!(
markdown,
"**Active when**: {}\n\n",
markdown_code_span(marker_expr)
)
.unwrap();
}
}
fn push_latest_hover_section(
markdown: &mut String,
latest_line: Option<(&str, Option<PublishTime>)>,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
) {
use std::fmt::Write as _;
let Some((latest_ver, raw_published_at)) = latest_line else {
return;
};
let published_at = freshness.enabled.then_some(raw_published_at).flatten();
let age_secs = published_at.map(|p| p.age_secs_from(now));
write!(markdown, "**Latest**: {}", markdown_code_span(latest_ver)).unwrap();
if let Some(age_secs) = age_secs {
write!(markdown, " *(published {})*", format_relative_age(age_secs)).unwrap();
}
markdown.push_str("\n\n");
if age_secs.is_some_and(|age| is_within_cooldown(age, freshness.cooldown_secs)) {
markdown.push_str(
"> ⏳ **Recently published** — this release is still within the cooldown window.\n\
> It may still be yanked or superseded; consider verifying before upgrading.\n\n",
);
}
}
fn push_recent_versions_hover_section(
markdown: &mut String,
available_versions: &[Box<dyn Version>],
live_latest_idx: Option<usize>,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
formatter: &dyn EcosystemFormatter,
) {
use std::fmt::Write as _;
if available_versions.is_empty() {
return;
}
markdown.push_str("**Recent versions**:\n");
for (i, version) in available_versions
.iter()
.take(HOVER_RECENT_VERSIONS)
.enumerate()
{
let version_span = markdown_code_span(version.version_string().as_str());
let age_suffix = if freshness.enabled {
version_age_suffix(version.as_ref(), now)
} else {
String::new()
};
if Some(i) == live_latest_idx {
if version.removal_status().is_flagged() {
writeln!(
markdown,
"- {version_span} *(latest)* {}{age_suffix}",
formatter.yanked_label()
)
.unwrap();
} else {
writeln!(markdown, "- {version_span} *(latest)*{age_suffix}").unwrap();
}
} else if version.removal_status().is_flagged() {
writeln!(
markdown,
"- {} {}{}",
version_span,
formatter.yanked_label(),
age_suffix
)
.unwrap();
} else {
writeln!(markdown, "- {version_span}{age_suffix}").unwrap();
}
}
}
fn push_cmd_dot_footer_hover_section(
markdown: &mut String,
resolvable: bool,
available_versions: Option<&[Box<dyn Version>]>,
cached_latest: Option<&super::PackageVersions>,
vuln_outcome: Option<&ScanOutcome>,
deprecation: Option<&Deprecation>,
offline: bool,
) {
let has_offline_actionable_data = available_versions.is_some_and(|v| !v.is_empty())
|| cached_latest.is_some()
|| matches!(vuln_outcome, Some(ScanOutcome::Vulnerable(_)))
|| deprecation.is_some();
let live_fetch_definitively_empty = available_versions.is_some_and(<[_]>::is_empty);
let footer_actionable =
has_offline_actionable_data || (!live_fetch_definitively_empty && !offline);
if resolvable && footer_actionable {
markdown.push_str(CMD_DOT_FOOTER);
}
}
fn push_offline_footer_hover_section(markdown: &mut String, resolvable: bool, offline: bool) {
if offline && resolvable {
markdown.push_str("\n---\n📴 *Offline: version and vulnerability data not checked*");
}
}
const fn severity_label(severity: crate::osv::VulnSeverity) -> &'static str {
match severity {
crate::osv::VulnSeverity::Critical => "critical",
crate::osv::VulnSeverity::High => "high",
crate::osv::VulnSeverity::Medium => "medium",
crate::osv::VulnSeverity::Low => "low",
crate::osv::VulnSeverity::Unknown => "unknown severity",
}
}
fn push_deprecation_hover_section(
markdown: &mut String,
formatter: &dyn EcosystemFormatter,
deprecation: Option<&Deprecation>,
) {
use std::fmt::Write as _;
let Some(deprecation) = deprecation else {
return;
};
markdown.push_str("### Deprecated\n\n");
let _ = writeln!(markdown, "{}\n", formatter.deprecated_message());
if let Some(reason) = deprecation.reason.as_deref().filter(|r| !r.is_empty()) {
let _ = writeln!(markdown, "{}\n", escape_markdown(reason));
}
if let Some(replacement) = deprecation.replacement.as_deref().filter(|r| !r.is_empty()) {
let _ = writeln!(
markdown,
"Suggested replacement: {}\n",
markdown_code_span(replacement)
);
}
}
fn push_vulnerability_hover_section(markdown: &mut String, outcome: Option<&ScanOutcome>) {
use std::fmt::Write;
match outcome {
Some(ScanOutcome::Vulnerable(dv)) => {
markdown.push_str("### Security advisories\n\n");
for advisory in dv.advisories.items() {
writeln!(
markdown,
"- **[{}]({})** — {}",
escape_markdown(&advisory.id),
advisory.url,
severity_label(advisory.severity)
)
.unwrap();
writeln!(
markdown,
" {}",
escape_markdown(
advisory
.summary
.as_deref()
.unwrap_or("(no summary provided)")
)
)
.unwrap();
let mut details = Vec::with_capacity(2);
if let Some(fixed) = advisory.fixed_versions.last() {
details.push(format!("Fixed in: {}", markdown_code_span(fixed)));
}
if !advisory.aliases.is_empty() {
details.push(format!(
"Aliases: {}",
escape_markdown(&advisory.aliases.join(", "))
));
}
if !details.is_empty() {
writeln!(markdown, " {}", details.join(" \u{b7} ")).unwrap();
}
}
let remaining = dv.advisories.remaining();
if remaining > 0 {
writeln!(markdown, "- *(+{remaining} more advisories)*").unwrap();
}
if let crate::osv::UpgradeStatus::CandidateVulnerable { version, .. } =
&dv.upgrade_status
{
writeln!(
markdown,
"\n\u{26a0}\u{fe0f} Latest version {} is also affected.",
markdown_code_span(version)
)
.unwrap();
}
markdown.push('\n');
}
Some(ScanOutcome::Clean) => {
markdown.push_str("**No known vulnerabilities** (OSV.dev)\n\n");
}
Some(ScanOutcome::Skipped(_)) | None => {}
}
}
fn push_trust_signal_hover_section(markdown: &mut String, signal: Option<&SupplyChainTrustSignal>) {
use std::fmt::Write;
let Some(signal) = signal else {
return;
};
if signal.scorecard.is_none() && signal.provenance.is_none() {
return;
}
let mut parts: Vec<String> = Vec::with_capacity(2);
if let Some(scorecard) = &signal.scorecard {
let mut part = format!(
"OpenSSF Scorecard {}/10",
markdown_code_span(&format!("{:.1}", scorecard.overall_score))
);
if scorecard.self_reported {
part.push_str(" *(self-reported repo)*");
}
parts.push(part);
}
if let Some(provenance) = signal.provenance {
let label = match provenance {
ProvenanceStatus::Verified => "verified",
ProvenanceStatus::Unverified => "attested but unverified",
ProvenanceStatus::None => "none found",
};
parts.push(format!("Provenance: {label}"));
}
writeln!(
markdown,
"\u{1f510} **Supply chain**: {}",
parts.join(" \u{b7} ")
)
.unwrap();
markdown.push('\n');
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RemovalStatus;
use crate::lsp_helpers::test_support::*;
use crate::lsp_helpers::*;
use std::collections::HashMap;
use std::sync::Arc;
#[tokio::test]
async fn test_generate_hover_recent_versions_shows_age_when_known() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 2 * 24 * 60 * 60,
)),
}],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("- `1.2.3` *(latest)* — 2 days ago"),
"got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_recent_versions_omits_age_when_unknown() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("- `1.2.3` *(latest)*\n"));
assert!(!content.value.contains("ago"));
}
#[tokio::test]
async fn test_generate_hover_latest_marker_skips_prerelease_at_raw_top() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![
MockVersionWithAge {
version: "13.0.5-beta1".into(),
yanked: false,
published_at: None,
},
MockVersionWithAge {
version: "13.0.4".into(),
yanked: false,
published_at: None,
},
],
};
let parse_result = freshness_test_parse_result("Newtonsoft.Json");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `13.0.4`"),
"got: {}",
content.value
);
assert!(
content.value.contains("- `13.0.4` *(latest)*"),
"the stable version, not the raw-top pre-release, should carry the marker; got: {}",
content.value
);
assert!(
!content.value.contains("13.0.5-beta1` *(latest)*"),
"the pre-release must not be tagged latest; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_omitted_when_stable_outside_top_n() {
use std::collections::HashMap;
let mut versions: Vec<MockVersionWithAge> = (0..=HOVER_RECENT_VERSIONS)
.map(|i| MockVersionWithAge {
version: format!("2.0.0-alpha{i}").into(),
yanked: false,
published_at: None,
})
.collect();
versions.push(MockVersionWithAge {
version: "1.9.0".into(),
yanked: false,
published_at: None,
});
let registry = MockRegistryWithVersions { versions };
let parse_result = freshness_test_parse_result("example");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `1.9.0`"),
"got: {}",
content.value
);
assert!(
!content.value.contains("*(latest)*"),
"the stable latest isn't in the truncated top-N slice, so no entry should be marked; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_all_prerelease_degrades_gracefully() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![
MockVersionWithAge {
version: "2.0.0-beta2".into(),
yanked: false,
published_at: None,
},
MockVersionWithAge {
version: "2.0.0-beta1".into(),
yanked: false,
published_at: None,
},
MockVersionWithAge {
version: "1.9.0-alpha1".into(),
yanked: false,
published_at: None,
},
],
};
let parse_result = freshness_test_parse_result("example");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor, not panic");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("**Latest**:"),
"no stable version exists, so the header should be omitted rather than picking a pre-release; got: {}",
content.value
);
assert!(
!content.value.contains("*(latest)*"),
"no stable version exists, so no entry in the list should be marked latest; got: {}",
content.value
);
assert!(
content.value.contains("2.0.0-beta2"),
"the raw version list should still render even without a latest marker; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_all_prerelease_live_list_ignores_stale_cache() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "2.0.0-beta2".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = freshness_test_parse_result("example");
let mut cached_versions = HashMap::new();
cached_versions.insert("example".into(), PackageVersions::latest_only("1.5.0"));
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor, not panic");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("**Latest**:"),
"a live fetch with no stable entry must not fall back to a stale cached version \
that isn't in the live list; got: {}",
content.value
);
assert!(
!content.value.contains("1.5.0"),
"the stale cached version must not leak into the response at all; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_falls_back_to_get_latest_matching_when_list_pick_fails() {
use std::collections::HashMap;
let registry = MockRegistryListFailsLatestFallbackSucceeds {
versions: vec![MockVersionWithAge {
version: "v0.0.0-20230101000000-abcdef123456".into(),
yanked: false,
published_at: None,
}],
fallback_latest: MockVersionWithAge {
version: "v1.2.3".into(),
yanked: false,
published_at: None,
},
list_pick_index: None,
get_latest_matching_calls: std::sync::atomic::AtomicUsize::new(0),
};
let parse_result = freshness_test_parse_result("example.com/mod");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `v1.2.3`"),
"the list-based pick failed, so hover must fall back to get_latest_matching's \
result instead of omitting the Latest line; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_does_not_call_fallback_when_list_pick_succeeds() {
use std::collections::HashMap;
use std::sync::atomic::Ordering;
let registry = MockRegistryListFailsLatestFallbackSucceeds {
versions: vec![MockVersionWithAge {
version: "v1.2.3".into(),
yanked: false,
published_at: None,
}],
fallback_latest: MockVersionWithAge {
version: "v9.9.9".into(),
yanked: false,
published_at: None,
},
list_pick_index: Some(0),
get_latest_matching_calls: std::sync::atomic::AtomicUsize::new(0),
};
let parse_result = freshness_test_parse_result("example.com/mod");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `v1.2.3`"),
"expected the list-based pick's own version, not the fallback's; got: {}",
content.value
);
assert_eq!(
registry.get_latest_matching_calls.load(Ordering::Relaxed),
0,
"get_latest_matching must not be called when the list-based pick already succeeded"
);
}
#[tokio::test]
async fn test_generate_hover_renders_basic_card_when_fetch_fails() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&ErrorRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render on a fetch failure, not disappear entirely");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("serde"),
"basic card must still render the package name; got: {}",
content.value
);
assert!(
!content.value.contains("**Latest**"),
"no version data is available on a fetch failure; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_go_prefers_manifest_requirement_over_stale_resolved_version() {
use std::collections::HashMap;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "example.com/mod".into(),
version_req: "v0.8.1".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 16)),
}],
uri: crate::test_util::test_uri("/test/go.mod"),
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example.com/mod".into(), "v0.9.1".into());
let cached_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockGoFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Current**: `v0.8.1`"),
"expected hover to show go.mod's pinned version, got: {}",
content.value
);
assert!(
!content.value.contains("v0.9.1"),
"hover must not surface the stale go.sum version: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_non_go_formatter_uses_resolved_lockfile_version() {
use std::collections::HashMap;
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), "1.2.0".into());
let cached_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Current**: `1.2.0`"),
"expected hover to show the resolved lockfile version, got: {}",
content.value
);
assert!(!content.value.contains("**Current**: `1.0.0`"));
}
#[tokio::test]
async fn test_generate_hover_recent_versions_preserves_yanked_marker_with_age() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![
MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
},
MockVersionWithAge {
version: "1.2.1".into(),
yanked: true,
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 5 * 30 * 24 * 60 * 60,
)),
},
],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("- `1.2.1` *(yanked)* — 5 months ago"),
"got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_recent_versions_respects_freshness_disabled() {
use std::collections::HashMap;
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 2 * 24 * 60 * 60,
)),
}],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings {
enabled: false,
cooldown_secs: crate::freshness::DEFAULT_COOLDOWN_SECS,
},
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("- `1.2.3` *(latest)*\n"));
assert!(!content.value.contains("ago"));
}
#[tokio::test]
async fn test_generate_hover_latest_line_shows_age_and_cooldown_callout_when_within_cooldown() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("**Latest**: `2.0.0` *(published 1 hour ago)*"),
"got: {}",
content.value
);
assert!(
content.value.contains(
"> ⏳ **Recently published** — this release is still within the cooldown window."
),
"got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_line_no_callout_when_outside_cooldown() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 10 * 24 * 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("**Latest**: `2.0.0` *(published 1 week ago)*"),
"got: {}",
content.value
);
assert!(!content.value.contains("Recently published"));
}
#[tokio::test]
async fn test_generate_hover_latest_line_omits_age_when_published_at_unknown() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert("serde".into(), PackageVersions::latest_only("2.0.0"));
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("**Latest**: `2.0.0`\n\n"));
assert!(!content.value.contains("published"));
assert!(!content.value.contains("Recently published"));
}
#[tokio::test]
async fn test_generate_hover_latest_line_respects_freshness_disabled() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
PublishTime::now().as_unix_secs() - 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings {
enabled: false,
cooldown_secs: crate::freshness::DEFAULT_COOLDOWN_SECS,
},
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("**Latest**: `2.0.0`\n\n"));
assert!(!content.value.contains("published"));
assert!(!content.value.contains("Recently published"));
}
#[tokio::test]
async fn test_generate_hover_latest_line_cooldown_boundary_is_exclusive() {
use std::collections::HashMap;
const COOLDOWN_SECS: u64 = 100;
let now = PublishTime::from_unix_secs(10_000);
let published_at_at_boundary =
PublishTime::from_unix_secs(10_000 - COOLDOWN_SECS.cast_signed());
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(published_at_at_boundary),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings {
enabled: true,
cooldown_secs: COOLDOWN_SECS,
},
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Recently published"),
"age exactly equal to cooldown_secs must not be within cooldown, got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_line_cooldown_boundary_one_second_inside_shows_callout() {
use std::collections::HashMap;
const COOLDOWN_SECS: u64 = 100;
let now = PublishTime::from_unix_secs(10_000);
let published_at_just_inside =
PublishTime::from_unix_secs(10_000 - (COOLDOWN_SECS.cast_signed() - 1));
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "2.0.0".into(),
available: Arc::from(vec!["2.0.0".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(published_at_just_inside),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings {
enabled: true,
cooldown_secs: COOLDOWN_SECS,
},
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("Recently published"),
"age == cooldown_secs - 1 must be within cooldown, got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_line_prefers_live_fetch_over_stale_ch1_cache() {
use std::collections::HashMap;
let now = PublishTime::now();
let live_latest_published = PublishTime::from_unix_secs(now.as_unix_secs() - 60 * 60);
let registry = MockRegistryWithVersions {
versions: vec![
MockVersionWithAge {
version: "1.0.214".into(),
yanked: false,
published_at: Some(live_latest_published),
},
MockVersionWithAge {
version: "1.0.213".into(),
yanked: false,
published_at: Some(PublishTime::from_unix_secs(
now.as_unix_secs() - 30 * 24 * 60 * 60,
)),
},
],
};
let parse_result = freshness_test_parse_result("serde");
let mut cached_versions = HashMap::new();
cached_versions.insert(
"serde".into(),
PackageVersions {
latest: "1.0.213".into(),
available: Arc::from(vec!["1.0.213".into()]),
yanked: Arc::from(Vec::new()),
published_at: Some(PublishTime::from_unix_secs(
now.as_unix_secs() - 90 * 24 * 60 * 60,
)),
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("**Latest**: `1.0.214` *(published 1 hour ago)*"),
"Latest line must reflect the live Ch2 fetch, not the stale Ch1 cache entry \
`1.0.213`, got: {}",
content.value
);
assert!(
!content.value.contains("**Latest**: `1.0.213`"),
"must not render the stale Ch1 version, got: {}",
content.value
);
assert!(
content
.value
.contains("- `1.0.214` *(latest)* — 1 hour ago"),
"the Recent versions list's own *(latest)* entry must agree with the Latest \
line above it, got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_agrees_with_npm_shaped_deprecated_ranking() {
use std::collections::HashMap;
let now = PublishTime::now();
let registry = MockRegistryPreferringUnflagged {
versions: vec![
MockVersionWithStatus {
version: "2.0.0".into(),
status: RemovalStatus::AdvisoryDeprecated,
},
MockVersionWithStatus {
version: "1.9.0".into(),
status: RemovalStatus::Available,
},
],
};
let parse_result = freshness_test_parse_result("pkg");
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `1.9.0`"),
"hover must agree with select_latest_matching's non-deprecated-preferred pick \
(1.9.0), not a naive is_stable() scan that would pick the newer but deprecated \
2.0.0, got: {}",
content.value
);
assert!(
content.value.contains("- `1.9.0` *(latest)*"),
"the Recent versions list's own *(latest)* marker must agree with the Latest \
line above it, got: {}",
content.value
);
assert!(
content.value.contains("- `2.0.0` *(yanked)*"),
"2.0.0 must keep its flagged label even though it isn't the resolved latest, \
got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_keeps_flagged_label_when_resolved_version_is_flagged() {
use std::collections::HashMap;
let now = PublishTime::now();
let registry = MockRegistryPreferringUnflagged {
versions: vec![MockVersionWithStatus {
version: "2.0.0".into(),
status: RemovalStatus::AdvisoryDeprecated,
}],
};
let parse_result = freshness_test_parse_result("pkg");
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `2.0.0`"),
"the only version resolves as latest even though it's flagged, got: {}",
content.value
);
assert!(
content.value.contains("- `2.0.0` *(latest)* *(yanked)*"),
"the resolved latest must keep its flagged label instead of the warning \
silently vanishing behind *(latest)*, got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_latest_resolves_when_all_versions_yanked() {
use std::collections::HashMap;
let now = PublishTime::now();
let registry = MockRegistryPreferringUnflagged {
versions: vec![
MockVersionWithStatus {
version: "2.0.0".into(),
status: RemovalStatus::Yanked,
},
MockVersionWithStatus {
version: "1.9.0".into(),
status: RemovalStatus::Yanked,
},
],
};
let parse_result = freshness_test_parse_result("pkg");
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("**Latest**: `2.0.0`"),
"an all-yanked package still exists: hover must resolve the newest yanked \
version as latest rather than showing no Latest line, got: {}",
content.value
);
assert!(
content.value.contains("- `2.0.0` *(latest)* *(yanked)*"),
"the resolved latest must keep its yanked label, got: {}",
content.value
);
}
struct NpmLikeFormatter;
impl PackageNaming for NpmLikeFormatter {}
impl PackageRendering for NpmLikeFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &crate::PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for NpmLikeFormatter {}
impl DiagnosticMessages for NpmLikeFormatter {
fn yanked_label(&self) -> &'static str {
"*(deprecated)*"
}
}
impl DiagnosticPolicy for NpmLikeFormatter {}
impl SourcePolicy for NpmLikeFormatter {}
impl OsvNaming for NpmLikeFormatter {}
#[tokio::test]
async fn test_generate_hover_deprecated_section_and_per_row_labels_both_render() {
use std::collections::HashMap;
let now = PublishTime::now();
let registry = MockRegistryPreferringUnflagged {
versions: vec![MockVersionWithStatus {
version: "1.0.0".into(),
status: RemovalStatus::AdvisoryDeprecated,
}],
};
let parse_result = freshness_test_parse_result("pkg");
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let outcomes = crate::lsp_helpers::DependencyOutcomes::new().with_deprecation(
"pkg",
crate::Deprecation {
reason: Some("no longer maintained".to_string()),
replacement: None,
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions).with_outcomes(&outcomes),
®istry,
&NpmLikeFormatter,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("### Deprecated"),
"expected the package-level Deprecated section, got: {}",
content.value
);
assert!(
content.value.contains("no longer maintained"),
"expected the deprecation reason, got: {}",
content.value
);
assert!(
content
.value
.contains("This package is deprecated\n\nno longer maintained"),
"expected the message and reason on separate paragraphs, got: {}",
content.value
);
assert!(
content
.value
.contains("- `1.0.0` *(latest)* *(deprecated)*"),
"expected the pre-existing per-row label to still render alongside the new \
section (deliberately not deduped, S4), got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_surfaces_markers() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let parse_result = MockMarkedParseResult {
dep: MockMarkedDep {
name: "numpy".into(),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
markers: Some("python_full_version >= '3.9'".to_string()),
},
uri: crate::test_util::test_uri("/test/pyproject.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("**Active when**: `python_full_version >= '3.9'`")
);
}
#[tokio::test]
async fn test_generate_hover_omits_active_when_without_markers() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let parse_result = MockMarkedParseResult {
dep: MockMarkedDep {
name: "requests".into(),
name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
markers: None,
},
uri: crate::test_util::test_uri("/test/pyproject.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(!content.value.contains("Active when"));
}
#[tokio::test]
async fn test_generate_hover_escapes_malicious_dependency_name() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let malicious_name = "real-pkg](https://legit-looking-typosquat.example/download)[real-pkg";
let parse_result = MockParseResult {
deps: vec![MockDep {
name: malicious_name.into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(
Position::new(0, 0),
Position::new(0, malicious_name.len() as u32),
),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
let header_line = content
.value
.lines()
.next()
.expect("hover markdown has a header line");
let label = header_line
.strip_prefix("# [")
.expect("header starts with link label")
.split("](")
.next()
.expect("header contains label/url separator");
assert_eq!(
label,
r"real\-pkg\]\(https\:\/\/legit\-looking\-typosquat\.example\/download\)\[real\-pkg"
);
}
#[tokio::test]
async fn test_generate_hover_newline_in_name_cannot_forge_new_heading() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let malicious_name = "react\n# [fake](https://evil.example) <https://evil.example>";
let parse_result = MockParseResult {
deps: vec![MockDep {
name: malicious_name.into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(
Position::new(0, 0),
Position::new(0, malicious_name.len() as u32),
),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
let header_line = content
.value
.lines()
.next()
.expect("hover markdown has a header line");
let label = header_line
.strip_prefix("# [")
.expect("header starts with link label")
.split("](")
.next()
.expect("header contains label/url separator");
assert_eq!(label, escape_markdown(malicious_name));
assert!(!label.contains('\n'));
assert!(label.contains(r"\<https"));
}
#[tokio::test]
async fn test_generate_hover_marker_with_parens_renders_unescaped() {
use std::collections::HashMap;
use tower_lsp_server::ls_types::{Position, Range};
let marker = "python_version >= \"3.8\" and (sys_platform == \"linux\")";
let parse_result = MockMarkedParseResult {
dep: MockMarkedDep {
name: "numpy".into(),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
markers: Some(marker.to_string()),
},
uri: crate::test_util::test_uri("/test/pyproject.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains(&format!("**Active when**: `{marker}`"))
);
}
#[tokio::test]
async fn test_generate_hover_registry_sections_suppressed_for_non_registry_sources() {
use crate::parser::DependencySource;
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "9.9.9".into(),
yanked: false,
published_at: None,
}],
};
let cached_versions = {
let mut m = HashMap::new();
m.insert("dep".into(), PackageVersions::latest_only("9.9.9"));
m
};
let resolved_versions = HashMap::new();
let uri = crate::test_util::test_uri("/test/Cargo.toml");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("dep"),
DependencySource::CustomRegistry {
url: "my-corp".into(),
},
),
uri: uri.clone(),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(!content.value.contains("**Latest**"));
assert!(!content.value.contains("**Recent versions**"));
assert!(content.value.contains("**Requirement**"));
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let hover = generate_hover(
®istry_parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("**Latest**"));
assert!(content.value.contains("**Recent versions**"));
}
#[tokio::test]
async fn test_generate_hover_clean_outcome_states_no_known_vulnerabilities() {
use crate::osv::{ScanOutcome, VulnerabilityMap};
let parse_result = MockParseResult {
deps: vec![dep_at("clean-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert("clean-pkg".to_string(), ScanOutcome::Clean);
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("No known vulnerabilities"));
}
#[tokio::test]
async fn test_generate_hover_skipped_outcome_says_nothing_about_vulnerabilities() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = MockParseResult {
deps: vec![dep_at("path-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"path-pkg".to_string(),
ScanOutcome::Skipped(SkipReason::NonRegistrySource),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(!content.value.contains("Security advisories"));
assert!(!content.value.contains("No known vulnerabilities"));
}
#[tokio::test]
async fn test_generate_hover_vulnerable_outcome_shows_advisories_and_more_count() {
use crate::osv::{
Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
let parse_result = MockParseResult {
deps: vec![dep_at("bad-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"bad-pkg".to_string(),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::Critical)],
3,
),
fix_target_status: UpgradeStatus::NotChecked,
upgrade_status: UpgradeStatus::CandidateVulnerable {
version: "2.0.0".into(),
advisory_ids: Capped::new(vec!["RUSTSEC-2020-0071".to_string()], 1),
},
}),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(content.value.contains("Security advisories"));
assert!(content.value.contains("RUSTSEC-2020-0071"));
assert!(content.value.contains("Fixed in"));
assert!(
content.value.contains("1.5.0"),
"must show highest fixed version"
);
assert!(content.value.contains("+2 more advisories"));
assert!(content.value.contains("also affected"));
}
#[tokio::test]
async fn test_generate_hover_vulnerability_not_shared_across_duplicate_name_occurrences() {
use crate::osv::{
Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
let vulnerable_dep = MockDep {
name: "pkg".into(),
version_req: "=1.0.0".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
};
let patched_dep = MockDep {
name: "pkg".into(),
version_req: "=2.0.0".into(),
version_range: Range::new(Position::new(3, 10), Position::new(3, 20)),
name_range: Range::new(Position::new(3, 0), Position::new(3, 5)),
};
let parse_result = MockParseResult {
deps: vec![vulnerable_dep, patched_dep],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let keys = crate::osv::vulnerability_keys(
&parse_result,
&resolved_versions,
&MockFormatter,
crate::EcosystemId::Cargo,
);
let deps = parse_result.dependencies();
let vulnerable_key = keys.get(&deps[0].name_range()).unwrap().clone();
let patched_key = keys.get(&deps[1].name_range()).unwrap().clone();
assert_ne!(vulnerable_key, patched_key);
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
vulnerable_key,
ScanOutcome::Vulnerable(DependencyVulnerabilities {
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::Critical)],
1,
),
fix_target_status: UpgradeStatus::NotChecked,
upgrade_status: UpgradeStatus::NotChecked,
}),
);
vulns.insert(patched_key, ScanOutcome::Clean);
let versions = VersionData::new(&cached_versions, &resolved_versions)
.with_vulnerabilities(&vulns)
.with_ecosystem(crate::EcosystemId::Cargo);
let hover_on_patched = generate_hover(
&parse_result,
Position::new(3, 2),
versions,
&MockRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(patched_content) = hover_on_patched.contents else {
panic!("expected markup hover contents");
};
assert!(
!patched_content.value.contains("RUSTSEC-2020-0071"),
"the patched occurrence must not show the other occurrence's advisory: {}",
patched_content.value
);
assert!(patched_content.value.contains("No known vulnerabilities"));
let hover_on_vulnerable = generate_hover(
&parse_result,
Position::new(0, 2),
versions,
&MockRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(vulnerable_content) = hover_on_vulnerable.contents else {
panic!("expected markup hover contents");
};
assert!(vulnerable_content.value.contains("RUSTSEC-2020-0071"));
}
#[tokio::test]
async fn test_generate_hover_renders_basic_card_when_registry_reports_not_found() {
let parse_result = MockParseResult {
deps: vec![dep_at("com.example:..")],
uri: crate::test_util::test_uri("/test/pom.xml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&NotFoundRegistry,
&MockFormatter,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render the basic card on a registry error");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("**Latest**"),
"no version data is available on a registry error; got: {}",
content.value
);
assert!(
!content.value.contains("Recent versions"),
"must not render a broken version section from an empty list; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_footer_shown_for_resolvable_source_with_live_versions() {
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("Press `Cmd+.` to update version"),
"a resolvable source with live version data must show the update footer; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_footer_omitted_for_non_resolvable_source() {
use crate::parser::DependencySource;
let uri = crate::test_util::test_uri("/test/workflow.yml");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("local-action"),
DependencySource::Path {
path: "./local-action".into(),
},
),
uri,
};
let cached_versions = {
let mut m = HashMap::new();
m.insert("local-action".into(), PackageVersions::latest_only("9.9.9"));
m
};
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Press `Cmd+.` to update version"),
"a non-resolvable source offers no update code action, even with a cached \
latest value present; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_footer_omitted_when_offline_with_no_cached_data() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
"serde".to_string(),
ScanOutcome::Skipped(SkipReason::QueryFailed),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_offline(true)
.with_vulnerabilities(&vulns),
&ErrorRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Press `Cmd+.` to update version"),
"no code action can be produced while offline with nothing cached, so the \
footer must not render; got: {}",
content.value
);
assert!(
content
.value
.contains("📴 *Offline: version and vulnerability data not checked*"),
"the existing offline notice must still render; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_footer_shown_when_offline_with_warm_cache_versions() {
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = freshness_test_parse_result("serde");
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &HashMap::new()).with_offline(true),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("Press `Cmd+.` to update version"),
"a warm-cache live version list still offers a real REFACTOR action while \
offline, so the footer must render; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_offline_footer_shown_for_resolvable_source() {
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = freshness_test_parse_result("serde");
let cached = HashMap::new();
let resolved = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached, &resolved).with_offline(true),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("Offline: version and vulnerability data not checked"),
"a resolvable source must show the offline footer when versions.offline is set; \
got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_offline_footer_omitted_for_non_resolvable_source() {
use crate::parser::DependencySource;
let uri = crate::test_util::test_uri("/test/workflow.yml");
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("local-action"),
DependencySource::Path {
path: "./local-action".into(),
},
),
uri,
};
let cached_versions = {
let mut m = HashMap::new();
m.insert("local-action".into(), PackageVersions::latest_only("9.9.9"));
m
};
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&cached_versions, &resolved_versions).with_offline(true),
&MockRegistry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Offline:"),
"a non-resolvable source must not show the offline footer, even with \
versions.offline set; got: {}",
content.value
);
}
async fn deps_dev_mock_client() -> (mockito::ServerGuard, crate::DepsDevClient) {
let server = mockito::Server::new_async().await;
let client =
crate::DepsDevClient::for_test(Arc::new(crate::HttpCache::new()), server.url());
(server, client)
}
fn express_fixture() -> (
MockParseResult,
HashMap<crate::PackageName, ConcreteVersion>,
) {
let parse_result = freshness_test_parse_result("express");
let resolved_versions = HashMap::from([(
crate::PackageName::new("express"),
ConcreteVersion::new("4.19.2"),
)]);
(parse_result, resolved_versions)
}
#[tokio::test]
async fn test_generate_hover_trust_signal_renders_score_and_verified_provenance() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(200)
.with_body(
r#"{"slsaProvenances": [{"verified": true}], "attestations": [], "relatedProjects": [
{"projectKey": {"id": "github.com/expressjs/express"}, "relationType": "SOURCE_REPO", "relationProvenance": "SLSA_ATTESTATION"}
]}"#,
)
.create_async()
.await;
let _project = server
.mock("GET", "/v3/projects/github.com%2Fexpressjs%2Fexpress")
.with_status(200)
.with_body(r#"{"scorecard": {"overallScore": 8.5}}"#)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
let line = content
.value
.lines()
.find(|l| l.contains("Supply chain"))
.unwrap_or_else(|| panic!("expected a Supply chain line, got: {}", content.value));
insta::assert_snapshot!(line, @"🔐 **Supply chain**: OpenSSF Scorecard `8.5`/10 · Provenance: verified");
}
#[tokio::test]
async fn test_generate_hover_trust_signal_unverified_provenance() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(200)
.with_body(
r#"{"slsaProvenances": [{"verified": false}], "attestations": [], "relatedProjects": []}"#,
)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content
.value
.contains("Provenance: attested but unverified"),
"got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_trust_signal_none_provenance_and_self_reported_marker() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(200)
.with_body(
r#"{"slsaProvenances": [], "attestations": [], "relatedProjects": [
{"projectKey": {"id": "github.com/expressjs/express"}, "relationType": "SOURCE_REPO", "relationProvenance": "UNVERIFIED_METADATA"}
]}"#,
)
.create_async()
.await;
let _project = server
.mock("GET", "/v3/projects/github.com%2Fexpressjs%2Fexpress")
.with_status(200)
.with_body(r#"{"scorecard": {"overallScore": 7.2}}"#)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("Provenance: none found"),
"got: {}",
content.value
);
assert!(
content.value.contains("*(self-reported repo)*"),
"an UNVERIFIED_METADATA-only relation must be disclosed; got: {}",
content.value
);
}
#[tokio::test]
async fn test_generate_hover_trust_signal_skips_uncovered_ecosystems() {
for ecosystem in [
crate::EcosystemId::Composer,
crate::EcosystemId::Dart,
crate::EcosystemId::Swift,
] {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let never_called = server
.mock("GET", mockito::Matcher::Regex("^/v3/.*".into()))
.expect(0)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(ecosystem)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Supply chain"),
"{ecosystem:?}: got: {}",
content.value
);
never_called.assert_async().await;
}
}
#[tokio::test]
async fn test_generate_hover_trust_signal_skips_private_registry_source() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let never_called = server
.mock("GET", mockito::Matcher::Regex("^/v3/.*".into()))
.expect(0)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let parse_result = SingleDepParseResult {
dep: NonRegistryDep(
dep_at("internal-pkg"),
crate::parser::DependencySource::AlternateRegistry {
index: "https://index.mycorp.internal".to_string(),
mirrors_crates_io: false,
},
),
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let resolved_versions = HashMap::from([(
crate::PackageName::new("internal-pkg"),
ConcreteVersion::new("1.0.0"),
)]);
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Cargo)
.with_trust(&deps_dev),
®istry,
&MockWidenedResolveFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Supply chain"),
"a private registry's package name/version must never reach deps.dev; got: {}",
content.value
);
never_called.assert_async().await;
}
#[tokio::test]
async fn test_generate_hover_trust_signal_skips_when_offline() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let never_called = server
.mock("GET", mockito::Matcher::Regex("^/v3/.*".into()))
.expect(0)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev)
.with_offline(true),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
!content.value.contains("Supply chain"),
"got: {}",
content.value
);
never_called.assert_async().await;
}
#[tokio::test]
async fn test_generate_hover_trust_signal_failure_leaves_other_content_unchanged() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(500)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "4.19.2".into(),
yanked: false,
published_at: None,
}],
};
let with_failing_trust = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let without_trust = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(a) = with_failing_trust.contents else {
panic!("expected markup hover contents");
};
let HoverContents::Markup(b) = without_trust.contents else {
panic!("expected markup hover contents");
};
assert!(!a.value.contains("Supply chain"), "got: {}", a.value);
assert_eq!(a.value, b.value);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_generate_hover_trust_signal_over_real_wait_budget_omits_section() {
const CALL_DELAY: std::time::Duration = std::time::Duration::from_millis(375);
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(200)
.with_body_from_request(move |_req| {
std::thread::sleep(CALL_DELAY);
r#"{"slsaProvenances": [{"verified": true}], "attestations": [], "relatedProjects": [
{"projectKey": {"id": "github.com/expressjs/express"}, "relationType": "SOURCE_REPO", "relationProvenance": "SLSA_ATTESTATION"}
]}"#
.as_bytes()
.to_vec()
})
.create_async()
.await;
let _project = server
.mock("GET", "/v3/projects/github.com%2Fexpressjs%2Fexpress")
.with_status(200)
.with_body_from_request(move |_req| {
std::thread::sleep(CALL_DELAY);
r#"{"scorecard": {"overallScore": 8.5}}"#.as_bytes().to_vec()
})
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let first = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(first_content) = first.contents else {
panic!("expected markup hover contents");
};
assert!(
!first_content.value.contains("Supply chain"),
"the real ~750ms two-call sequence must exceed the 700ms wait budget on the \
first hover; got: {}",
first_content.value
);
}
#[tokio::test]
async fn test_generate_hover_renders_trust_signal_from_a_warm_memo() {
let (mut server, deps_dev) = deps_dev_mock_client().await;
let _version = server
.mock("GET", "/v3/systems/npm/packages/express/versions/4.19.2")
.with_status(200)
.with_body(
r#"{"slsaProvenances": [{"verified": true}], "attestations": [], "relatedProjects": [
{"projectKey": {"id": "github.com/expressjs/express"}, "relationType": "SOURCE_REPO", "relationProvenance": "SLSA_ATTESTATION"}
]}"#,
)
.create_async()
.await;
let _project = server
.mock("GET", "/v3/projects/github.com%2Fexpressjs%2Fexpress")
.with_status(200)
.with_body(r#"{"scorecard": {"overallScore": 8.5}}"#)
.create_async()
.await;
let warmed = deps_dev
.trust_signal("npm", "express", "4.19.2")
.await
.expect("the direct, un-delayed call must warm the memo deterministically");
assert!(warmed.scorecard.is_some(), "fixture includes a scorecard");
let deps_dev = Arc::new(deps_dev);
let (parse_result, resolved_versions) = express_fixture();
let registry = MockRegistryWithVersions { versions: vec![] };
let hover = generate_hover(
&parse_result,
Position::new(0, 2),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MockFormatter,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let HoverContents::Markup(content) = hover.contents else {
panic!("expected markup hover contents");
};
assert!(
content.value.contains("Supply chain"),
"a hover reading an already-warm memo must render the trust signal \
immediately; got: {}",
content.value
);
}
}