use std::sync::Arc;
use std::time::Duration;
use tower_lsp_server::ls_types::Position;
use crate::deps_dev::deps_dev_system;
use crate::edit::requirement_is_placeholder_for;
use crate::hover::Hover;
use crate::licenses::resolve_license_entries_for_display;
use crate::osv::ScanOutcome;
use crate::{
ConcreteVersion, Dependency, DependencySource, Deprecation, GossipLowUsage, LicenseSource,
ParseResult, ProvenanceStatus, PublishTime, Registry, SupplyChainTrustSignal, Version,
VersionReq,
};
use super::diagnostics::{MAX_DIAGNOSTIC_NAME_CHARS, MAX_DIAGNOSTIC_VALUE_CHARS};
#[cfg(test)]
use super::diagnostics::{MAX_DIAGNOSTIC_PROSE_CHARS, MAX_VERSION_DIAGNOSTIC_CHARS};
use super::hover_markdown::{FieldKind, HoverMarkdown};
use super::{
CooldownBlocker, CooldownPrecedence, EcosystemFormatter, HOVER_RECENT_VERSIONS, LatestVerdict,
VersionData, await_versions_fetch, cooldown_precedence, escape_markdown, in_use_version,
latest_verdict, markdown_code_span, position_in_range, resolve_in_use_version,
resolve_scan_outcome,
};
use crate::github::normalize_tag;
const DEPS_DEV_WAIT_BUDGET: Duration = Duration::from_millis(700);
const GOSSIP_WAIT_BUDGET: Duration = Duration::from_millis(700);
fn version_age_secs(version: &dyn Version, now: PublishTime) -> Option<u64> {
version
.published_at()
.map(|published| published.age_secs_from(now))
}
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 =
!d.name_range_is_synthetic() && position_in_range(position.into(), d.name_range());
let on_version = !super::version_range_is_synthetic_empty(*d)
&& d.version_range()
.is_some_and(|r| position_in_range(position.into(), 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 gossip_handle = spawn_gossip_low_usage_fetch(
dep,
&dep_source,
&versions,
formatter,
normalized_name.as_str(),
);
let available_versions = if resolvable {
await_versions_fetch(
registry.get_versions_from(dep.name(), &dep_source, freshness),
dep.name(),
"hover",
)
.await
.0
} else {
None
};
let url = (!formatter.suppress_package_url(&dep_source))
.then(|| formatter.package_url(dep.name()))
.filter(|u| !u.is_empty());
let mut markdown = HoverMarkdown::new();
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 {
in_use_version::resolve_occurrence_version(
dep,
normalized_name.as_str(),
versions.resolved,
versions.resolved_version_candidates,
formatter,
)
.map(ConcreteVersion::as_str)
};
push_current_or_requirement_hover_section(&mut markdown, dep, resolved);
push_markers_hover_section(&mut markdown, dep);
let wildcard_req = crate::existence_wildcard_req();
let selection_context = parse_result.selection_context();
let live_latest_idx = available_versions
.as_ref()
.and_then(|v| registry.select_latest_matching(v, &wildcard_req, &selection_context));
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,
&selection_context,
),
)
.await
{
Ok(Ok(found)) => {
tracing::debug!(package = %dep.name().for_tracing(), found = found.is_some(), "hover latest fallback (get_latest_matching) resolved");
found
}
Ok(Err(error)) => {
tracing::warn!(package = %dep.name().for_tracing(), %error, "hover latest fallback (get_latest_matching) failed");
None
}
Err(_) => {
tracing::warn!(
package = %dep.name().for_tracing(),
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
.and_then(|idx| v.get(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)),
};
let precedence = match (freshness, latest_line) {
(
crate::freshness::FreshnessSettings::Enabled { cooldown },
Some((latest_ver, published_at)),
) => cooldown_precedence(
versions.gossip_prefetch,
dep.name(),
latest_ver,
published_at,
cooldown,
now,
),
_ => CooldownPrecedence::Cleared,
};
let vuln_keys = versions.ecosystem.map(|ecosystem| {
crate::osv::vulnerability_keys(
parse_result,
versions.resolved,
versions.resolved_version_candidates,
formatter,
ecosystem,
)
});
let latest_verdict_result = match latest_line {
Some((latest_ver, _)) => latest_verdict(
versions.latest_status,
dep,
vuln_keys.as_ref(),
&normalized_name,
latest_ver,
formatter,
),
None => LatestVerdict::NotApplicable,
};
push_latest_hover_section(
&mut markdown,
latest_line,
freshness,
now,
precedence,
&latest_verdict_result,
);
let vuln_outcome = versions
.vulnerabilities
.and_then(|m| resolve_scan_outcome(m, dep, vuln_keys.as_ref(), &normalized_name));
let deprecation = versions
.outcomes
.and_then(|o| o.deprecation(&normalized_name));
push_deprecation_hover_section(&mut markdown, formatter, deprecation);
let latest_status_for_dep = versions
.latest_status
.and_then(|m| super::resolve_latest_status(m, dep, vuln_keys.as_ref(), &normalized_name));
push_vulnerability_hover_section(
&mut markdown,
formatter,
vuln_outcome,
latest_status_for_dep,
);
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());
let gossip_low_usage = match gossip_handle {
Some(handle) => match tokio::time::timeout(GOSSIP_WAIT_BUDGET, handle).await {
Ok(Ok(findings)) => findings.and_then(|f| f.low_usage),
Ok(Err(_)) | Err(_) => None,
},
None => None,
};
push_gossip_low_usage_hover_section(&mut markdown, gossip_low_usage.as_ref());
let in_use_version: Option<ConcreteVersion> = versions.ecosystem.and_then(|ecosystem| {
resolve_in_use_version(
dep,
normalized_name.as_str(),
versions.resolved,
versions.resolved_version_candidates,
formatter,
ecosystem,
)
});
let resolved_key: Option<&str> = in_use_version
.as_ref()
.map(ConcreteVersion::as_str)
.or(resolved);
let license_source = versions.license_source.unwrap_or_default();
let resolved_license: Vec<String> = resolved_key
.and_then(|r| {
available_versions.as_ref().and_then(|versions| {
versions
.iter()
.find(|v| normalize_tag(v.version_string().as_str()) == normalize_tag(r))
})
})
.map(|v| v.license().to_vec())
.filter(|l| !l.is_empty())
.or_else(|| {
trust_signal
.as_ref()
.map(|s| s.licenses.clone())
.filter(|l| !l.is_empty())
})
.or_else(|| {
versions
.license_prefetch
.and_then(|m| m.get(dep.name()))
.filter(|raw| !raw.is_empty())
.map(|raw| resolve_license_entries_for_display(license_source, raw))
})
.unwrap_or_default();
let latest_license: Option<Vec<String>> = (!resolved_license.is_empty())
.then(|| {
latest_line.map(|(latest_ver, _)| {
if resolved_key == Some(latest_ver) {
resolved_license.clone()
} else {
live_latest_idx
.and_then(|idx| available_versions.as_ref().and_then(|v| v.get(idx)))
.map(|v| v.license().to_vec())
.filter(|l| !l.is_empty())
.unwrap_or_default()
}
})
})
.flatten();
let license_is_detected = license_source == LicenseSource::DetectedSpdx;
push_license_hover_section(
&mut markdown,
&resolved_license,
latest_license.as_deref(),
license_is_detected,
);
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,
);
}
let requirement_is_placeholder = dep
.version_requirement()
.is_some_and(|req| requirement_is_placeholder_for(formatter, dep, req.as_str()));
push_cmd_dot_footer_hover_section(
&mut markdown,
CmdDotFooterState {
resolvable,
available_versions: available_versions.as_deref(),
cached_latest,
vuln_outcome,
deprecation,
offline: versions.network.is_offline(),
requirement_is_placeholder,
latest_verdict: latest_verdict_result.clone(),
},
);
let offline = versions.network.is_offline();
push_offline_footer_hover_section(&mut markdown, resolvable, offline);
push_skip_reason_footer_hover_section(&mut markdown, resolvable, offline, vuln_outcome);
Some(markdown.finish(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.network.is_offline() || !formatter.source_is_public_registry_content(dep_source)
{
return None;
}
let version = resolve_in_use_version(
dep,
normalized_name,
versions.resolved,
versions.resolved_version_candidates,
formatter,
ecosystem,
)?;
let client = Arc::clone(client);
let name = dep.name().as_str().to_string();
Some(tokio::spawn(async move {
client.trust_signal(system, &name, version.as_str()).await
}))
})
}
fn spawn_gossip_low_usage_fetch(
dep: &dyn Dependency,
dep_source: &DependencySource,
versions: &VersionData<'_>,
formatter: &dyn EcosystemFormatter,
normalized_name: &str,
) -> Option<tokio::task::JoinHandle<Option<crate::GossipFindings>>> {
versions.gossip_client.and_then(|client| {
let ecosystem = versions.ecosystem?;
let system = deps_dev_system(ecosystem)?;
if versions.network.is_offline() || !formatter.source_is_public_registry_content(dep_source)
{
return None;
}
let version = resolve_in_use_version(
dep,
normalized_name,
versions.resolved,
versions.resolved_version_candidates,
formatter,
ecosystem,
)?;
let client = Arc::clone(client);
let name = dep.name().as_str().to_string();
Some(tokio::spawn(async move {
client
.gossip_findings_for_version(system, &name, version.as_str())
.await
}))
})
}
fn push_header_hover_section(
markdown: &mut HoverMarkdown,
dep: &dyn Dependency,
url: Option<&str>,
) {
markdown.push_static("# ");
match url {
Some(url) => {
markdown.push_link(dep.name().as_str(), FieldKind::Name, url);
}
None => {
markdown.push_label(dep.name().as_str(), FieldKind::Name);
}
}
markdown.push_static("\n\n");
}
fn push_current_or_requirement_hover_section(
markdown: &mut HoverMarkdown,
dep: &dyn Dependency,
resolved: Option<&str>,
) {
if let Some(resolved_ver) = resolved {
markdown.push_static("**Current**: ");
markdown.push_code(resolved_ver, FieldKind::Version);
markdown.push_static("\n\n");
} else if let Some(version_req) = dep.version_requirement() {
markdown.push_static("**Requirement**: ");
markdown.push_code(version_req.as_str(), FieldKind::Version);
markdown.push_static("\n\n");
}
}
fn push_markers_hover_section(markdown: &mut HoverMarkdown, dep: &dyn Dependency) {
if let Some(marker_expr) = dep.markers() {
markdown.push_static("**Active when**: ");
markdown.push_code(marker_expr, FieldKind::Name);
markdown.push_static("\n\n");
}
}
fn push_latest_hover_section(
markdown: &mut HoverMarkdown,
latest_line: Option<(&str, Option<PublishTime>)>,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
precedence: CooldownPrecedence,
latest_verdict: &LatestVerdict,
) {
let Some((latest_ver, raw_published_at)) = latest_line else {
return;
};
let published_at = freshness.is_enabled().then_some(raw_published_at).flatten();
let age_secs = published_at.map(|p| p.age_secs_from(now));
markdown.push_static("**Latest**: ");
markdown.push_code(latest_ver, FieldKind::Version);
if let Some(age_secs) = age_secs {
markdown.push_static(" *(published ");
markdown.push_relative_age(age_secs);
markdown.push_static(")*");
}
markdown.push_static("\n\n");
if let LatestVerdict::Flagged {
advisory_ids,
malicious,
} = latest_verdict
{
if *malicious {
markdown.push_static(
"> 🚫 **Latest version is confirmed malicious by OSV.dev** — do not upgrade to \
this version.\n",
);
} else {
markdown.push_static(
"> ⚠️ **Latest version is flagged by OSV.dev** — do not upgrade to this \
version.\n",
);
}
if !advisory_ids.is_empty() {
markdown.push_static("> ");
markdown.push_text(&advisory_ids.join(", "), FieldKind::Prose);
markdown.push_static("\n");
}
markdown.push_static("\n");
return;
}
if !freshness.is_enabled() {
return;
}
match precedence {
CooldownPrecedence::Blocked(CooldownBlocker::Gossip) => {
markdown.push_static(
"> ⏳ **Recently published** — deps.dev/GOSSIP reports this release is still \
within its cooldown window.\n\
> It may still be yanked or superseded; consider verifying before upgrading.\n\n",
);
}
CooldownPrecedence::Blocked(CooldownBlocker::Local { .. }) => {
markdown.push_static(
"> ⏳ **Recently published** — this release is still within the cooldown \
window.\n\
> It may still be yanked or superseded; consider verifying before \
upgrading.\n\n",
);
}
CooldownPrecedence::Cleared => {}
}
}
fn push_gossip_low_usage_hover_section(
markdown: &mut HoverMarkdown,
low_usage: Option<&GossipLowUsage>,
) {
let Some(low_usage) = low_usage else {
return;
};
markdown.push_static(
"> 🔍 **Low usage** — deps.dev/GOSSIP flags this package version as having \
suspiciously low real-world usage. Double-check this is the package you intended \
to install, not a similarly-named or LLM-hallucinated name.",
);
if let Some(alternative) = low_usage.alternative_packages.first() {
markdown.push_static(" A more widely-used alternative: ");
markdown.push_code(alternative, FieldKind::Name);
markdown.push_static(".");
}
markdown.push_static("\n\n");
}
fn push_recent_versions_hover_section(
markdown: &mut HoverMarkdown,
available_versions: &[Box<dyn Version>],
live_latest_idx: Option<usize>,
freshness: crate::freshness::FreshnessSettings,
now: PublishTime,
formatter: &dyn EcosystemFormatter,
) {
if available_versions.is_empty() {
return;
}
let mut entries: Vec<(usize, &Box<dyn Version>)> = available_versions
.iter()
.enumerate()
.take(HOVER_RECENT_VERSIONS)
.collect();
if let Some(idx) = live_latest_idx
&& idx >= HOVER_RECENT_VERSIONS
&& let Some(pick) = available_versions.get(idx)
{
entries.truncate(HOVER_RECENT_VERSIONS - 1);
entries.push((idx, pick));
}
markdown.push_static("**Recent versions**:\n");
for (i, version) in entries {
let age_secs = freshness
.is_enabled()
.then(|| version_age_secs(version.as_ref(), now))
.flatten();
markdown.push_static("- ");
markdown.push_code(version.version_string().as_str(), FieldKind::Version);
let is_latest = Some(i) == live_latest_idx;
let flagged = version.removal_status().is_flagged();
match (is_latest, flagged) {
(true, true) => {
markdown.push_static(" *(latest)* ");
markdown.push_static(formatter.yanked_label());
}
(true, false) => {
markdown.push_static(" *(latest)*");
}
(false, true) => {
markdown.push_static(" ");
markdown.push_static(formatter.yanked_label());
}
(false, false) => {}
}
if let Some(age_secs) = age_secs {
markdown.push_static(" — ");
markdown.push_relative_age(age_secs);
}
markdown.push_static("\n");
}
}
fn push_cmd_dot_footer_hover_section(markdown: &mut HoverMarkdown, state: CmdDotFooterState<'_>) {
let latest_is_safe = matches!(
state.latest_verdict,
LatestVerdict::Verified | LatestVerdict::NotApplicable
);
let has_offline_actionable_data = (state.cached_latest.is_some()
|| state.available_versions.is_some_and(|v| !v.is_empty()))
&& latest_is_safe
|| matches!(state.vuln_outcome, Some(ScanOutcome::Vulnerable(_)))
|| state.deprecation.is_some();
let live_fetch_definitively_empty = state.available_versions.is_some_and(<[_]>::is_empty);
let footer_actionable = has_offline_actionable_data
|| (!live_fetch_definitively_empty && !state.offline && latest_is_safe);
if state.resolvable && footer_actionable && !state.requirement_is_placeholder {
markdown.push_static(CMD_DOT_FOOTER);
}
}
struct CmdDotFooterState<'a> {
resolvable: bool,
available_versions: Option<&'a [Box<dyn Version>]>,
cached_latest: Option<&'a super::PackageVersions>,
vuln_outcome: Option<&'a ScanOutcome>,
deprecation: Option<&'a Deprecation>,
offline: bool,
requirement_is_placeholder: bool,
latest_verdict: LatestVerdict,
}
fn push_offline_footer_hover_section(
markdown: &mut HoverMarkdown,
resolvable: bool,
offline: bool,
) {
if offline && resolvable {
markdown.push_static("\n---\n📴 *Offline: version and vulnerability data not checked*");
}
}
fn push_skip_reason_footer_hover_section(
markdown: &mut HoverMarkdown,
resolvable: bool,
offline: bool,
vuln_outcome: Option<&ScanOutcome>,
) {
if offline || !resolvable {
return;
}
if let Some(ScanOutcome::Skipped(reason)) = vuln_outcome
&& let Some(text) = reason.unchecked_reason()
{
markdown.push_static("\n---\n🔍 *Vulnerability data not checked: ");
markdown.push_static(text);
markdown.push_static("*");
}
}
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",
crate::osv::VulnSeverity::Malicious => "confirmed malicious package",
crate::osv::VulnSeverity::Informational => "maintenance-status notice, not a vulnerability",
}
}
fn push_deprecation_hover_section(
markdown: &mut HoverMarkdown,
formatter: &dyn EcosystemFormatter,
deprecation: Option<&Deprecation>,
) {
let Some(deprecation) = deprecation else {
return;
};
markdown.push_static("### Deprecated\n\n");
markdown.push_static(formatter.deprecated_message());
markdown.push_static("\n\n");
if let Some(reason) = deprecation.reason.as_deref().filter(|r| !r.is_empty()) {
markdown.push_text(reason, FieldKind::Prose);
markdown.push_static("\n\n");
}
if let Some(replacement) = deprecation.replacement.as_deref().filter(|r| !r.is_empty()) {
markdown.push_static("Suggested replacement: ");
markdown.push_code(replacement, FieldKind::Name);
markdown.push_static("\n\n");
}
}
fn candidate_vulnerable_line_should_render(
candidate_ids: &crate::osv::Capped<String>,
known_advisories: &[Arc<crate::osv::Advisory>],
) -> bool {
if !candidate_ids.is_complete() || candidate_ids.items().is_empty() {
return true;
}
!candidate_ids.items().iter().all(|id| {
known_advisories
.iter()
.find(|advisory| &advisory.id == id)
.is_some_and(|advisory| advisory.severity == crate::osv::VulnSeverity::Informational)
})
}
fn push_vulnerability_hover_section(
markdown: &mut HoverMarkdown,
formatter: &dyn EcosystemFormatter,
outcome: Option<&ScanOutcome>,
latest_status: Option<&crate::osv::UpgradeStatus>,
) {
match outcome {
Some(ScanOutcome::Vulnerable(dv)) => {
markdown.push_static("### Security advisories\n\n");
let display_advisories = dv.advisories_for_display();
for advisory in display_advisories.items() {
markdown.push_static("- **");
markdown.push_link(&advisory.id, FieldKind::Prose, advisory.url());
markdown.push_static("** — ");
markdown.push_static(severity_label(advisory.severity));
markdown.push_static("\n ");
markdown.push_text(
advisory
.summary
.as_deref()
.unwrap_or("(no summary provided)"),
FieldKind::Prose,
);
markdown.push_static("\n");
if let Some(tags) = dv.sibling_match(&advisory.id) {
markdown.push_static(" *(");
markdown.push_static(formatter.sibling_match_label());
markdown.push_static(" ");
markdown.push_trusted(escape_markdown(
&super::diagnostics::format_matched_tags(tags),
));
markdown.push_static(")*\n");
}
let has_fixed =
advisory.fixed_versions.last().is_some() && dv.fix_is_upgrade(advisory);
let has_aliases = !advisory.aliases.is_empty();
if has_fixed || has_aliases {
markdown.push_static(" ");
if let Some(fixed) = advisory.fixed_versions.last().filter(|_| has_fixed) {
let native = formatter.osv_version_to_native(fixed);
markdown.push_static("Fixed in: ");
markdown.push_code(native.as_str(), FieldKind::Version);
}
if has_aliases {
if has_fixed {
markdown.push_static(" \u{b7} ");
}
markdown.push_static("Aliases: ");
markdown.push_trusted(format_advisory_aliases(&advisory.aliases));
}
markdown.push_static("\n");
}
}
let remaining = display_advisories.remaining();
if remaining > 0 {
markdown.push_static("- *(+");
markdown.push_number(remaining);
markdown.push_static(" more advisories)*\n");
}
if let Some(crate::osv::UpgradeStatus::CandidateVulnerable {
version,
advisory_ids,
..
}) = latest_status
&& candidate_vulnerable_line_should_render(advisory_ids, dv.advisories.items())
{
markdown.push_static("\n\u{26a0}\u{fe0f} Latest version ");
markdown.push_code(version.as_str(), FieldKind::Version);
markdown.push_static(" is also affected.\n");
}
markdown.push_static("\n");
}
Some(ScanOutcome::Clean) => {
markdown.push_static("**No known vulnerabilities** (OSV.dev)\n\n");
}
Some(ScanOutcome::Skipped(_)) | None => {}
}
}
const MAX_ADVISORY_ALIASES_RENDERED: usize = 8;
fn format_advisory_aliases(aliases: &[String]) -> String {
let truncated: Vec<String> = aliases
.iter()
.map(|a| super::truncate_for_diagnostic(a, MAX_DIAGNOSTIC_NAME_CHARS).into_owned())
.collect();
escape_markdown(&crate::licenses::join_capped(
&truncated,
MAX_ADVISORY_ALIASES_RENDERED,
))
}
fn push_trust_signal_hover_section(
markdown: &mut HoverMarkdown,
signal: Option<&SupplyChainTrustSignal>,
) {
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}"));
}
markdown.push_static("\u{1f510} **Supply chain**: ");
markdown.push_trusted(parts.join(" \u{b7} "));
markdown.push_static("\n\n");
}
const MAX_LICENSE_ENTRIES_RENDERED: usize = 8;
fn format_license_list(licenses: &[String]) -> String {
let shown = licenses.len().min(MAX_LICENSE_ENTRIES_RENDERED);
#[expect(
clippy::indexing_slicing,
reason = "shown is min(licenses.len(), MAX_LICENSE_ENTRIES_RENDERED), always <= \
licenses.len()"
)]
let mut rendered: Vec<String> = licenses[..shown]
.iter()
.map(|l| {
markdown_code_span(&super::truncate_for_diagnostic(
l,
MAX_DIAGNOSTIC_VALUE_CHARS,
))
})
.collect();
let remaining = licenses.len() - shown;
if remaining > 0 {
rendered.push(format!("(+{remaining} more)"));
}
rendered.join(", ")
}
fn license_sets_differ(a: &[String], b: &[String]) -> bool {
let a_set: std::collections::BTreeSet<String> = a.iter().map(|l| l.to_lowercase()).collect();
let b_set: std::collections::BTreeSet<String> = b.iter().map(|l| l.to_lowercase()).collect();
a_set != b_set
}
fn push_license_hover_section(
markdown: &mut HoverMarkdown,
resolved_license: &[String],
latest_license: Option<&[String]>,
detected: bool,
) {
if resolved_license.is_empty() {
return;
}
markdown.push_static("**License");
if detected {
markdown.push_static(" (detected)");
}
markdown.push_static("**: ");
markdown.push_trusted(format_license_list(resolved_license));
match latest_license {
None => {}
Some([]) => {
markdown.push_static(" *(latest version license unavailable)*");
}
Some(latest) if license_sets_differ(resolved_license, latest) => {
markdown.push_static("\n\n\u{26a0}\u{fe0f} **License changed**: ");
markdown.push_trusted(format_license_list(resolved_license));
markdown.push_static(" \u{2192} ");
markdown.push_trusted(format_license_list(latest));
}
Some(_) => {}
}
markdown.push_static("\n\n");
}
#[cfg(test)]
#[expect(
clippy::cast_possible_truncation,
reason = "#673: fixed test-fixture lengths cast to u32 for Position fixtures never approach \
truncation range"
)]
mod tests {
use super::*;
use crate::RemovalStatus;
use crate::deps_dev::DepsDevSystem;
use crate::lsp_helpers::test_support::*;
use crate::lsp_helpers::test_support_lsp::*;
use crate::lsp_helpers::*;
use crate::position::{Position, Range};
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn license_sets_differ_is_order_insensitive() {
let a = vec!["MIT".to_string(), "Apache-2.0".to_string()];
let b = vec!["Apache-2.0".to_string(), "MIT".to_string()];
assert!(!license_sets_differ(&a, &b));
}
#[test]
fn license_sets_differ_detects_real_change() {
let a = vec!["MIT".to_string()];
let b = vec!["GPL-3.0".to_string()];
assert!(license_sets_differ(&a, &b));
}
#[test]
fn license_sets_differ_is_case_insensitive() {
let a = vec!["MIT".to_string()];
let b = vec!["mit".to_string()];
assert!(!license_sets_differ(&a, &b));
}
#[test]
fn format_license_list_caps_entries_and_labels_the_remainder() {
let licenses: Vec<String> = (0..12).map(|i| format!("LICENSE-{i}")).collect();
let rendered = format_license_list(&licenses);
assert!(rendered.contains("(+4 more)"), "got: {rendered}");
assert!(rendered.contains("`LICENSE-0`"));
assert!(
!rendered.contains("LICENSE-11"),
"the 12th entry must not render; got: {rendered}"
);
}
#[test]
fn format_license_list_truncates_an_overlong_identifier() {
let long_id = "A".repeat(500);
let rendered = format_license_list(std::slice::from_ref(&long_id));
assert!(
rendered.len() < long_id.len(),
"an untrusted, excessively long license id must be truncated; got len {}",
rendered.len()
);
}
#[test]
fn format_advisory_aliases_caps_entries_and_labels_the_remainder() {
let aliases: Vec<String> = (0..12).map(|i| format!("CVE-2020-{i:04}")).collect();
let rendered = format_advisory_aliases(&aliases);
assert!(rendered.contains("+4 more"), "got: {rendered}");
assert!(rendered.contains("0000"));
assert!(
!rendered.contains("0011"),
"the 12th entry must not render; got: {rendered}"
);
}
#[test]
fn format_advisory_aliases_truncates_an_overlong_alias() {
let long_alias = "A".repeat(500);
let rendered = format_advisory_aliases(std::slice::from_ref(&long_alias));
assert_eq!(
rendered,
format!("{}…", "A".repeat(MAX_DIAGNOSTIC_NAME_CHARS))
);
}
#[test]
fn format_advisory_aliases_truncates_a_multi_byte_alias_on_a_char_boundary() {
let long_alias = "é".repeat(500);
let rendered = format_advisory_aliases(std::slice::from_ref(&long_alias));
assert_eq!(
rendered,
format!("{}…", "é".repeat(MAX_DIAGNOSTIC_NAME_CHARS))
);
}
#[test]
fn push_current_or_requirement_hover_section_truncates_overlong_current() {
let dep = MockDep {
name: "pkg".into(),
version_req: "1.0".into(),
version_range: Range::default(),
name_range: Range::default(),
};
let long = "9".repeat(5000);
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, Some(&long));
assert!(markdown.as_str().len() < long.len(), "got: {markdown}");
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_current_or_requirement_hover_section_truncates_overlong_requirement() {
let long = "9".repeat(5000);
let dep = MockDep {
name: "pkg".into(),
version_req: long.as_str().into(),
version_range: Range::default(),
name_range: Range::default(),
};
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, None);
assert!(markdown.as_str().len() < long.len(), "got: {markdown}");
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_current_or_requirement_hover_section_current_boundary_at_and_over_cap() {
let dep = MockDep {
name: "pkg".into(),
version_req: "1.0".into(),
version_range: Range::default(),
name_range: Range::default(),
};
let cap = MAX_VERSION_DIAGNOSTIC_CHARS;
let at_cap = "9".repeat(cap);
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, Some(&at_cap));
assert_eq!(markdown.as_str(), format!("**Current**: `{at_cap}`\n\n"));
let over_cap = "9".repeat(cap + 1);
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, Some(&over_cap));
assert_eq!(
markdown.as_str(),
format!("**Current**: `{}…`\n\n", "9".repeat(cap))
);
}
#[test]
fn push_current_or_requirement_hover_section_requirement_boundary_at_and_over_cap() {
let cap = MAX_VERSION_DIAGNOSTIC_CHARS;
let at_cap = "9".repeat(cap);
let dep = MockDep {
name: "pkg".into(),
version_req: at_cap.as_str().into(),
version_range: Range::default(),
name_range: Range::default(),
};
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, None);
assert_eq!(
markdown.as_str(),
format!("**Requirement**: `{at_cap}`\n\n")
);
let over_cap = "9".repeat(cap + 1);
let dep = MockDep {
name: "pkg".into(),
version_req: over_cap.as_str().into(),
version_range: Range::default(),
name_range: Range::default(),
};
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, None);
assert_eq!(
markdown.as_str(),
format!("**Requirement**: `{}…`\n\n", "9".repeat(cap))
);
}
#[test]
fn push_current_or_requirement_hover_section_strips_u0600() {
let value = format!("1.0{}0", '\u{0600}');
let dep = MockDep {
name: "pkg".into(),
version_req: value.as_str().into(),
version_range: Range::default(),
name_range: Range::default(),
};
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, Some(&value));
assert!(!markdown.as_str().contains('\u{0600}'), "got: {markdown}");
let mut markdown = HoverMarkdown::new();
push_current_or_requirement_hover_section(&mut markdown, &dep, None);
assert!(!markdown.as_str().contains('\u{0600}'), "got: {markdown}");
}
#[test]
fn push_markers_hover_section_truncates_overlong_marker_expr() {
let long = "x".repeat(5000);
let dep = MockMarkedDep {
name: "pkg".into(),
name_range: Range::default(),
markers: Some(long.clone()),
};
let mut markdown = HoverMarkdown::new();
push_markers_hover_section(&mut markdown, &dep);
assert!(markdown.as_str().len() < long.len(), "got: {markdown}");
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_markers_hover_section_boundary_at_and_over_cap() {
let cap = MAX_DIAGNOSTIC_NAME_CHARS;
let at_cap = "x".repeat(cap);
let dep = MockMarkedDep {
name: "pkg".into(),
name_range: Range::default(),
markers: Some(at_cap.clone()),
};
let mut markdown = HoverMarkdown::new();
push_markers_hover_section(&mut markdown, &dep);
assert_eq!(
markdown.as_str(),
format!("**Active when**: `{at_cap}`\n\n")
);
let over_cap = "x".repeat(cap + 1);
let dep = MockMarkedDep {
name: "pkg".into(),
name_range: Range::default(),
markers: Some(over_cap),
};
let mut markdown = HoverMarkdown::new();
push_markers_hover_section(&mut markdown, &dep);
assert_eq!(
markdown.as_str(),
format!("**Active when**: `{}…`\n\n", "x".repeat(cap))
);
}
#[test]
fn push_latest_hover_section_truncates_overlong_latest_version() {
let long = "9".repeat(5000);
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some((long.as_str(), None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
CooldownPrecedence::Cleared,
&LatestVerdict::NotApplicable,
);
assert!(markdown.as_str().len() < long.len(), "got: {markdown}");
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_latest_hover_section_boundary_at_and_over_cap() {
let cap = MAX_VERSION_DIAGNOSTIC_CHARS;
let at_cap = "9".repeat(cap);
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some((at_cap.as_str(), None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
CooldownPrecedence::Cleared,
&LatestVerdict::NotApplicable,
);
assert_eq!(markdown.as_str(), format!("**Latest**: `{at_cap}`\n\n"));
let over_cap = "9".repeat(cap + 1);
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some((over_cap.as_str(), None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
CooldownPrecedence::Cleared,
&LatestVerdict::NotApplicable,
);
assert_eq!(
markdown.as_str(),
format!("**Latest**: `{}…`\n\n", "9".repeat(cap))
);
}
#[test]
fn push_latest_hover_section_strips_u0600() {
let value = format!("1.0{}0", '\u{0600}');
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some((value.as_str(), None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
CooldownPrecedence::Cleared,
&LatestVerdict::NotApplicable,
);
assert!(!markdown.as_str().contains('\u{0600}'), "got: {markdown}");
}
#[test]
fn push_latest_hover_section_gossip_cooldown_renders_attributed_callout() {
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some(("8.3.1", None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::from_unix_secs(1_000),
CooldownPrecedence::Blocked(CooldownBlocker::Gossip),
&LatestVerdict::NotApplicable,
);
assert!(
markdown.as_str().contains("deps.dev/GOSSIP"),
"got: {markdown}"
);
}
#[test]
fn push_latest_hover_section_no_gossip_falls_back_to_local_heuristic() {
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some(("8.3.1", Some(PublishTime::from_unix_secs(999_999)))),
crate::freshness::FreshnessSettings::default(),
PublishTime::from_unix_secs(1_000_000),
CooldownPrecedence::Blocked(CooldownBlocker::Local {
published_at: PublishTime::from_unix_secs(999_999),
}),
&LatestVerdict::NotApplicable,
);
assert!(
!markdown.as_str().contains("deps.dev/GOSSIP"),
"got: {markdown}"
);
assert!(
markdown.as_str().contains("Recently published"),
"got: {markdown}"
);
}
#[test]
fn push_latest_hover_section_gossip_not_active_suppresses_local_fallback() {
let mut markdown = HoverMarkdown::new();
push_latest_hover_section(
&mut markdown,
Some(("8.3.1", Some(PublishTime::from_unix_secs(999_999)))),
crate::freshness::FreshnessSettings::default(),
PublishTime::from_unix_secs(1_000_000),
CooldownPrecedence::Cleared,
&LatestVerdict::NotApplicable,
);
assert!(
!markdown.as_str().contains("Recently published"),
"GOSSIP's authoritative negative must suppress the local heuristic too: {markdown}"
);
}
#[test]
fn push_latest_hover_section_flagged_suppresses_cooldown_callout() {
let mut markdown = HoverMarkdown::new();
let verdict = LatestVerdict::Flagged {
advisory_ids: vec!["MAL-2026-16332".to_string()],
malicious: true,
};
push_latest_hover_section(
&mut markdown,
Some(("1.0.8", Some(PublishTime::from_unix_secs(999_999)))),
crate::freshness::FreshnessSettings::default(),
PublishTime::from_unix_secs(1_000_000),
CooldownPrecedence::Blocked(CooldownBlocker::Gossip),
&verdict,
);
assert!(
!markdown.as_str().contains("Recently published"),
"a flagged/malicious latest must never render as a benign cooldown notice: {markdown}"
);
assert!(
markdown.as_str().contains("confirmed malicious"),
"got: {markdown}"
);
assert!(
markdown.as_str().contains("MAL") && markdown.as_str().contains("16332"),
"got: {markdown}"
);
}
#[test]
fn push_latest_hover_section_flagged_non_malicious_uses_flagged_wording() {
let mut markdown = HoverMarkdown::new();
let verdict = LatestVerdict::Flagged {
advisory_ids: vec!["GHSA-xxxx".to_string()],
malicious: false,
};
push_latest_hover_section(
&mut markdown,
Some(("2.0.0", None)),
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
CooldownPrecedence::Cleared,
&verdict,
);
assert!(
markdown.as_str().contains("flagged by OSV.dev"),
"got: {markdown}"
);
assert!(
!markdown.as_str().contains("confirmed malicious"),
"got: {markdown}"
);
}
#[test]
fn push_gossip_low_usage_hover_section_none_renders_nothing() {
let mut markdown = HoverMarkdown::new();
push_gossip_low_usage_hover_section(&mut markdown, None);
assert_eq!(markdown.as_str(), "");
}
#[test]
fn push_gossip_low_usage_hover_section_renders_alternative_package() {
let low_usage = crate::GossipLowUsage {
risk: crate::GossipRiskLevel::Medium,
alternative_packages: vec!["popular-pkg".to_string()],
};
let mut markdown = HoverMarkdown::new();
push_gossip_low_usage_hover_section(&mut markdown, Some(&low_usage));
assert!(markdown.as_str().contains("Low usage"), "got: {markdown}");
assert!(markdown.as_str().contains("popular-pkg"), "got: {markdown}");
}
#[test]
fn push_recent_versions_hover_section_truncates_overlong_version_string() {
let long = "9".repeat(5000);
let versions: Vec<Box<dyn crate::Version>> = vec![Box::new(TestVersion {
version: long.as_str().into(),
yanked: false,
})];
let mut markdown = HoverMarkdown::new();
push_recent_versions_hover_section(
&mut markdown,
&versions,
None,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
&MOCK_FORMATTER,
);
assert!(markdown.as_str().len() < long.len(), "got: {markdown}");
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_recent_versions_hover_section_boundary_at_and_over_cap() {
let cap = MAX_VERSION_DIAGNOSTIC_CHARS;
let at_cap = "9".repeat(cap);
let versions: Vec<Box<dyn crate::Version>> = vec![Box::new(TestVersion {
version: at_cap.as_str().into(),
yanked: false,
})];
let mut markdown = HoverMarkdown::new();
push_recent_versions_hover_section(
&mut markdown,
&versions,
None,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
&MOCK_FORMATTER,
);
assert_eq!(
markdown.as_str(),
format!("**Recent versions**:\n- `{at_cap}`\n")
);
let over_cap = "9".repeat(cap + 1);
let versions: Vec<Box<dyn crate::Version>> = vec![Box::new(TestVersion {
version: over_cap.as_str().into(),
yanked: false,
})];
let mut markdown = HoverMarkdown::new();
push_recent_versions_hover_section(
&mut markdown,
&versions,
None,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
&MOCK_FORMATTER,
);
assert_eq!(
markdown.as_str(),
format!("**Recent versions**:\n- `{}…`\n", "9".repeat(cap))
);
}
#[test]
fn push_recent_versions_hover_section_strips_u0600() {
let value = format!("1.0{}0", '\u{0600}');
let versions: Vec<Box<dyn crate::Version>> = vec![Box::new(TestVersion {
version: value.as_str().into(),
yanked: false,
})];
let mut markdown = HoverMarkdown::new();
push_recent_versions_hover_section(
&mut markdown,
&versions,
None,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
&MOCK_FORMATTER,
);
assert!(!markdown.as_str().contains('\u{0600}'), "got: {markdown}");
}
#[test]
fn push_deprecation_hover_section_truncates_overlong_reason_and_replacement() {
let long_reason = "r".repeat(5000);
let long_replacement = "p".repeat(5000);
let deprecation = Deprecation {
reason: Some(long_reason.clone()),
replacement: Some(long_replacement.clone()),
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown.as_str().len() < long_reason.len() + long_replacement.len(),
"got: {markdown}"
);
assert!(markdown.as_str().contains('…'));
}
#[test]
fn push_deprecation_hover_section_reason_boundary_at_and_over_cap() {
let cap = MAX_DIAGNOSTIC_PROSE_CHARS;
let at_cap = "r".repeat(cap);
let deprecation = Deprecation {
reason: Some(at_cap.clone()),
replacement: None,
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown.as_str().contains(&format!("{at_cap}\n\n")),
"got: {markdown}"
);
assert!(!markdown.as_str().contains('…'));
let over_cap = "r".repeat(cap + 1);
let deprecation = Deprecation {
reason: Some(over_cap),
replacement: None,
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown
.as_str()
.contains(&format!("{}…\n\n", "r".repeat(cap))),
"got: {markdown}"
);
}
#[test]
fn push_deprecation_hover_section_replacement_boundary_at_and_over_cap() {
let cap = MAX_DIAGNOSTIC_NAME_CHARS;
let at_cap = "p".repeat(cap);
let deprecation = Deprecation {
reason: None,
replacement: Some(at_cap.clone()),
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown
.as_str()
.contains(&format!("Suggested replacement: `{at_cap}`\n\n")),
"got: {markdown}"
);
let over_cap = "p".repeat(cap + 1);
let deprecation = Deprecation {
reason: None,
replacement: Some(over_cap),
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown.as_str().contains(&format!(
"Suggested replacement: `{}…`\n\n",
"p".repeat(cap)
)),
"got: {markdown}"
);
}
#[test]
fn push_deprecation_hover_section_still_blocks_rlo_override_after_truncation() {
let reason = format!("safe {}text", '\u{202e}');
let deprecation = Deprecation {
reason: Some(reason),
replacement: None,
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
!markdown.as_str().contains('\u{202e}'),
"RLO override must still be blocked; got: {markdown:?}"
);
}
#[test]
fn push_deprecation_hover_section_replacement_strips_u0600() {
let replacement = format!("left{}pad", '\u{0600}');
let deprecation = Deprecation {
reason: None,
replacement: Some(replacement),
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
!markdown.as_str().contains('\u{0600}'),
"U+0600 must be stripped from the name-shaped replacement field; got: {markdown:?}"
);
}
#[test]
fn push_deprecation_hover_section_reason_does_not_strip_u0600_or_rtl_marks() {
let reason = format!("note{} with RTL{}mark", '\u{0600}', '\u{200f}');
let deprecation = Deprecation {
reason: Some(reason),
replacement: None,
};
let mut markdown = HoverMarkdown::new();
push_deprecation_hover_section(&mut markdown, &MOCK_FORMATTER, Some(&deprecation));
assert!(
markdown.as_str().contains('\u{0600}') && markdown.as_str().contains('\u{200f}'),
"reason (Prose) must preserve U+0600 and RTL marks unchanged; got: {markdown:?}"
);
}
#[test]
fn push_markers_hover_section_strips_u0600() {
let dep = MockMarkedDep {
name: "pkg".into(),
name_range: Range::default(),
markers: Some(format!("python_version{}>= '3.8'", '\u{0600}')),
};
let mut markdown = HoverMarkdown::new();
push_markers_hover_section(&mut markdown, &dep);
assert!(
!markdown.as_str().contains('\u{0600}'),
"U+0600 must be stripped from the name-shaped marker expression; got: {markdown:?}"
);
}
#[test]
fn push_license_hover_section_renders_nothing_when_resolved_unknown() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(&mut markdown, &[], Some(&["MIT".to_string()]), false);
assert!(markdown.as_str().is_empty());
}
#[test]
fn push_license_hover_section_renders_nothing_extra_when_no_latest_version_exists() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(&mut markdown, &["MIT".to_string()], None, false);
assert!(markdown.as_str().contains("**License**: `MIT`"));
assert!(!markdown.as_str().contains("unavailable"));
assert!(!markdown.as_str().contains("License changed"));
}
#[test]
fn push_license_hover_section_renders_resolved_only_when_latest_unavailable() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(&mut markdown, &["MIT".to_string()], Some(&[]), false);
assert!(markdown.as_str().contains("**License**: `MIT`"));
assert!(
markdown
.as_str()
.contains("latest version license unavailable")
);
assert!(!markdown.as_str().contains("License changed"));
}
#[test]
fn push_license_hover_section_flags_change_when_licenses_differ() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(
&mut markdown,
&["MIT".to_string()],
Some(&["Apache-2.0".to_string()]),
false,
);
assert!(markdown.as_str().contains("**License**: `MIT`"));
assert!(markdown.as_str().contains("License changed"));
assert!(markdown.as_str().contains("`MIT` \u{2192} `Apache-2.0`"));
}
#[test]
fn push_license_hover_section_detected_flag_adds_qualifier() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(&mut markdown, &["MIT".to_string()], None, true);
assert!(markdown.as_str().contains("**License (detected)**: `MIT`"));
}
#[test]
fn push_license_hover_section_change_line_is_a_separate_paragraph() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(
&mut markdown,
&["MIT".to_string()],
Some(&["Apache-2.0".to_string()]),
false,
);
assert!(
markdown
.as_str()
.contains("`MIT`\n\n\u{26a0}\u{fe0f} **License changed**"),
"expected a blank line (paragraph break) before the License changed line; got: {markdown:?}"
);
}
#[test]
fn push_license_hover_section_no_flag_when_licenses_equal() {
let mut markdown = HoverMarkdown::new();
push_license_hover_section(
&mut markdown,
&["MIT".to_string()],
Some(&["MIT".to_string()]),
false,
);
assert!(markdown.as_str().contains("**License**: `MIT`"));
assert!(!markdown.as_str().contains("License changed"));
assert!(!markdown.as_str().contains("unavailable"));
}
#[tokio::test]
async fn test_generate_hover_renders_license_changed_from_native_version_list() {
use std::collections::HashMap;
let registry = MockRegistryWithLicensedVersions {
versions: vec![
MockVersionWithLicense {
version: "2.0.0".into(),
license: vec!["Apache-2.0".to_string()],
},
MockVersionWithLicense {
version: "1.0.0".into(),
license: vec!["MIT".to_string()],
},
],
};
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**License**: `MIT`"), "got: {}", content);
assert!(
content.contains("License changed"),
"resolved (1.0.0/MIT) and latest (2.0.0/Apache-2.0) licenses differ; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_does_not_match_a_synthetic_range_dependency_at_position_zero() {
use std::collections::HashMap;
let parse_result = MockMixedParseResult {
deps: vec![
Box::new(MockSyntheticRangeDep {
name: "synthetic-pkg".into(),
}),
Box::new(MockDep {
name: "real-pkg".into(),
version_req: "1.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, 8)),
}),
],
uri: crate::test_util::test_uri("/test/pubspec.yaml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 0).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await;
assert!(
hover.is_none(),
"position (0,0) must not match the synthetic-range dependency's \
Range::default() sentinel"
);
}
#[tokio::test]
async fn test_generate_hover_does_not_match_version_range_with_no_requirement() {
use std::collections::HashMap;
let version_range = Range::new(Position::new(5, 15), Position::new(5, 15));
let parse_result = MockMixedParseResult {
deps: vec![Box::new(MockNoRequirementDep {
name: "com.example:foo".into(),
name_range: Range::new(Position::new(4, 18), Position::new(4, 21)),
version_range,
})],
uri: crate::test_util::test_uri("/test/pom.xml"),
};
let hover = generate_hover(
&parse_result,
version_range.start.into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await;
assert!(
hover.is_none(),
"must not match a version_range with no version_requirement behind it"
);
}
#[tokio::test]
async fn test_generate_hover_matches_non_empty_version_range_with_no_requirement() {
use std::collections::HashMap;
let version_range = Range::new(Position::new(4, 40), Position::new(4, 45));
let parse_result = MockMixedParseResult {
deps: vec![Box::new(MockNoRequirementDep {
name: "com.example:guava".into(),
name_range: Range::new(Position::new(4, 18), Position::new(4, 21)),
version_range,
})],
uri: crate::test_util::test_uri("/test/libs.versions.toml"),
};
let hover = generate_hover(
&parse_result,
version_range.start.into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await;
assert!(
hover.is_some(),
"must match a real, non-empty version_range even with no version_requirement"
);
}
#[tokio::test]
async fn test_generate_hover_up_to_date_dependency_shows_no_spurious_unavailable_note() {
use std::collections::HashMap;
let registry = MockRegistryWithLicensedVersions {
versions: vec![MockVersionWithLicense {
version: "1.0.0".into(),
license: vec!["MIT".to_string()],
}],
};
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**License**: `MIT`"));
assert!(
!content.contains("unavailable"),
"the resolved version is the latest version, so its already-known license \
must be reused rather than reported unavailable; got: {}",
content
);
assert!(
!content.contains("License changed"),
"resolved and latest are the same version, so no change flag should fire; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_native_list_license_found_via_concrete_pin_fallback() {
use std::collections::HashMap;
let registry = MockRegistryWithLicensedVersions {
versions: vec![MockVersionWithLicense {
version: "1.0.0".into(),
license: vec!["MIT".to_string()],
}],
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "example".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, 7)),
}],
uri: crate::test_util::test_uri("/test/composer.json"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_ecosystem(crate::EcosystemId::Composer),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.contains("**Current**"),
"no lock-file resolution exists, so no Current line should render; got: {}",
content
);
assert!(
content.contains("**License**: `MIT`"),
"the concrete-pin fallback (=1.0.0) should still resolve a license even \
without a lock-file match; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_dart_license_from_prefetch_is_labeled_detected() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["BSD-3-Clause".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Dart)
.with_license_source(crate::LicenseSource::DetectedSpdx)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License (detected)**: `BSD-3-Clause`"),
"got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_swift_license_from_prefetch_is_labeled_detected() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["Apache-2.0".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Swift)
.with_license_source(crate::LicenseSource::DetectedSpdx)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License (detected)**: `Apache-2.0`"),
"got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_gradle_license_from_prefetch_is_not_labeled_detected() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["Apache-2.0".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Gradle)
.with_license_source(crate::LicenseSource::PomFreeText)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License**: `Apache-2.0`"),
"got: {}",
content
);
assert!(!content.contains("(detected)"));
}
#[tokio::test]
async fn test_generate_hover_gradle_pom_free_text_license_is_normalized() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["The Apache Software License, Version 2.0".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Gradle)
.with_license_source(crate::LicenseSource::PomFreeText)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License**: `Apache-2.0`"),
"expected the normalized SPDX id, not raw POM text, got: {}",
content
);
assert!(!content.contains("The Apache Software License"));
}
#[tokio::test]
async fn test_generate_hover_gradle_unrecognized_pom_license_falls_back_to_raw_text() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["Some Bespoke Corporate License".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Gradle)
.with_license_source(crate::LicenseSource::PomFreeText)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License**: `Some Bespoke Corporate License`"),
"expected the raw POM text as a fallback, got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_gradle_gpl_family_renders_single_canonical_id() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["GNU General Public License v3".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Gradle)
.with_license_source(crate::LicenseSource::PomFreeText)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**License**: `GPL-3.0`"),
"expected exactly one canonical id, got: {}",
content
);
assert!(
!content.contains("GPL-3.0-only") && !content.contains("GPL-3.0-or-later"),
"must not render the full policy-matching synonym slice in hover, got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_gradle_disjunctive_license_renders_both_ids() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("example");
let mut resolved_versions = HashMap::new();
resolved_versions.insert("example".into(), "1.0.0".into());
let mut licenses = HashMap::new();
licenses.insert(
crate::PackageName::new("example"),
vec!["CDDL + GPLv2 with classpath exception".to_string()],
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Gradle)
.with_license_source(crate::LicenseSource::PomFreeText)
.with_license_prefetch(&licenses),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("CDDL-1.1") && content.contains("GPL-2.0-with-classpath-exception"),
"expected both disjunctive ids, got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_deps_dev_no_lockfile_exact_pin_matching_latest_reuses_license() {
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": [], "licenses": ["MIT"]}"#,
)
.create_async()
.await;
let deps_dev = Arc::new(deps_dev);
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "express".into(),
version_req: "=4.19.2".into(),
version_range: Range::new(Position::new(0, 10), Position::new(0, 20)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
}],
uri: crate::test_util::test_uri("/test/package.json"),
};
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "4.19.2".into(),
yanked: false,
published_at: None,
}],
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
!content.contains("**Current**"),
"no lock-file resolution exists, so no Current line should render; got: {}",
content
);
assert!(content.contains("**License**: `MIT`"), "got: {}", content);
assert!(
!content.contains("unavailable"),
"the exact pin (4.19.2) equals the only/live-latest version, so the \
already-known license must be reused, not reported unavailable; got: {}",
content
);
assert!(!content.contains("License changed"));
}
#[test]
fn severity_label_for_malicious_is_distinct_from_unknown_and_every_graded_label() {
let malicious = severity_label(crate::osv::VulnSeverity::Malicious);
assert_ne!(malicious, "unknown severity");
for graded in [
crate::osv::VulnSeverity::Critical,
crate::osv::VulnSeverity::High,
crate::osv::VulnSeverity::Medium,
crate::osv::VulnSeverity::Low,
] {
assert_ne!(malicious, severity_label(graded));
}
}
#[test]
fn severity_label_for_informational_is_distinct_from_unknown_and_every_graded_label() {
let informational = severity_label(crate::osv::VulnSeverity::Informational);
assert_ne!(informational, "unknown severity");
assert_ne!(
informational,
severity_label(crate::osv::VulnSeverity::Malicious)
);
for graded in [
crate::osv::VulnSeverity::Critical,
crate::osv::VulnSeverity::High,
crate::osv::VulnSeverity::Medium,
crate::osv::VulnSeverity::Low,
] {
assert_ne!(informational, severity_label(graded));
}
}
#[test]
fn candidate_vulnerable_line_suppressed_only_when_every_id_is_known_informational() {
use crate::osv::{Capped, VulnSeverity};
let informational = sample_advisory("RUSTSEC-2024-0320", VulnSeverity::Informational);
let graded = sample_advisory("RUSTSEC-2020-0071", VulnSeverity::High);
assert!(
!candidate_vulnerable_line_should_render(
&Capped::new(vec!["RUSTSEC-2024-0320".to_string()], 1),
std::slice::from_ref(&informational)
),
"all-known-Informational, complete set must suppress the line"
);
assert!(
candidate_vulnerable_line_should_render(
&Capped::new(
vec![
"RUSTSEC-2024-0320".to_string(),
"RUSTSEC-2020-0071".to_string()
],
2
),
&[informational.clone(), graded]
),
"mixed set must still render the line"
);
assert!(
candidate_vulnerable_line_should_render(
&Capped::new(vec!["RUSTSEC-UNKNOWN-ID".to_string()], 1),
std::slice::from_ref(&informational)
),
"an id with no known severity must default to rendering the line, not suppressing it"
);
assert!(
candidate_vulnerable_line_should_render(
&Capped::new(vec![], 0),
std::slice::from_ref(&informational)
),
"N1: an empty (and complete) candidate_ids set is reachable (a real \
advisory whose detail fetch/into_advisory validation failed) and \
must fail open — rendering the line, not silently suppressing a \
real signal"
);
assert!(
candidate_vulnerable_line_should_render(
&Capped::new(vec!["RUSTSEC-2024-0320".to_string()], 2),
std::slice::from_ref(&informational)
),
"L2/FR-010: an all-known-Informational but INCOMPLETE (truncated) set \
must still fail open — an undisplayed advisory beyond the cap could \
be a real, non-Informational finding"
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("- `1.2.3` *(latest)* — 2 days ago"),
"got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("- `1.2.3` *(latest)*\n"));
assert!(!content.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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**Latest**: `13.0.4`"), "got: {}", content);
assert!(
content.contains("- `13.0.4` *(latest)*"),
"the stable version, not the raw-top pre-release, should carry the marker; got: {}",
content
);
assert!(
!content.contains("13.0.5-beta1` *(latest)*"),
"the pre-release must not be tagged latest; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_bumped_in_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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**Latest**: `1.9.0`"), "got: {}", content);
assert!(
content.contains("- `1.9.0` *(latest)*"),
"the stable pick must be bumped into the capped list instead of omitted; got: {}",
content
);
assert_eq!(
content.matches("*(latest)*").count(),
1,
"exactly one entry should carry the marker; got: {}",
content
);
assert_eq!(
content.lines().filter(|l| l.starts_with("- `")).count(),
HOVER_RECENT_VERSIONS,
"the list must stay capped at HOVER_RECENT_VERSIONS entries even after the bump-in; got: {}",
content
);
assert!(
!content.contains("2.0.0-alpha7"),
"the displaced 8th natural entry must not remain in the capped list; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_at_last_window_slot_not_bumped() {
use std::collections::HashMap;
let mut versions: Vec<MockVersionWithAge> = (0..HOVER_RECENT_VERSIONS - 1)
.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,
});
assert_eq!(versions.len(), HOVER_RECENT_VERSIONS);
let registry = MockRegistryWithVersions { versions };
let parse_result = freshness_test_parse_result("example");
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert_eq!(
content.matches("- `1.9.0` *(latest)*").count(),
1,
"the in-window pick must render exactly once, not duplicated by a bump-in; got: {}",
content
);
assert_eq!(
content.lines().filter(|l| l.starts_with("- `")).count(),
HOVER_RECENT_VERSIONS,
"got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_one_past_window_triggers_bump() {
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,
});
assert_eq!(versions.len(), HOVER_RECENT_VERSIONS + 1);
let registry = MockRegistryWithVersions { versions };
let parse_result = freshness_test_parse_result("example");
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("- `1.9.0` *(latest)*"),
"the pick at index HOVER_RECENT_VERSIONS must be bumped in; got: {}",
content
);
assert!(
!content.contains(&format!("2.0.0-alpha{}", HOVER_RECENT_VERSIONS - 1)),
"the displaced last natural entry must not remain in the capped list; got: {}",
content
);
assert_eq!(
content.lines().filter(|l| l.starts_with("- `")).count(),
HOVER_RECENT_VERSIONS,
"got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_latest_marker_flagged_pick_bumped_in_keeps_yanked_label() {
use crate::RemovalStatus;
use std::collections::HashMap;
let mut versions: Vec<MockVersionWithStatus> = (0..HOVER_RECENT_VERSIONS)
.map(|i| MockVersionWithStatus {
version: format!("5.0.{i}").into(),
status: RemovalStatus::Yanked,
})
.collect();
versions.push(MockVersionWithStatus {
version: "5.0.99".into(),
status: RemovalStatus::AdvisoryDeprecated,
});
let registry = MockRegistryPreferringUnflagged { versions };
let parse_result = freshness_test_parse_result("example");
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("- `5.0.99` *(latest)* *(yanked)*"),
"the bumped-in flagged pick must carry both the latest marker and its flag label; got: {}",
content
);
assert!(
!content.contains("5.0.7`"),
"the displaced last natural entry must not remain in the capped list; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor, not panic");
let content = hover.markdown();
assert!(
!content.contains("**Latest**:"),
"no stable version exists, so the header should be omitted rather than picking a pre-release; got: {}",
content
);
assert!(
!content.contains("*(latest)*"),
"no stable version exists, so no entry in the list should be marked latest; got: {}",
content
);
assert!(
content.contains("2.0.0-beta2"),
"the raw version list should still render even without a latest marker; got: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor, not panic");
let content = hover.markdown();
assert!(
!content.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
);
assert!(
!content.contains("1.5.0"),
"the stale cached version must not leak into the response at all; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.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
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Latest**: `v1.2.3`"),
"expected the list-based pick's own version, not the fallback's; got: {}",
content
);
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&ErrorRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render on a fetch failure, not disappear entirely");
let content = hover.markdown();
assert!(
content.contains("serde"),
"basic card must still render the package name; got: {}",
content
);
assert!(
!content.contains("**Latest**"),
"no version data is available on a fetch failure; got: {}",
content
);
}
#[tokio::test(start_paused = true)]
async fn test_generate_hover_registry_fetch_timeout_degrades_to_basic_card() {
use std::collections::HashMap;
let parse_result = freshness_test_parse_result("serde");
let registry = SlowRegistry {
delay: REGISTRY_FETCH_BUDGET + Duration::from_secs(1),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render when the primary fetch times out, not hang or panic");
let content = hover.markdown();
assert!(
content.contains("serde"),
"basic card must still render the package name; got: {}",
content
);
assert!(
!content.contains("**Latest**"),
"no version data is available once the fetch times out; got: {}",
content
);
}
#[tokio::test(start_paused = true)]
async fn test_generate_hover_registry_fetch_timeout_log_redacts_credential_shaped_package_name()
{
use std::collections::HashMap;
let sentinel_name = "com.example:deploy:AUDITSENTINEL0000@git.internal.corp";
let parse_result = freshness_test_parse_result(sentinel_name);
let registry = SlowRegistry {
delay: REGISTRY_FETCH_BUDGET + Duration::from_secs(1),
};
let log = crate::test_util::capture_tracing_output_async_at(tracing::Level::WARN, async {
generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render when the primary fetch times out");
})
.await;
assert!(
log.contains("primary registry version fetch timed out"),
"expected the primary-fetch-timeout WARN to fire: {log:?}"
);
assert!(
!log.contains("AUDITSENTINEL0000"),
"tracing output leaked a credential-shaped package name: {log:?}"
);
assert!(
log.contains("git.internal.corp"),
"host should survive redaction: {log:?}"
);
}
#[tokio::test]
async fn test_generate_hover_fallback_failure_log_redacts_credential_shaped_package_name() {
use std::any::Any;
use std::collections::HashMap;
struct FallbackFailsRegistry;
impl crate::Registry for FallbackFailsRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Version>>>>
{
Box::pin(async move {
Ok(vec![Box::new(TestVersion {
version: ConcreteVersion::new("1.2.3"),
yanked: false,
}) as Box<dyn crate::Version>])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<
'a,
crate::error::Result<Option<Box<dyn crate::Version>>>,
> {
Box::pin(async move {
Err(crate::error::DepsError::CacheError(
"transient backend failure".to_string(),
))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Vec<Box<dyn crate::Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let sentinel_name = "com.example:deploy:AUDITSENTINEL0000@git.internal.corp";
let parse_result = freshness_test_parse_result(sentinel_name);
let registry = FallbackFailsRegistry;
let log = crate::test_util::capture_tracing_output_async_at(tracing::Level::WARN, async {
generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render when the fallback fetch fails");
})
.await;
assert!(
log.contains("hover latest fallback (get_latest_matching) failed"),
"expected the fallback-failure WARN to fire: {log:?}"
);
assert!(
!log.contains("AUDITSENTINEL0000"),
"tracing output leaked a credential-shaped package name: {log:?}"
);
assert!(
log.contains("git.internal.corp"),
"host should survive redaction: {log:?}"
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_GO_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Current**: `v0.8.1`"),
"expected hover to show go.mod's pinned version, got: {}",
content
);
assert!(
!content.contains("v0.9.1"),
"hover must not surface the stale go.sum version: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Current**: `1.2.0`"),
"expected hover to show the resolved lockfile version, got: {}",
content
);
assert!(!content.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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("- `1.2.1` *(yanked)* — 5 months ago"),
"got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::Disabled,
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("- `1.2.3` *(latest)*\n"));
assert!(!content.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,
)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Latest**: `2.0.0` *(published 1 hour ago)*"),
"got: {}",
content
);
assert!(
content.contains(
"> ⏳ **Recently published** — this release is still within the cooldown window."
),
"got: {}",
content
);
}
#[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,
)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Latest**: `2.0.0` *(published 1 week ago)*"),
"got: {}",
content
);
assert!(!content.contains("Recently published"));
}
#[tokio::test]
async fn test_generate_hover_gossip_not_active_suppresses_local_callout() {
use std::collections::HashMap;
let now = PublishTime::from_unix_secs(1_000_000);
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(now.as_unix_secs() - 60 * 60)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let mut gossip_prefetch = HashMap::new();
gossip_prefetch.insert(
crate::PackageName::new("serde"),
crate::GossipFindings {
version: "2.0.0".to_string(),
cooldown: Some(crate::GossipCooldown::new(
PublishTime::from_unix_secs(now.as_unix_secs() - 1),
crate::GossipRiskLevel::High,
)),
low_usage: None,
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_gossip_prefetch(&gossip_prefetch),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.contains("Recently published"),
"GOSSIP's authoritative NotActive answer must suppress the local heuristic: {content}"
);
}
#[tokio::test]
async fn test_generate_hover_no_gossip_prefetch_renders_unattributed_local_callout() {
use std::collections::HashMap;
let now = PublishTime::from_unix_secs(1_000_000);
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(now.as_unix_secs() - 60 * 60)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("Recently published"), "got: {content}");
assert!(
!content.contains("deps.dev/GOSSIP"),
"no prefetch data means the local heuristic renders unattributed: {content}"
);
}
#[tokio::test]
async fn test_generate_hover_freshness_disabled_suppresses_callout_despite_active_gossip() {
use std::collections::HashMap;
let now = PublishTime::from_unix_secs(1_000_000);
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(now.as_unix_secs() - 60 * 60)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let mut gossip_prefetch = HashMap::new();
gossip_prefetch.insert(
crate::PackageName::new("serde"),
crate::GossipFindings {
version: "2.0.0".to_string(),
cooldown: Some(crate::GossipCooldown::new(
PublishTime::from_unix_secs(now.as_unix_secs() + 1_000),
crate::GossipRiskLevel::High,
)),
low_usage: None,
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_gossip_prefetch(&gossip_prefetch),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::Disabled,
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.contains("Recently published"),
"FreshnessSettings::Disabled must suppress the callout regardless of GOSSIP: {content}"
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**Latest**: `2.0.0`\n\n"));
assert!(!content.contains("published"));
assert!(!content.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,
)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::Disabled,
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**Latest**: `2.0.0`\n\n"));
assert!(!content.contains("published"));
assert!(!content.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),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::Enabled {
cooldown: crate::CooldownWindow::from_secs(COOLDOWN_SECS),
},
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.contains("Recently published"),
"age exactly equal to the cooldown window must not be within cooldown, got: {}",
content
);
}
#[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),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::Enabled {
cooldown: crate::CooldownWindow::from_secs(COOLDOWN_SECS),
},
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("Recently published"),
"age == cooldown window - 1 must be within cooldown, got: {}",
content
);
}
#[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,
)),
gossip_excluded_version: None,
cooldown_fallback: None,
},
);
let resolved_versions = HashMap::new();
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.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
);
assert!(
!content.contains("**Latest**: `1.0.213`"),
"must not render the stale Ch1 version, got: {}",
content
);
assert!(
content.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
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.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
);
assert!(
content.contains("- `1.9.0` *(latest)*"),
"the Recent versions list's own *(latest)* marker must agree with the Latest \
line above it, got: {}",
content
);
assert!(
content.contains("- `2.0.0` *(yanked)*"),
"2.0.0 must keep its flagged label even though it isn't the resolved latest, \
got: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("**Latest**: `2.0.0`"),
"the only version resolves as latest even though it's flagged, got: {}",
content
);
assert!(
content.contains("- `2.0.0` *(latest)* *(yanked)*"),
"the resolved latest must keep its flagged label instead of the warning \
silently vanishing behind *(latest)*, got: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
now,
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.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
);
assert!(
content.contains("- `2.0.0` *(latest)* *(yanked)*"),
"the resolved latest must keep its yanked label, got: {}",
content
);
}
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.as_str())
}
}
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).into(),
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 content = hover.markdown();
assert!(
content.contains("### Deprecated"),
"expected the package-level Deprecated section, got: {}",
content
);
assert!(
content.contains("no longer maintained"),
"expected the deprecation reason, got: {}",
content
);
assert!(
content.contains("This package is deprecated\n\nno longer maintained"),
"expected the message and reason on separate paragraphs, got: {}",
content
);
assert!(
content.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
);
}
#[tokio::test]
async fn test_generate_hover_surfaces_markers() {
use crate::position::{Position, Range};
use std::collections::HashMap;
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.contains("**Active when**: `python_full_version >= '3.9'`"));
}
#[tokio::test]
async fn test_generate_hover_omits_active_when_without_markers() {
use crate::position::{Position, Range};
use std::collections::HashMap;
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(!content.contains("Active when"));
}
#[tokio::test]
async fn test_generate_hover_escapes_malicious_dependency_name() {
use crate::position::{Position, Range};
use std::collections::HashMap;
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.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 crate::position::{Position, Range};
use std::collections::HashMap;
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.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_bidi_override_in_name_cannot_spoof_rendered_label() {
use crate::position::{Position, Range};
use std::collections::HashMap;
let malicious_name = "real\u{202E}gnp.sj";
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.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!(!label.contains('\u{202E}'));
assert!(!header_line.contains('\u{202E}'));
}
#[tokio::test]
async fn test_generate_hover_bidi_override_in_name_cannot_spoof_link_destination() {
use crate::position::{Position, Range};
use std::collections::HashMap;
let malicious_name = "real\u{202E}gnp.sj";
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.lines()
.next()
.expect("hover markdown has a header line");
let destination = header_line
.split("](")
.nth(1)
.expect("header contains label/url separator")
.strip_suffix(')')
.expect("destination ends with a closing paren");
assert!(
!destination.contains('\u{202E}'),
"link destination must not carry the bidi override; got: {destination}"
);
assert!(
url::Url::parse(destination).is_ok(),
"sanitized destination must still be a valid, parseable URL — a space \
substitution (rather than removal) would break the link; got: {destination:?}"
);
assert_eq!(
destination, "https://example.com/realgnp.sj",
"removing the bidi char must not introduce a stray space or other artifact"
);
}
#[tokio::test]
async fn test_generate_hover_header_caps_rendered_label_length_after_escaping() {
use crate::position::{Position, Range};
use std::collections::HashMap;
let punctuation_heavy_name = "-".repeat(300);
let parse_result = MockParseResult {
deps: vec![MockDep {
name: punctuation_heavy_name.clone().into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 310), Position::new(0, 320)),
name_range: Range::new(
Position::new(0, 0),
Position::new(0, punctuation_heavy_name.len() as u32),
),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.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.chars().count(),
MAX_DIAGNOSTIC_NAME_CHARS + 1,
"rendered (post-escape) label must be capped to MAX_DIAGNOSTIC_NAME_CHARS \
chars plus the ellipsis marker, proving truncation runs after escaping, \
not before; got: {label}"
);
assert!(
label.ends_with('…'),
"expected truncation marker; got: {label}"
);
}
#[tokio::test]
async fn test_generate_hover_header_does_not_truncate_long_legitimate_destination() {
use crate::position::{Position, Range};
use std::collections::HashMap;
let long_legitimate_name = "x".repeat(200);
let parse_result = MockParseResult {
deps: vec![MockDep {
name: long_legitimate_name.clone().into(),
version_req: "1.0.0".into(),
version_range: Range::new(Position::new(0, 210), Position::new(0, 220)),
name_range: Range::new(
Position::new(0, 0),
Position::new(0, long_legitimate_name.len() as u32),
),
}],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
let header_line = content
.lines()
.next()
.expect("hover markdown has a header line");
let destination = header_line
.split("](")
.nth(1)
.expect("header contains label/url separator")
.strip_suffix(')')
.expect("destination ends with a closing paren");
let expected = format!("https://example.com/{long_legitimate_name}");
assert_eq!(
destination, expected,
"a long but legitimate destination must render in full, untruncated"
);
assert!(
url::Url::parse(destination).is_ok(),
"destination must still be a valid, parseable URL; got: {destination:?}"
);
}
#[tokio::test]
async fn test_generate_hover_marker_with_parens_renders_unescaped() {
use crate::position::{Position, Range};
use std::collections::HashMap;
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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(content.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).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let content = hover.markdown();
assert!(!content.contains("**Latest**"));
assert!(!content.contains("**Recent versions**"));
assert!(content.contains("**Requirement**"));
let registry_parse_result = SingleDepParseResult {
dep: dep_at("dep"),
uri,
};
let hover = generate_hover(
®istry_parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(content.contains("**Latest**"));
assert!(content.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(crate::test_util::vuln_key("clean-pkg"), ScanOutcome::Clean);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(content.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(
crate::test_util::vuln_key("path-pkg"),
ScanOutcome::Skipped(SkipReason::NonRegistrySource),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(!content.contains("Security advisories"));
assert!(!content.contains("No known vulnerabilities"));
}
#[tokio::test]
async fn test_generate_hover_vulnerable_outcome_shows_advisories_and_more_count() {
use crate::osv::{
Capped, DependencyVulnerabilities, LatestStatusMap, 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(
crate::test_util::vuln_key("bad-pkg"),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::Critical)],
3,
),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
let mut latest_status = LatestStatusMap::new();
latest_status.insert(
crate::test_util::vuln_key("bad-pkg"),
UpgradeStatus::CandidateVulnerable {
version: "2.0.0".into(),
advisory_ids: Capped::new(vec!["RUSTSEC-2020-0071".to_string()], 1),
worst_severity: Some(VulnSeverity::Critical),
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_vulnerabilities(&vulns)
.with_latest_status(&latest_status),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(content.contains("Security advisories"));
assert!(content.contains("RUSTSEC-2020-0071"));
assert!(content.contains("Fixed in"));
assert!(content.contains("1.5.0"), "must show highest fixed version");
assert!(content.contains("+2 more advisories"));
assert!(content.contains("also affected"));
}
#[test]
fn push_vulnerability_hover_section_caps_fixed_version_summary_and_aliases() {
use crate::osv::{
Advisory, Capped, DependencyVulnerabilities, OsvVersion, ScanOutcome, UpgradeStatus,
VulnSeverity,
};
let mut advisory = Advisory::new(
"RUSTSEC-2020-0071".to_string(),
"2023-01-01T00:00:00Z".to_string(),
VulnSeverity::High,
)
.expect("valid osv id");
advisory.summary = Some("S".repeat(500));
advisory.fixed_versions = vec![OsvVersion::new("F".repeat(500))];
let mut aliases: Vec<String> = (0..7).map(|i| format!("CVE-2020-{i:04}")).collect();
aliases.push("é".repeat(500));
aliases.extend((7..11).map(|i| format!("CVE-2020-{i:04}")));
advisory.aliases = aliases;
let dv = DependencyVulnerabilities::new(Capped::new(vec![Arc::new(advisory)], 1));
let latest = UpgradeStatus::CandidateVulnerable {
version: ConcreteVersion::new("V".repeat(500)),
advisory_ids: Capped::new(vec!["RUSTSEC-2020-0071".to_string()], 1),
worst_severity: Some(VulnSeverity::High),
};
let outcome = ScanOutcome::Vulnerable(dv);
let mut markdown = HoverMarkdown::new();
push_vulnerability_hover_section(
&mut markdown,
&MOCK_FORMATTER,
Some(&outcome),
Some(&latest),
);
assert!(
markdown
.as_str()
.contains(&format!("{}…", "S".repeat(MAX_DIAGNOSTIC_PROSE_CHARS))),
"summary must be truncated to exactly {MAX_DIAGNOSTIC_PROSE_CHARS} chars plus an \
ellipsis; got: {markdown}"
);
assert!(
!markdown
.as_str()
.contains(&"S".repeat(MAX_DIAGNOSTIC_PROSE_CHARS + 1)),
"summary must not exceed the cap; got: {markdown}"
);
assert!(
markdown
.as_str()
.contains(&format!("{}…", "F".repeat(MAX_VERSION_DIAGNOSTIC_CHARS))),
"fixed version must be truncated to exactly {MAX_VERSION_DIAGNOSTIC_CHARS} chars \
plus an ellipsis; got: {markdown}"
);
assert!(
markdown
.as_str()
.contains(&format!("{}…", "V".repeat(MAX_VERSION_DIAGNOSTIC_CHARS))),
"candidate version must be truncated to exactly {MAX_VERSION_DIAGNOSTIC_CHARS} \
chars plus an ellipsis; got: {markdown}"
);
assert!(
markdown
.as_str()
.contains(&format!("{}…", "é".repeat(MAX_DIAGNOSTIC_NAME_CHARS))),
"a multi-byte alias must be truncated on a char boundary, not a byte boundary; \
got: {markdown}"
);
assert!(
markdown.as_str().contains("+4 more"),
"alias list must be capped; got: {markdown}"
);
}
#[test]
fn push_vulnerability_hover_section_converts_fixed_version_to_native_namespace() {
use crate::osv::{
Advisory, Capped, DependencyVulnerabilities, OsvVersion, ScanOutcome, VulnSeverity,
};
const GO_STYLE_FORMATTER: crate::test_util::StubFormatter =
crate::test_util::StubFormatter::new().with_go_style_osv_version_to_native();
let advisory = Advisory::new(
"RUSTSEC-2024-0432".to_string(),
"2024-01-01T00:00:00Z".to_string(),
VulnSeverity::High,
)
.expect("valid osv id")
.with_fixed_versions(vec![OsvVersion::new("0.55.0")]);
let dv = DependencyVulnerabilities::new(Capped::new(vec![Arc::new(advisory)], 1));
let outcome = ScanOutcome::Vulnerable(dv);
let mut markdown = HoverMarkdown::new();
push_vulnerability_hover_section(&mut markdown, &GO_STYLE_FORMATTER, Some(&outcome), None);
assert!(
markdown.as_str().contains("v0.55.0"),
"Fixed in: must render the native-namespace version (v0.55.0), not the raw OSV \
wire spelling (0.55.0); got: {markdown}"
);
assert!(
!markdown.as_str().contains("Fixed in: `0.55.0`"),
"must not render the unconverted OSV wire spelling; got: {markdown}"
);
}
#[test]
fn push_vulnerability_hover_section_labels_sibling_only_advisories() {
use crate::osv::{
Capped, DependencyVulnerabilities, MatchedTags, ScanOutcome, SiblingMatches,
VulnSeverity,
};
let mut matches = SiblingMatches::new(crate::osv::OsvVersion::new("1.0.0"));
matches.insert(
"A-1".to_string(),
MatchedTags::from_tags(vec![
crate::ConcreteVersion::new("v4.9.0"),
crate::ConcreteVersion::new("v4.*10"),
])
.unwrap(),
);
let dv = DependencyVulnerabilities::new(Capped::new(
vec![
sample_advisory("A-1", VulnSeverity::High),
sample_advisory("A-2", VulnSeverity::High),
],
2,
))
.with_sibling_matches(matches);
let mut markdown = HoverMarkdown::new();
push_vulnerability_hover_section(
&mut markdown,
&crate::test_util::StubFormatter::DEFAULT,
Some(&ScanOutcome::Vulnerable(dv)),
None,
);
let rendered = markdown.as_str();
assert_eq!(rendered.matches("matched tag").count(), 1, "{rendered}");
assert!(
rendered.contains(r"*(matched tag v4\.9\.0\, v4\.\*10)*"),
"{rendered}"
);
}
#[test]
fn push_vulnerability_hover_section_hides_non_upgrade_fix_of_sibling_only_advisory() {
use crate::osv::{
Capped, DependencyVulnerabilities, MatchedTags, OsvVersion, ScanOutcome,
SiblingMatches, VulnSeverity,
};
let mut matches = SiblingMatches::new(OsvVersion::new("9.0.0"));
matches.insert(
"A-1".to_string(),
MatchedTags::from_tags(vec![crate::ConcreteVersion::new("v8.0.0")]).unwrap(),
);
let dv = DependencyVulnerabilities::new(Capped::new(
vec![
sample_advisory("A-1", VulnSeverity::High),
sample_advisory("A-2", VulnSeverity::High),
],
2,
))
.with_sibling_matches(matches);
let mut markdown = HoverMarkdown::new();
push_vulnerability_hover_section(
&mut markdown,
&crate::test_util::StubFormatter::DEFAULT,
Some(&ScanOutcome::Vulnerable(dv)),
None,
);
assert_eq!(
markdown.as_str().matches("Fixed in").count(),
1,
"{markdown}"
);
}
#[tokio::test]
async fn test_generate_hover_malicious_advisory_never_renders_unknown_severity() {
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(
crate::test_util::vuln_key("bad-pkg"),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(
vec![sample_advisory("MAL-2025-47141", VulnSeverity::Malicious)],
1,
),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(content.contains("MAL-2025-47141"));
assert!(content.contains("confirmed malicious package"));
assert!(!content.contains("unknown severity"));
}
#[tokio::test]
async fn test_generate_hover_informational_advisory_never_renders_unknown_severity() {
use crate::osv::{
Advisory, Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
let parse_result = MockParseResult {
deps: vec![dep_at("yaml-rust")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let advisory = Advisory::new(
"RUSTSEC-2024-0320".to_string(),
"2024-11-01T12:31:51Z".to_string(),
VulnSeverity::Informational,
)
.expect("valid osv id");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("yaml-rust"),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(vec![std::sync::Arc::new(advisory)], 1),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(content.contains("RUSTSEC-2024-0320"));
assert!(!content.contains("unknown severity"));
assert!(
content.contains(severity_label(VulnSeverity::Informational)),
"label must stand alone as a comprehensible notice: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_suppresses_also_affected_when_candidate_is_all_informational() {
use crate::osv::{
Capped, DependencyVulnerabilities, LatestStatusMap, ScanOutcome, UpgradeStatus,
VulnSeverity, VulnerabilityMap,
};
let parse_result = MockParseResult {
deps: vec![dep_at("yaml-rust")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let advisory = sample_advisory("RUSTSEC-2024-0320", VulnSeverity::Informational);
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("yaml-rust"),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(vec![advisory], 1),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
let mut latest_status = LatestStatusMap::new();
latest_status.insert(
crate::test_util::vuln_key("yaml-rust"),
UpgradeStatus::CandidateVulnerable {
version: ConcreteVersion::new("0.5.0"),
advisory_ids: Capped::new(vec!["RUSTSEC-2024-0320".to_string()], 1),
worst_severity: Some(VulnSeverity::Informational),
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_vulnerabilities(&vulns)
.with_latest_status(&latest_status),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
!content.contains("also affected"),
"an all-Informational candidate-vulnerable set must not render the \
misleading 'also affected' line: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_still_shows_also_affected_when_candidate_is_mixed() {
use crate::osv::{
Capped, DependencyVulnerabilities, LatestStatusMap, ScanOutcome, UpgradeStatus,
VulnSeverity, VulnerabilityMap,
};
let parse_result = MockParseResult {
deps: vec![dep_at("mixed-pkg")],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let resolved_versions = HashMap::new();
let informational_advisory =
sample_advisory("RUSTSEC-2024-0320", VulnSeverity::Informational);
let graded_advisory = sample_advisory("RUSTSEC-2020-0071", VulnSeverity::High);
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("mixed-pkg"),
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(vec![informational_advisory, graded_advisory], 2),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
let mut latest_status = LatestStatusMap::new();
latest_status.insert(
crate::test_util::vuln_key("mixed-pkg"),
UpgradeStatus::CandidateVulnerable {
version: ConcreteVersion::new("2.0.0"),
advisory_ids: Capped::new(
vec![
"RUSTSEC-2024-0320".to_string(),
"RUSTSEC-2020-0071".to_string(),
],
2,
),
worst_severity: Some(VulnSeverity::High),
},
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_vulnerabilities(&vulns)
.with_latest_status(&latest_status),
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("also affected"),
"a mixed informational+graded candidate-vulnerable set must still \
render the line: {}",
content
);
}
#[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,
None,
&MOCK_FORMATTER,
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 {
sibling_matches: None,
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::Critical)],
1,
),
fix_target_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).into(),
versions,
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let patched_content = hover_on_patched.markdown();
assert!(
!patched_content.contains("RUSTSEC-2020-0071"),
"the patched occurrence must not show the other occurrence's advisory: {}",
patched_content
);
assert!(patched_content.contains("No known vulnerabilities"));
let hover_on_vulnerable = generate_hover(
&parse_result,
Position::new(0, 2).into(),
versions,
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let vulnerable_content = hover_on_vulnerable.markdown();
assert!(vulnerable_content.contains("RUSTSEC-2020-0071"));
}
#[tokio::test]
async fn test_generate_hover_attributed_via_resolved_version_candidates() {
use crate::osv::{
Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
let current_major = MockDep {
name: "serde".into(),
version_req: "1.0".into(),
version_range: Range::new(Position::new(0, 8), Position::new(0, 13)),
name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
};
let renamed_old_major = MockDep {
name: "serde".into(),
version_req: "0.9".into(),
version_range: Range::new(Position::new(1, 8), Position::new(1, 13)),
name_range: Range::new(Position::new(1, 0), Position::new(1, 9)),
};
let parse_result = MockParseResult {
deps: vec![current_major, renamed_old_major],
uri: crate::test_util::test_uri("/test/Cargo.toml"),
};
let cached_versions = HashMap::new();
let mut resolved_versions = HashMap::new();
resolved_versions.insert("serde".into(), ConcreteVersion::from("1.0.219"));
let mut resolved_version_candidates = HashMap::new();
resolved_version_candidates.insert(
"serde".into(),
vec![
ConcreteVersion::from("0.9.15"),
ConcreteVersion::from("1.0.219"),
],
);
let keys = crate::osv::vulnerability_keys(
&parse_result,
&resolved_versions,
Some(&resolved_version_candidates),
&MOCK_FORMATTER,
crate::EcosystemId::Cargo,
);
let deps = parse_result.dependencies();
let current_key = keys.get(&deps[0].name_range()).unwrap().clone();
let renamed_key = keys.get(&deps[1].name_range()).unwrap().clone();
assert_ne!(current_key, renamed_key);
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
current_key,
ScanOutcome::Vulnerable(DependencyVulnerabilities {
sibling_matches: None,
advisories: Capped::new(
vec![sample_advisory("RUSTSEC-2020-0071", VulnSeverity::Critical)],
1,
),
fix_target_status: UpgradeStatus::NotChecked,
}),
);
vulns.insert(renamed_key, ScanOutcome::Clean);
let versions = VersionData::new(&cached_versions, &resolved_versions)
.with_resolved_version_candidates(&resolved_version_candidates)
.with_vulnerabilities(&vulns)
.with_ecosystem(crate::EcosystemId::Cargo);
let hover_on_renamed = generate_hover(
&parse_result,
Position::new(1, 2).into(),
versions,
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let renamed_content = hover_on_renamed.markdown();
assert!(
renamed_content.contains("0.9.15"),
"renamed occurrence must show its own resolved version, not the collapsed 1.0.219: {}",
renamed_content
);
assert!(
!renamed_content.contains("RUSTSEC-2020-0071"),
"the renamed (0.9) occurrence must not show the other occurrence's advisory: {}",
renamed_content
);
assert!(renamed_content.contains("No known vulnerabilities"));
let hover_on_current = generate_hover(
&parse_result,
Position::new(0, 2).into(),
versions,
&MockRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let current_content = hover_on_current.markdown();
assert!(current_content.contains("1.0.219"));
assert!(current_content.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).into(),
VersionData::new(&cached_versions, &resolved_versions),
&NotFoundRegistry,
&MOCK_FORMATTER,
crate::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover must still render the basic card on a registry error");
let content = hover.markdown();
assert!(
!content.contains("**Latest**"),
"no version data is available on a registry error; got: {}",
content
);
assert!(
!content.contains("Recent versions"),
"must not render a broken version section from an empty list; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("Press `Cmd+.` to update version"),
"a resolvable source with live version data must show the update footer; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_footer_omitted_for_placeholder_requirement() {
let registry = MockRegistryWithVersions {
versions: vec![MockVersionWithAge {
version: "1.2.3".into(),
yanked: false,
published_at: None,
}],
};
let parse_result = MockParseResult {
deps: vec![MockDep {
name: "serde".into(),
version_req: "{{ SERDE_VERSION }}".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 hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.contains("Press `Cmd+.` to update version"),
"an unresolved template placeholder has no code action `Cmd+.` could ever \
produce, so the footer must not render; got: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let content = hover.markdown();
assert!(
!content.contains("Press `Cmd+.` to update version"),
"a non-resolvable source offers no update code action, even with a cached \
latest value present; got: {}",
content
);
}
#[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(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::QueryFailed),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_network(crate::NetworkMode::Offline)
.with_vulnerabilities(&vulns),
&ErrorRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
!content.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
);
assert!(
content.contains("📴 *Offline: version and vulnerability data not checked*"),
"the existing offline notice must still render; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_network(crate::NetworkMode::Offline),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.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
);
}
#[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).into(),
VersionData::new(&cached, &resolved).with_network(crate::NetworkMode::Offline),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated for a dependency at the cursor");
let content = hover.markdown();
assert!(
content.contains("Offline: version and vulnerability data not checked"),
"a resolvable source must show the offline footer when versions.offline is set; \
got: {}",
content
);
}
#[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).into(),
VersionData::new(&cached_versions, &resolved_versions)
.with_network(crate::NetworkMode::Offline),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should still be generated for a non-resolvable-source dependency");
let content = hover.markdown();
assert!(
!content.contains("Offline:"),
"a non-resolvable source must not show the offline footer, even with \
versions.offline set; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_shown_for_no_concrete_version() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::NoConcreteVersion),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Vulnerability data not checked: no resolved or exact version"),
"a resolvable source with a NoConcreteVersion skip must show the reason-specific \
footer; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_shown_for_resolved_tag_not_full_version() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::ResolvedTagNotFullVersion),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content
.contains("Vulnerability data not checked: the resolved tag is not a full version")
&& !content.contains("no resolved or exact version"),
"got: {content}"
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_shown_for_canonical_name_unconfirmed() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::CanonicalNameUnconfirmed),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content
.contains("Vulnerability data not checked: the repository's canonical name has not been confirmed")
&& !content.contains("no resolved or exact version"),
"got: {content}"
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_omitted_while_offline() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::QueryFailed),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new())
.with_vulnerabilities(&vulns)
.with_network(crate::NetworkMode::Offline),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Offline: version and vulnerability data not checked"),
"offline must still show its own broader footer; got: {}",
content
);
assert!(
!content.contains("Vulnerability data not checked: the OSV.dev query failed"),
"the reason-specific footer must not also render while offline; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_shown_for_query_failed_online() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(SkipReason::QueryFailed),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Vulnerability data not checked: the OSV.dev query failed"),
"an online QueryFailed skip must show the reason-specific footer; got: {}",
content
);
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_covers_remaining_variants() {
use crate::osv::{ScanOutcome, SkipReason, VulnerabilityMap};
let cases = [
(
SkipReason::UnmappableName,
"the package name could not be mapped",
),
(
SkipReason::UnmappableEcosystem,
"this ecosystem is not supported",
),
(
SkipReason::Truncated,
"the OSV.dev result set was truncated",
),
];
for (reason, expected_fragment) in cases {
let parse_result = freshness_test_parse_result("serde");
let mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("serde"),
ScanOutcome::Skipped(reason),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains(expected_fragment),
"{reason:?} must show its reason-specific footer; got: {content}"
);
}
}
#[tokio::test]
async fn test_generate_hover_skip_reason_footer_omitted_for_non_registry_source() {
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 mut vulns: VulnerabilityMap = VulnerabilityMap::new();
vulns.insert(
crate::test_util::vuln_key("path-pkg"),
ScanOutcome::Skipped(SkipReason::NonRegistrySource),
);
let hover = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &HashMap::new()).with_vulnerabilities(&vulns),
&MockRegistry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
!content.contains("Vulnerability data not checked"),
"a non-registry source must never render the reason-specific footer; got: {}",
content
);
}
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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
let line = content
.lines()
.find(|l| l.contains("Supply chain"))
.unwrap_or_else(|| panic!("expected a Supply chain line, got: {}", content));
insta::assert_snapshot!(line, @"🔐 **Supply chain**: OpenSSF Scorecard `8.5`/10 · Provenance: verified");
}
#[tokio::test]
async fn test_generate_hover_deps_dev_license_renders_license_line() {
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": [], "licenses": ["MIT"]}"#,
)
.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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("**License**: `MIT`"),
"expected the deps.dev-sourced license to render; got: {}",
content
);
assert!(
!content.contains("unavailable"),
"no latest version exists to compare against; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Provenance: attested but unverified"),
"got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Provenance: none found"),
"got: {}",
content
);
assert!(
content.contains("*(self-reported repo)*"),
"an UNVERIFIED_METADATA-only relation must be disclosed; got: {}",
content
);
}
#[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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(ecosystem)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
!content.contains("Supply chain"),
"{ecosystem:?}: got: {}",
content
);
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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Cargo)
.with_trust(&deps_dev),
®istry,
&MOCK_WIDENED_RESOLVE_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
!content.contains("Supply chain"),
"a private registry's package name/version must never reach deps.dev; got: {}",
content
);
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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev)
.with_network(crate::NetworkMode::Offline),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(!content.contains("Supply chain"), "got: {}", content);
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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let without_trust = generate_hover(
&parse_result,
Position::new(0, 2).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let a = with_failing_trust.markdown();
let b = without_trust.markdown();
assert!(!a.contains("Supply chain"), "got: {}", a);
assert_eq!(a, b);
}
#[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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let first_content = first.markdown();
assert!(
!first_content.contains("Supply chain"),
"the real ~750ms two-call sequence must exceed the 700ms wait budget on the \
first hover; got: {}",
first_content
);
}
#[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(DepsDevSystem::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).into(),
VersionData::new(&HashMap::new(), &resolved_versions)
.with_ecosystem(crate::EcosystemId::Npm)
.with_trust(&deps_dev),
®istry,
&MOCK_FORMATTER,
crate::freshness::FreshnessSettings::default(),
PublishTime::now(),
)
.await
.expect("hover should be generated");
let content = hover.markdown();
assert!(
content.contains("Supply chain"),
"a hover reading an already-warm memo must render the trust signal \
immediately; got: {}",
content
);
}
}