use super::actions::code_action_response;
use super::backend::{
Backend, RefreshLogSummary, refresh_completed_log_message, refresh_failed_log_message,
};
use super::capabilities::{initialize_result, root_from_initialize_params};
use super::config::LspAnalysisConfig;
use super::diagnostics::{
DiagnosticBatch, WorkspaceDiagnostics, diagnostic_for_classified_seam, diagnostic_for_finding,
diagnostic_refresh_plan, diagnostic_severity_for_class, take_all_uris,
workspace_diagnostic_batches,
};
use super::hover::{hover_response, hover_with_snapshot_status};
use super::state::{AnalysisSnapshot, DocumentStore, RefreshMetadata, format_duration};
use super::uri::{encode_uri_path, file_uri_for_path, path_from_file_uri};
use super::{
COLLECT_CONTEXT_COMMAND, COPY_AFTER_SNAPSHOT_COMMAND, COPY_AGENT_BRIEF_COMMAND,
COPY_AGENT_PACKET_COMMAND, COPY_AGENT_RECEIPT_COMMAND, COPY_AGENT_VERIFY_COMMAND,
COPY_CONTEXT_COMMAND, COPY_SUGGESTED_ASSERTION_COMMAND, COPY_TARGETED_TEST_BRIEF_COMMAND,
HOVER_TEXT, OPEN_RELATED_TEST_COMMAND, REFRESH_COMMAND,
};
use crate::app::Mode;
use crate::domain::{
Confidence, DeltaKind, ExposureClass, Finding, OracleKind, OracleStrength, Probe, ProbeFamily,
ProbeId, RelatedTest, RevealEvidence, RiprEvidence, SourceLocation, StageEvidence, StageState,
};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tower_lsp_server::LanguageServer;
use tower_lsp_server::ls_types::{
CodeActionContext, CodeActionOrCommand, CodeActionParams, DiagnosticSeverity,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
ExecuteCommandParams, HoverContents, HoverParams, HoverProviderCapability, InitializeParams,
MarkedString, NumberOrString, Position, Range, TextDocumentContentChangeEvent,
TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams,
TextDocumentSyncCapability, TextDocumentSyncKind, VersionedTextDocumentIdentifier,
WorkspaceFolder,
};
use tower_lsp_server::{LspService, Server};
#[test]
fn initialize_result_exposes_existing_lsp_capabilities() -> Result<(), String> {
let result = initialize_result();
assert_eq!(
result.capabilities.text_document_sync,
Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL))
);
assert_eq!(
result.capabilities.hover_provider,
Some(HoverProviderCapability::Simple(true))
);
let Some(provider) = result.capabilities.execute_command_provider else {
return Err("expected execute command provider".to_string());
};
let commands = provider.commands;
assert_eq!(commands, vec![REFRESH_COMMAND, COLLECT_CONTEXT_COMMAND]);
Ok(())
}
#[test]
fn framed_lsp_protocol_smoke_exercises_tower_server() -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
let (client_read, mut client_write) = tokio::io::split(client_io);
let (server_read, server_write) = tokio::io::split(server_io);
let (service, socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let mut server_task = tokio::spawn(async move {
Server::new(server_read, server_write, socket)
.serve(service)
.await;
});
let mut client_read = client_read;
let text_uri = "file:///workspace/src/lib.rs";
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"processId": null,
"rootUri": "file:///target/ripr/lsp-protocol-smoke-missing-root",
"capabilities": {}
}
}),
)
.await?;
let initialize = read_lsp_response(&mut client_read, 1).await?;
assert_eq!(
initialize["result"]["capabilities"]["executeCommandProvider"]["commands"][0],
REFRESH_COMMAND
);
assert_eq!(
initialize["result"]["capabilities"]["executeCommandProvider"]["commands"][1],
COLLECT_CONTEXT_COMMAND
);
assert_eq!(
initialize["result"]["capabilities"]["hoverProvider"],
serde_json::Value::Bool(true)
);
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
}),
)
.await?;
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": text_uri,
"languageId": "rust",
"version": 1,
"text": "pub fn demo() -> bool { true }\n"
}
}
}),
)
.await?;
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "workspace/executeCommand",
"params": {
"command": REFRESH_COMMAND,
"arguments": []
}
}),
)
.await?;
let (refresh, notifications) =
read_lsp_response_with_notifications(&mut client_read, 2).await?;
assert!(refresh.get("error").is_none());
assert_eq!(refresh["result"], serde_json::Value::Null);
let notification_messages = log_notification_messages(¬ifications);
assert!(
notification_messages
.iter()
.any(|message| message.contains("ripr analysis refresh started"))
);
assert!(
notification_messages
.iter()
.any(|message| message.contains("ripr analysis refresh failed after"))
);
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": text_uri },
"position": { "line": 0, "character": 4 }
}
}),
)
.await?;
let hover = read_lsp_response(&mut client_read, 3).await?;
let hover_value = hover["result"]["contents"]["value"]
.as_str()
.ok_or_else(|| "expected hover markdown value".to_string())?;
assert!(hover_value.contains("ripr estimates static RIPR exposure"));
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 4,
"method": "textDocument/codeAction",
"params": {
"textDocument": { "uri": text_uri },
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 4 }
},
"context": { "diagnostics": [] }
}
}),
)
.await?;
let actions = read_lsp_response(&mut client_read, 4).await?;
assert_eq!(actions["result"][0]["title"], "Refresh ripr analysis");
assert_eq!(actions["result"][0]["command"]["command"], REFRESH_COMMAND);
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 5,
"method": "shutdown",
"params": null
}),
)
.await?;
let shutdown = read_lsp_response(&mut client_read, 5).await?;
assert!(shutdown.get("error").is_none());
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"method": "exit",
"params": null
}),
)
.await?;
client_write
.shutdown()
.await
.map_err(|err| format!("failed to close test client: {err}"))?;
match tokio::time::timeout(std::time::Duration::from_secs(2), &mut server_task).await {
Ok(join_result) => {
join_result.map_err(|err| format!("LSP server task failed: {err}"))?;
}
Err(_) => {
server_task.abort();
return Err("LSP server did not stop after exit notification".to_string());
}
}
Ok(())
})
}
#[test]
fn framed_lsp_protocol_smoke_logs_successful_refresh_completion() -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (client_io, server_io) = tokio::io::duplex(64 * 1024);
let (client_read, mut client_write) = tokio::io::split(client_io);
let (server_read, server_write) = tokio::io::split(server_io);
let (service, socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let mut server_task = tokio::spawn(async move {
Server::new(server_read, server_write, socket)
.serve(service)
.await;
});
let mut client_read = client_read;
let repo_root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/boundary_gap/input");
let root_uri = file_uri_for_path(&repo_root)?;
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"processId": null,
"rootUri": root_uri.as_str(),
"initializationOptions": {
"baseRef": "HEAD",
"checkMode": "instant"
},
"capabilities": {}
}
}),
)
.await?;
let initialize = read_lsp_response(&mut client_read, 1).await?;
assert!(initialize.get("error").is_none());
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "workspace/executeCommand",
"params": {
"command": REFRESH_COMMAND,
"arguments": []
}
}),
)
.await?;
let (refresh, notifications) =
read_lsp_response_with_notifications(&mut client_read, 2).await?;
assert!(refresh.get("error").is_none());
assert_eq!(refresh["result"], serde_json::Value::Null);
let notification_messages = log_notification_messages(¬ifications);
assert!(
notification_messages
.iter()
.any(|message| message.contains("ripr analysis refresh started"))
);
assert!(
notification_messages
.iter()
.any(|message| message.contains("ripr analysis refresh completed in"))
);
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"id": 3,
"method": "shutdown",
"params": null
}),
)
.await?;
let shutdown = read_lsp_response(&mut client_read, 3).await?;
assert!(shutdown.get("error").is_none());
write_lsp_message(
&mut client_write,
serde_json::json!({
"jsonrpc": "2.0",
"method": "exit",
"params": null
}),
)
.await?;
client_write
.shutdown()
.await
.map_err(|err| format!("failed to close test client: {err}"))?;
match tokio::time::timeout(std::time::Duration::from_secs(2), &mut server_task).await {
Ok(join_result) => {
join_result.map_err(|err| format!("LSP server task failed: {err}"))?;
}
Err(_) => {
server_task.abort();
return Err("LSP server did not stop after exit notification".to_string());
}
}
Ok(())
})
}
#[test]
fn hover_response_keeps_current_guidance_text() -> Result<(), String> {
let hover = hover_response();
match hover.contents {
HoverContents::Markup(markup) => {
assert_eq!(markup.value, HOVER_TEXT);
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn hover_for_position_uses_latest_matching_diagnostic() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected diagnostic hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("**ripr** `weakly_exposed`"));
assert!(markup.value.contains("Add an exact boundary assertion."));
assert!(markup.value.contains("## RIPR Evidence"));
assert!(markup.value.contains("* reach yes: related tests found"));
assert!(
markup
.value
.contains("* infection yes: predicate can alter branch behavior")
);
assert!(
markup
.value
.contains("* propagation yes: branch influences return value")
);
assert!(
markup
.value
.contains("* observation weak: return value asserted")
);
assert!(
markup
.value
.contains("* discriminator weak: boundary value missing")
);
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn hover_for_position_shows_snapshot_age_and_refresh_duration() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic],
vec![finding],
);
diagnostics
.snapshot
.refresh
.record_duration(Duration::from_millis(42));
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected diagnostic hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("Analysis snapshot: generated "));
assert!(markup.value.contains(" ago; last refresh took 42 ms."));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn hover_for_position_adds_snapshot_status_to_seam_hover() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let seam = sample_classified_seam();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let line = diagnostic.range.start.line;
let mut diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic],
Vec::new(),
);
diagnostics.snapshot.classified_seams = vec![seam];
diagnostics
.snapshot
.refresh
.record_duration(Duration::from_millis(11));
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, line, 1)) else {
return Err("expected seam hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("**ripr** behavioral seam"));
assert!(markup.value.contains("`weakly_gripped`"));
assert!(markup.value.contains("Analysis snapshot: generated "));
assert!(markup.value.contains(" ago; last refresh took 11 ms."));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn snapshot_status_leaves_non_markup_hover_content_unchanged() -> Result<(), String> {
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot =
sample_analysis_snapshot(PathBuf::from("/workspace"), uri, Vec::new(), Vec::new());
let hover = tower_lsp_server::ls_types::Hover {
contents: HoverContents::Scalar(MarkedString::String("plain".to_string())),
range: None,
};
let hover = hover_with_snapshot_status(hover, &snapshot);
match hover.contents {
HoverContents::Scalar(MarkedString::String(value)) => {
assert_eq!(value, "plain");
Ok(())
}
_ => Err("expected scalar hover".to_string()),
}
}
#[test]
fn hover_fallback_to_diagnostic_without_matching_finding() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut mismatched_finding = sample_finding();
mismatched_finding.id = "probe:other:1:predicate".to_string();
mismatched_finding.probe.id.0 = "probe:other:1:predicate".to_string();
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![mismatched_finding],
);
let batches = vec![DiagnosticBatch {
uri: uri.clone(),
diagnostics: vec![diagnostic.clone()],
}];
let workspace_diagnostics = WorkspaceDiagnostics { snapshot, batches };
let Some(_) = backend.refresh_plan(workspace_diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected diagnostic hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("**ripr** `weakly_exposed`"));
assert!(markup.value.contains("Add an exact boundary assertion."));
assert!(
markup
.value
.contains("Finding: `probe:pricing:88:predicate`")
);
assert!(!markup.value.contains("## RIPR Evidence"));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn hover_for_position_returns_none_when_no_diagnostic_matches() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
assert!(
backend
.hover_for_position(&hover_params(uri, 0, 1))
.is_none(),
"expected None when no diagnostic matches position"
);
let generic = hover_response();
match generic.contents {
HoverContents::Markup(markup) => {
assert_eq!(markup.value, HOVER_TEXT);
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn finding_hover_renders_related_tests_and_oracle_text() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let mut finding = sample_finding();
finding.related_tests.push(RelatedTest {
name: "discount_boundary_is_exact".to_string(),
file: PathBuf::from("tests/pricing.rs"),
line: 12,
oracle: Some("assert_eq!(total, expected)".to_string()),
oracle_kind: OracleKind::ExactValue,
oracle_strength: OracleStrength::Strong,
});
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected finding hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("## Related Tests"));
assert!(
markup
.value
.contains("`tests/pricing.rs:12` `discount_boundary_is_exact`")
);
assert!(
markup
.value
.contains("\u{2014} strong exact_value oracle: assert_eq!(total, expected)")
);
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn finding_hover_renders_weakness_section() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let mut finding = sample_finding();
finding
.missing
.push("no equality-boundary case was found".to_string());
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected finding hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("## Weakness"));
assert!(
markup
.value
.contains("- no equality-boundary case was found")
);
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn finding_hover_avoids_mutation_runtime_terms() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected finding hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
let banned: Vec<String> = vec![
std::iter::once('k').chain("illed".chars()).collect(),
std::iter::once('s').chain("urvived".chars()).collect(),
std::iter::once('p').chain("roven".chars()).collect(),
std::iter::once('a').chain("dequate".chars()).collect(),
std::iter::once('u').chain("ntested".chars()).collect(),
];
for term in banned {
assert!(
!markup.value.to_ascii_lowercase().contains(&term),
"hover contained banned mutation-runtime term: {term}"
);
}
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn analysis_snapshot_finds_finding_from_diagnostic_data() -> Result<(), String> {
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
vec![finding],
);
let Some(found) = snapshot.finding_for_diagnostic(&diagnostic) else {
return Err("expected finding from diagnostic data".to_string());
};
assert_eq!(found.id, "probe:pricing:88:predicate");
assert_eq!(found.probe.expression, "amount >= threshold");
Ok(())
}
#[test]
fn overlapping_diagnostics_prefer_seam_id_lookup_over_finding_id_lookup() -> Result<(), String> {
let finding = sample_finding();
let finding_diag = diagnostic_for_finding(Path::new("/workspace"), &finding);
let mut seam_diag = finding_diag.clone();
seam_diag.data = Some(serde_json::json!({
"schema_version": "0.1",
"seam_id": "f3c9e4d21a0b7c88",
"seam_kind": "predicate_boundary",
"grip_class": "weakly_gripped",
}));
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![finding_diag.clone(), seam_diag.clone()],
vec![finding],
);
if snapshot.finding_for_diagnostic(&finding_diag).is_none() {
return Err("finding lookup should still resolve".to_string());
}
if snapshot
.classified_seam_for_diagnostic(&finding_diag)
.is_some()
{
return Err(
"classified_seam_for_diagnostic should reject diagnostics carrying finding_id only"
.to_string(),
);
}
Ok(())
}
#[test]
fn given_diagnostic_with_unknown_seam_id_when_lookup_runs_then_no_classified_seam_is_returned()
-> Result<(), String> {
let finding = sample_finding();
let mut diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
diagnostic.data = Some(serde_json::json!({
"schema_version": "0.1",
"seam_id": "deadbeef00000000",
"seam_kind": "predicate_boundary",
"grip_class": "weakly_gripped",
}));
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
vec![finding],
);
if snapshot
.classified_seam_for_diagnostic(&diagnostic)
.is_some()
{
return Err("expected None for unknown seam_id".to_string());
}
if snapshot.finding_for_diagnostic(&diagnostic).is_some() {
return Err(
"expected None for finding_for_diagnostic when seam_id is set instead of finding_id"
.to_string(),
);
}
Ok(())
}
#[test]
fn given_finding_diagnostic_when_lookup_runs_then_finding_hover_path_still_resolves()
-> Result<(), String> {
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
vec![finding],
);
if snapshot
.classified_seam_for_diagnostic(&diagnostic)
.is_some()
{
return Err("Finding diagnostics carry finding_id, not seam_id; \
classified_seam_for_diagnostic should return None"
.to_string());
}
if snapshot.finding_for_diagnostic(&diagnostic).is_none() {
return Err("expected Finding hover lookup to still work".to_string());
}
Ok(())
}
#[test]
fn refresh_plan_stores_latest_analysis_snapshot() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(latest) = backend.latest_analysis_snapshot() else {
return Err("expected latest analysis snapshot".to_string());
};
assert_eq!(latest.root, PathBuf::from("/workspace"));
assert_eq!(latest.base.as_deref(), Some("origin/main"));
assert_eq!(latest.mode, Mode::Draft);
assert_eq!(latest.findings.len(), 1);
assert_eq!(latest.diagnostics_by_uri.len(), 1);
Ok(())
}
#[test]
fn refresh_plan_stores_snapshot_refresh_metadata() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri,
vec![diagnostic],
vec![finding],
);
diagnostics
.snapshot
.refresh
.record_duration(Duration::from_millis(42));
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(latest) = backend.latest_analysis_snapshot() else {
return Err("expected latest analysis snapshot".to_string());
};
assert_eq!(latest.refresh.duration, Some(Duration::from_millis(42)));
assert!(latest.refresh.age().is_some());
assert_eq!(latest.diagnostic_count(), 1);
assert_eq!(latest.diagnostic_uri_count(), 1);
assert_eq!(latest.finding_count(), 1);
assert_eq!(latest.seam_diagnostic_count(), 0);
Ok(())
}
#[test]
fn refresh_completion_log_message_includes_duration_and_counts() -> Result<(), String> {
let seam = sample_classified_seam();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic],
Vec::new(),
);
snapshot.classified_seams = vec![seam];
snapshot.refresh.record_duration(Duration::from_millis(17));
let summary = RefreshLogSummary::from_snapshot(7, &snapshot);
let message = refresh_completed_log_message(&summary, 1, 2);
assert!(message.contains("ripr analysis refresh completed in 17 ms"));
assert!(message.contains("generation=7"));
assert!(message.contains("diagnostics=1"));
assert!(message.contains("files=1"));
assert!(message.contains("findings=0"));
assert!(message.contains("seam_diagnostics=1"));
assert!(message.contains("published_files=1"));
assert!(message.contains("cleared_files=2"));
Ok(())
}
#[test]
fn refresh_completion_log_message_defaults_missing_duration_to_zero() -> Result<(), String> {
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic],
vec![finding],
);
let summary = RefreshLogSummary::from_snapshot(3, &snapshot);
let message = refresh_completed_log_message(&summary, 1, 0);
assert!(message.contains("ripr analysis refresh completed in 0 ms"));
Ok(())
}
#[test]
fn refresh_failure_log_message_includes_actionable_duration() {
let message = refresh_failed_log_message(
"workspace analysis failed: Cargo.toml not found",
Duration::from_millis(9),
);
assert_eq!(
message,
"ripr analysis refresh failed after 9 ms: workspace analysis failed: Cargo.toml not found"
);
}
#[test]
fn format_duration_renders_milliseconds_and_whole_seconds() {
assert_eq!(format_duration(Duration::from_millis(9)), "9 ms");
assert_eq!(format_duration(Duration::from_secs(1)), "1 second");
assert_eq!(format_duration(Duration::from_secs(2)), "2 seconds");
}
#[test]
fn refresh_metadata_default_records_generation_time() {
let metadata = RefreshMetadata::default();
assert!(metadata.age().is_some());
assert_eq!(metadata.duration, None);
}
#[test]
fn stale_refresh_generation_does_not_store_older_snapshot() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let Some(first_generation) = backend.next_refresh_generation() else {
return Err("expected first generation".to_string());
};
let Some(second_generation) = backend.next_refresh_generation() else {
return Err("expected second generation".to_string());
};
assert!(!backend.is_current_refresh_generation(first_generation));
assert!(backend.is_current_refresh_generation(second_generation));
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let current_uri = test_uri("file:///workspace/src/current.rs")?;
let current = sample_workspace_diagnostics(
PathBuf::from("/workspace/current"),
current_uri,
vec![diagnostic],
vec![finding],
);
let Some(_) = backend.refresh_plan(current) else {
return Err("expected current refresh plan".to_string());
};
if backend.is_current_refresh_generation(first_generation) {
let stale = sample_workspace_diagnostics(
PathBuf::from("/workspace/stale"),
test_uri("file:///workspace/src/stale.rs")?,
Vec::new(),
Vec::new(),
);
let Some(_) = backend.refresh_plan(stale) else {
return Err("expected stale refresh plan".to_string());
};
}
let Some(latest) = backend.latest_analysis_snapshot() else {
return Err("expected latest analysis snapshot".to_string());
};
assert_eq!(latest.root, PathBuf::from("/workspace/current"));
Ok(())
}
#[test]
fn refresh_plan_rejects_mismatched_snapshot_and_batches() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let baseline = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding.clone()],
);
let Some(_) = backend.refresh_plan(baseline) else {
return Err("expected baseline refresh plan".to_string());
};
let mismatched = WorkspaceDiagnostics {
snapshot: sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic],
vec![finding],
),
batches: Vec::new(),
};
assert!(backend.refresh_plan(mismatched).is_none());
let Some(latest) = backend.latest_analysis_snapshot() else {
return Err("expected baseline snapshot to remain stored".to_string());
};
assert_eq!(latest.findings.len(), 1);
assert_eq!(latest.diagnostics_by_uri.len(), 1);
Ok(())
}
#[test]
fn code_action_response_keeps_current_commands() -> Result<(), String> {
let mut finding = sample_finding();
finding.related_tests.clear();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let actions = code_action_response(&code_action_params(vec![diagnostic])?, None);
let mut titles_kinds_and_commands = Vec::new();
let mut command_arguments = Vec::new();
for action in &actions {
match action {
CodeActionOrCommand::CodeAction(action) => {
let Some(command) = &action.command else {
return Err("expected code action command".to_string());
};
let Some(kind) = &action.kind else {
return Err("expected code action kind".to_string());
};
titles_kinds_and_commands.push((
action.title.as_str(),
kind.as_str(),
command.title.as_str(),
command.command.as_str(),
));
command_arguments.push(command.arguments.clone());
}
CodeActionOrCommand::Command(_) => {
return Err("expected code action".to_string());
}
}
}
assert_eq!(
titles_kinds_and_commands,
vec![
(
"Copy ripr context packet",
"quickfix",
"Copy ripr context",
COPY_CONTEXT_COMMAND,
),
(
"Refresh ripr analysis",
"source",
"Refresh ripr analysis",
REFRESH_COMMAND,
),
]
);
let Some(Some(arguments)) = command_arguments.first() else {
return Err("expected copy context arguments".to_string());
};
assert_eq!(arguments[0]["uri"], "file:///workspace/src/pricing.rs");
assert_eq!(arguments[0]["line"], 88);
assert_eq!(arguments[0]["finding_id"], "probe:pricing:88:predicate");
assert_eq!(arguments[0]["probe_id"], "probe:pricing:88:predicate");
Ok(())
}
#[test]
fn code_action_response_omits_context_action_without_ripr_diagnostic() -> Result<(), String> {
let actions = code_action_response(&code_action_params(Vec::new())?, None);
assert_eq!(actions.len(), 1);
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
return Err("expected code action".to_string());
};
let Some(command) = &action.command else {
return Err("expected refresh command".to_string());
};
assert_eq!(command.command, REFRESH_COMMAND);
Ok(())
}
#[test]
fn seam_code_actions_surface_packet_assertion_related_test_and_refresh() -> Result<(), String> {
let seam = sample_classified_seam();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.classified_seams = vec![seam.clone()];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
assert_eq!(
commands
.iter()
.map(|(_, command, _)| command.as_str())
.collect::<Vec<_>>(),
vec![
COPY_CONTEXT_COMMAND,
COPY_TARGETED_TEST_BRIEF_COMMAND,
COPY_AGENT_PACKET_COMMAND,
COPY_AGENT_BRIEF_COMMAND,
COPY_AFTER_SNAPSHOT_COMMAND,
COPY_AGENT_VERIFY_COMMAND,
COPY_AGENT_RECEIPT_COMMAND,
COPY_SUGGESTED_ASSERTION_COMMAND,
OPEN_RELATED_TEST_COMMAND,
REFRESH_COMMAND,
]
);
assert_eq!(commands[0].0, "Copy seam packet");
assert_eq!(commands[0].2[0]["seam_id"], seam.seam.id().as_str());
assert_eq!(commands[0].2[0]["seam_kind"], "predicate_boundary");
assert_eq!(commands[0].2[0]["line"], 88);
assert_eq!(commands[1].0, "Copy targeted test brief");
assert_eq!(commands[1].2[0]["seam_id"], seam.seam.id().as_str());
assert!(
commands[1].2[0]["brief"]
.as_str()
.is_some_and(|value| value.contains("Add a targeted test:")),
"expected targeted test brief argument, got {:?}",
commands[1].2
);
assert_eq!(commands[2].0, "Copy Agent Packet Command");
assert_eq!(commands[2].2[0]["label"], "agent_packet");
assert_eq!(commands[2].2[0]["root"], ".");
assert_eq!(commands[2].2[0]["mode"], "draft");
assert_eq!(commands[2].2[0]["seam_id"], seam.seam.id().as_str());
assert_eq!(commands[2].2[0]["seam_kind"], "predicate_boundary");
assert_eq!(commands[2].2[0]["seam_file"], "src/pricing.rs");
assert_eq!(commands[2].2[0]["owner"], "pricing::discounted_total");
assert_eq!(commands[2].2[0]["line"], 88);
assert_eq!(commands[2].2[0]["severity"], "warning");
assert_eq!(
commands[2].2[0]["target_artifact"],
"target/ripr/agent/agent-packet.json"
);
assert_eq!(
commands[2].2[0]["command"],
format!(
"ripr agent packet --root . --seam-id {} --json > target/ripr/agent/agent-packet.json",
seam.seam.id().as_str()
)
);
assert_eq!(commands[3].0, "Copy Agent Brief Command");
assert_eq!(
commands[3].2[0]["command"],
format!(
"ripr agent brief --root . --seam-id {} --json > target/ripr/agent/agent-brief.json",
seam.seam.id().as_str()
)
);
assert_eq!(commands[4].0, "Copy After Snapshot Command");
assert_eq!(
commands[4].2[0]["command"],
"ripr check --root . --mode draft --format repo-exposure-json > target/ripr/pilot/after.repo-exposure.json"
);
assert_eq!(commands[5].0, "Copy Agent Verify Command");
assert_eq!(
commands[5].2[0]["command"],
"ripr agent verify --root . --before target/ripr/pilot/repo-exposure.json --after target/ripr/pilot/after.repo-exposure.json --json > target/ripr/agent/agent-verify.json"
);
assert_eq!(commands[6].0, "Copy Agent Receipt Command");
assert_eq!(
commands[6].2[0]["command"],
format!(
"ripr agent receipt --root . --verify-json target/ripr/agent/agent-verify.json --seam-id {} --json --out target/ripr/agent/agent-receipt.json",
seam.seam.id().as_str()
)
);
assert_eq!(commands[7].0, "Copy suggested assertion");
assert!(
commands[7].2[0]["assertion"]
.as_str()
.is_some_and(|value| value.contains("assert_eq!(discounted_total")),
"expected assertion argument, got {:?}",
commands[7].2
);
assert_eq!(commands[8].0, "Open best related test");
assert_eq!(
commands[8].2[0]["uri"],
"file:///workspace/tests/pricing.rs"
);
assert_eq!(commands[8].2[0]["line"], 12);
Ok(())
}
#[test]
fn agent_loop_command_payloads_stay_workspace_relative_for_platform_roots() -> Result<(), String> {
let seam = sample_classified_seam();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from(r"workspace root\ripr workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.mode = Mode::Ready;
snapshot.classified_seams = vec![seam.clone()];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
let expected_commands = [
(
COPY_AGENT_PACKET_COMMAND,
"agent_packet",
"target/ripr/agent/agent-packet.json",
format!(
"ripr agent packet --root . --seam-id {} --json > target/ripr/agent/agent-packet.json",
seam.seam.id().as_str()
),
),
(
COPY_AGENT_BRIEF_COMMAND,
"agent_brief",
"target/ripr/agent/agent-brief.json",
format!(
"ripr agent brief --root . --seam-id {} --json > target/ripr/agent/agent-brief.json",
seam.seam.id().as_str()
),
),
(
COPY_AFTER_SNAPSHOT_COMMAND,
"after_snapshot",
"target/ripr/pilot/after.repo-exposure.json",
"ripr check --root . --mode ready --format repo-exposure-json > target/ripr/pilot/after.repo-exposure.json"
.to_string(),
),
(
COPY_AGENT_VERIFY_COMMAND,
"agent_verify",
"target/ripr/agent/agent-verify.json",
"ripr agent verify --root . --before target/ripr/pilot/repo-exposure.json --after target/ripr/pilot/after.repo-exposure.json --json > target/ripr/agent/agent-verify.json"
.to_string(),
),
(
COPY_AGENT_RECEIPT_COMMAND,
"agent_receipt",
"target/ripr/agent/agent-receipt.json",
format!(
"ripr agent receipt --root . --verify-json target/ripr/agent/agent-verify.json --seam-id {} --json --out target/ripr/agent/agent-receipt.json",
seam.seam.id().as_str()
),
),
];
for (command_id, label, target_artifact, expected_command) in expected_commands {
let argument = commands
.iter()
.find(|(_, command, _)| command == command_id)
.and_then(|(_, _, arguments)| arguments.first())
.ok_or_else(|| format!("missing command payload for {command_id}"))?;
assert_eq!(argument["label"], label);
assert_eq!(argument["root"], ".");
assert_eq!(argument["mode"], "ready");
assert_eq!(argument["seam_id"], seam.seam.id().as_str());
assert_eq!(argument["seam_file"], "src/pricing.rs");
assert_eq!(argument["owner"], "pricing::discounted_total");
assert_eq!(argument["severity"], "warning");
assert_eq!(argument["target_artifact"], target_artifact);
assert_eq!(argument["command"], expected_command);
let copied = argument["command"]
.as_str()
.ok_or_else(|| "expected command string".to_string())?;
assert!(
!copied.contains('\\'),
"copied commands should use workspace-relative slash paths, got {copied}"
);
assert!(
!copied.contains("ripr workspace"),
"copied commands should not leak platform-specific workspace roots, got {copied}"
);
}
Ok(())
}
#[test]
fn seam_code_actions_fail_closed_for_stale_seam_diagnostic() -> Result<(), String> {
let seam = sample_classified_seam();
let mut diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
diagnostic.data = Some(serde_json::json!({
"schema_version": "0.1",
"seam_id": "deadbeef00000000",
"seam_kind": "predicate_boundary",
}));
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.classified_seams = vec![seam];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
assert_eq!(
commands
.iter()
.map(|(title, command, _)| (title.as_str(), command.as_str()))
.collect::<Vec<_>>(),
vec![("Refresh ripr analysis", REFRESH_COMMAND)]
);
Ok(())
}
#[test]
fn seam_code_actions_keep_legacy_finding_context_when_both_diagnostics_are_present()
-> Result<(), String> {
let seam = sample_classified_seam();
let seam_diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let finding_diagnostic = diagnostic_for_finding(Path::new("/workspace"), &sample_finding());
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![seam_diagnostic.clone(), finding_diagnostic.clone()],
vec![sample_finding()],
);
snapshot.classified_seams = vec![seam.clone()];
let actions = code_action_response(
&code_action_params(vec![seam_diagnostic, finding_diagnostic])?,
Some(&snapshot),
);
let commands = code_action_commands(&actions)?;
assert_eq!(
commands
.iter()
.map(|(title, _, _)| title.as_str())
.collect::<Vec<_>>(),
vec![
"Copy seam packet",
"Copy targeted test brief",
"Copy Agent Packet Command",
"Copy Agent Brief Command",
"Copy After Snapshot Command",
"Copy Agent Verify Command",
"Copy Agent Receipt Command",
"Copy suggested assertion",
"Open best related test",
"Copy ripr context packet",
"Refresh ripr analysis",
]
);
assert_eq!(commands[0].2[0]["seam_id"], seam.seam.id().as_str());
assert_eq!(commands[9].2[0]["finding_id"], "probe:pricing:88:predicate");
assert_eq!(commands[9].2[0]["probe_id"], "probe:pricing:88:predicate");
Ok(())
}
#[test]
fn seam_code_actions_open_strong_related_test_before_first_related_test() -> Result<(), String> {
use crate::analysis::test_grip_evidence::{
RelatedTestGrip, RelationConfidence, RelationReason,
};
let mut seam = sample_classified_seam();
seam.evidence.related_tests = vec![
RelatedTestGrip {
test_name: "nearby_smoke_reaches_owner".to_string(),
file: PathBuf::from("tests/smoke.rs"),
line: 7,
oracle_kind: OracleKind::SmokeOnly,
oracle_strength: OracleStrength::Smoke,
evidence_summary: "smoke-only assertion".to_string(),
relation_reason: RelationReason::DirectOwnerCall,
relation_confidence: RelationConfidence::High,
},
RelatedTestGrip {
test_name: "below_threshold_has_no_discount".to_string(),
file: PathBuf::from("tests/pricing.rs"),
line: 12,
oracle_kind: OracleKind::ExactValue,
oracle_strength: OracleStrength::Strong,
evidence_summary: "exact value assertion".to_string(),
relation_reason: RelationReason::DirectOwnerCall,
relation_confidence: RelationConfidence::Medium,
},
];
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.classified_seams = vec![seam];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
let Some((_, command, args)) = commands
.iter()
.find(|(title, _, _)| title == "Open best related test")
else {
return Err(format!(
"expected open related test action, got {commands:?}"
));
};
assert_eq!(command, OPEN_RELATED_TEST_COMMAND);
assert_eq!(args[0]["uri"], "file:///workspace/tests/pricing.rs");
assert_eq!(args[0]["test_name"], "below_threshold_has_no_discount");
Ok(())
}
#[test]
fn seam_code_actions_open_highest_confidence_related_test_when_no_strong_test_exists()
-> Result<(), String> {
use crate::analysis::test_grip_evidence::{
RelatedTestGrip, RelationConfidence, RelationReason,
};
let mut seam = sample_classified_seam();
seam.evidence.related_tests = vec![
RelatedTestGrip {
test_name: "opaque_fixture_hint".to_string(),
file: PathBuf::from("tests/opaque.rs"),
line: 3,
oracle_kind: OracleKind::Unknown,
oracle_strength: OracleStrength::None,
evidence_summary: "opaque relation".to_string(),
relation_reason: RelationReason::FixtureOwnerAffinity,
relation_confidence: RelationConfidence::Opaque,
},
RelatedTestGrip {
test_name: "low_confidence_smoke".to_string(),
file: PathBuf::from("tests/low.rs"),
line: 5,
oracle_kind: OracleKind::SmokeOnly,
oracle_strength: OracleStrength::Smoke,
evidence_summary: "smoke-only assertion".to_string(),
relation_reason: RelationReason::FixtureOwnerAffinity,
relation_confidence: RelationConfidence::Low,
},
RelatedTestGrip {
test_name: "medium_confidence_property".to_string(),
file: PathBuf::from("tests/medium.rs"),
line: 9,
oracle_kind: OracleKind::RelationalCheck,
oracle_strength: OracleStrength::Medium,
evidence_summary: "medium oracle".to_string(),
relation_reason: RelationReason::SameModule,
relation_confidence: RelationConfidence::Medium,
},
RelatedTestGrip {
test_name: "high_confidence_weak_assertion".to_string(),
file: PathBuf::from("tests/high.rs"),
line: 11,
oracle_kind: OracleKind::RelationalCheck,
oracle_strength: OracleStrength::Weak,
evidence_summary: "weak oracle".to_string(),
relation_reason: RelationReason::DirectOwnerCall,
relation_confidence: RelationConfidence::High,
},
];
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.classified_seams = vec![seam];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
let Some((_, command, args)) = commands
.iter()
.find(|(title, _, _)| title == "Open best related test")
else {
return Err(format!(
"expected open related test action, got {commands:?}"
));
};
assert_eq!(command, OPEN_RELATED_TEST_COMMAND);
assert_eq!(args[0]["uri"], "file:///workspace/tests/high.rs");
assert_eq!(args[0]["test_name"], "high_confidence_weak_assertion");
Ok(())
}
#[test]
fn seam_code_actions_omit_assertion_and_related_test_when_evidence_is_missing() -> Result<(), String>
{
let seam = sample_side_effect_seam_without_related_tests();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/service.rs")?;
let mut snapshot = sample_analysis_snapshot(
PathBuf::from("/workspace"),
uri,
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.classified_seams = vec![seam];
let actions = code_action_response(&code_action_params(vec![diagnostic])?, Some(&snapshot));
let commands = code_action_commands(&actions)?;
assert_eq!(
commands
.iter()
.map(|(_, command, _)| command.as_str())
.collect::<Vec<_>>(),
vec![
COPY_CONTEXT_COMMAND,
COPY_TARGETED_TEST_BRIEF_COMMAND,
COPY_AGENT_PACKET_COMMAND,
COPY_AGENT_BRIEF_COMMAND,
COPY_AFTER_SNAPSHOT_COMMAND,
COPY_AGENT_VERIFY_COMMAND,
COPY_AGENT_RECEIPT_COMMAND,
REFRESH_COMMAND
]
);
assert_eq!(commands[0].0, "Copy seam packet");
assert_eq!(commands[1].0, "Copy targeted test brief");
assert_eq!(commands[2].0, "Copy Agent Packet Command");
assert_eq!(commands[6].0, "Copy Agent Receipt Command");
Ok(())
}
#[test]
fn boundary_gap_lsp_diagnostics_match_fixture_expectation() -> Result<(), String> {
let (diagnostics, _) = boundary_gap_lsp_fixture_outputs()?;
assert_json_fixture("lsp-diagnostics.json", diagnostics)
}
#[test]
fn boundary_gap_lsp_code_actions_match_fixture_expectation() -> Result<(), String> {
let (_, actions) = boundary_gap_lsp_fixture_outputs()?;
assert_json_fixture("lsp-code-actions.json", actions)
}
#[test]
fn diagnostic_for_finding_preserves_lsp_payload_shape() -> Result<(), String> {
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
assert_eq!(diagnostic.range.start.line, 87);
assert_eq!(diagnostic.range.start.character, 0);
assert_eq!(diagnostic.range.end.line, 87);
assert_eq!(diagnostic.range.end.character, 19);
assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
assert_eq!(
diagnostic.code,
Some(NumberOrString::String("weakly_exposed".to_string()))
);
assert_eq!(diagnostic.source.as_deref(), Some("ripr"));
assert_eq!(diagnostic.message, "Add an exact boundary assertion.");
let Some(data) = diagnostic.data else {
return Err("expected diagnostic data".to_string());
};
assert_eq!(data["schema_version"], "0.1");
assert_eq!(data["finding_id"], "probe:pricing:88:predicate");
assert_eq!(data["probe_id"], "probe:pricing:88:predicate");
assert_eq!(data["classification"], "weakly_exposed");
assert_eq!(data["probe_family"], "predicate");
assert_eq!(data["confidence"], 0.75);
assert_eq!(data["source_range"]["file"], "src/pricing.rs");
assert_eq!(data["source_range"]["line"], 88);
assert_eq!(data["source_range"]["column"], 1);
Ok(())
}
#[test]
fn diagnostic_for_finding_uses_probe_column_and_expression_width() {
let mut finding = sample_finding();
finding.probe.location.column = 5;
finding.probe.expression = "total".to_string();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
assert_eq!(diagnostic.range.start.line, 87);
assert_eq!(diagnostic.range.start.character, 4);
assert_eq!(diagnostic.range.end.line, 87);
assert_eq!(diagnostic.range.end.character, 9);
}
#[test]
fn diagnostic_for_finding_uses_one_character_range_for_empty_expression() {
let mut finding = sample_finding();
finding.probe.location.column = 3;
finding.probe.expression.clear();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
assert_eq!(diagnostic.range.start.character, 2);
assert_eq!(diagnostic.range.end.character, 3);
}
#[test]
fn diagnostic_for_finding_attaches_related_test_information() -> Result<(), String> {
let mut finding = sample_finding();
finding.related_tests.push(RelatedTest {
name: "discount_boundary_is_exact".to_string(),
file: PathBuf::from("tests/pricing.rs"),
line: 12,
oracle: Some("assert_eq!(total, expected)".to_string()),
oracle_kind: OracleKind::ExactValue,
oracle_strength: OracleStrength::Strong,
});
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let Some(related) = diagnostic.related_information else {
return Err("expected related diagnostic information".to_string());
};
assert_eq!(related.len(), 1);
assert_eq!(
related[0].location.uri.as_str(),
"file:///workspace/tests/pricing.rs"
);
assert_eq!(related[0].location.range.start.line, 11);
assert_eq!(
related[0].message,
"Related test `discount_boundary_is_exact` has strong oracle: assert_eq!(total, expected)"
);
Ok(())
}
#[test]
fn diagnostic_severity_tracks_static_exposure_class() {
let cases = [
(ExposureClass::Exposed, DiagnosticSeverity::INFORMATION),
(ExposureClass::WeaklyExposed, DiagnosticSeverity::WARNING),
(
ExposureClass::ReachableUnrevealed,
DiagnosticSeverity::WARNING,
),
(ExposureClass::NoStaticPath, DiagnosticSeverity::WARNING),
(ExposureClass::InfectionUnknown, DiagnosticSeverity::WARNING),
(
ExposureClass::PropagationUnknown,
DiagnosticSeverity::INFORMATION,
),
(
ExposureClass::StaticUnknown,
DiagnosticSeverity::INFORMATION,
),
];
for (class, expected) in cases {
assert_eq!(diagnostic_severity_for_class(&class), expected);
}
}
#[test]
fn diagnostic_refresh_plan_clears_stale_previous_uris() -> Result<(), String> {
let stale_uri = test_uri("file:///workspace/src/stale.rs")?;
let current_uri = test_uri("file:///workspace/src/current.rs")?;
let mut previous_uris = BTreeSet::new();
previous_uris.insert(stale_uri.clone());
previous_uris.insert(current_uri.clone());
let plan = diagnostic_refresh_plan(
&previous_uris,
vec![DiagnosticBatch {
uri: current_uri.clone(),
diagnostics: Vec::new(),
}],
);
assert_eq!(plan.publish_batches.len(), 1);
assert_eq!(plan.publish_batches[0].uri, current_uri);
assert_eq!(plan.clear_uris, vec![stale_uri]);
assert_eq!(plan.current_uris.len(), 1);
Ok(())
}
#[test]
fn take_all_uris_returns_and_clears_previous_diagnostic_uris() -> Result<(), String> {
let first_uri = test_uri("file:///workspace/src/first.rs")?;
let second_uri = test_uri("file:///workspace/src/second.rs")?;
let mut uris = BTreeSet::new();
uris.insert(first_uri.clone());
uris.insert(second_uri.clone());
let cleared = take_all_uris(&mut uris);
assert_eq!(cleared, vec![first_uri, second_uri]);
assert!(uris.is_empty());
Ok(())
}
#[test]
fn refresh_failure_clear_helper_clears_tracked_diagnostics() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let tracked_uri = test_uri("file:///workspace/src/stale.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
tracked_uri.clone(),
Vec::new(),
Vec::new(),
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
assert!(backend.latest_analysis_snapshot().is_some());
assert_eq!(backend.clear_all_diagnostic_uris(), vec![tracked_uri]);
assert!(backend.clear_all_diagnostic_uris().is_empty());
assert!(backend.latest_analysis_snapshot().is_none());
Ok(())
}
#[test]
fn refresh_generation_marks_older_requests_stale() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let Some(first) = backend.next_refresh_generation() else {
return Err("expected first refresh generation".to_string());
};
assert!(backend.is_current_refresh_generation(first));
let Some(second) = backend.next_refresh_generation() else {
return Err("expected second refresh generation".to_string());
};
assert!(!backend.is_current_refresh_generation(first));
assert!(backend.is_current_refresh_generation(second));
Ok(())
}
#[test]
fn refresh_diagnostics_advances_generation_before_analysis() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let Some(generation) = backend.next_refresh_generation() else {
return Err("expected refresh generation".to_string());
};
assert_eq!(generation, 1);
assert!(backend.is_current_refresh_generation(generation));
assert!(backend.latest_analysis_snapshot().is_none());
Ok(())
}
#[test]
fn document_store_tracks_open_change_and_close() -> Result<(), String> {
let uri = test_uri("file:///workspace/src/lib.rs")?;
let mut store = DocumentStore::default();
store.open(DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(
uri.clone(),
"rust".to_string(),
1,
"fn old() {}".to_string(),
),
});
let Some(opened) = store.documents.get(&uri) else {
return Err("expected opened document".to_string());
};
assert_eq!(opened.path, PathBuf::from("/workspace/src/lib.rs"));
assert_eq!(opened.version, Some(1));
assert_eq!(opened.text, "fn old() {}");
store.change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier::new(uri.clone(), 2),
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "fn new() {}".to_string(),
}],
});
let Some(changed) = store.documents.get(&uri) else {
return Err("expected changed document".to_string());
};
assert_eq!(changed.version, Some(2));
assert_eq!(changed.text, "fn new() {}");
store.close(DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier::new(uri.clone()),
});
assert!(!store.documents.contains_key(&uri));
Ok(())
}
#[test]
fn document_store_creates_document_from_full_change_when_missing() -> Result<(), String> {
let uri = test_uri("file:///workspace/src/lib.rs")?;
let mut store = DocumentStore::default();
store.change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier::new(uri.clone(), 7),
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "fn discovered() {}".to_string(),
}],
});
let Some(document) = store.documents.get(&uri) else {
return Err("expected document from full change".to_string());
};
assert_eq!(document.version, Some(7));
assert_eq!(document.text, "fn discovered() {}");
Ok(())
}
#[test]
fn initialize_root_prefers_first_workspace_folder() -> Result<(), String> {
let fallback = PathBuf::from("/fallback");
let params = initialize_params(
Some(vec![
WorkspaceFolder {
uri: test_uri("file:///workspace/main")?,
name: "main".to_string(),
},
WorkspaceFolder {
uri: test_uri("file:///workspace/other")?,
name: "other".to_string(),
},
]),
Some(test_uri("file:///workspace/root-uri")?),
);
let root = root_from_initialize_params(¶ms, &fallback);
assert_eq!(root, PathBuf::from("/workspace/main"));
Ok(())
}
#[test]
fn initialize_root_uses_root_uri_when_workspace_folders_are_missing() -> Result<(), String> {
let fallback = PathBuf::from("/fallback");
let params = initialize_params(None, Some(test_uri("file:///workspace/root-uri")?));
let root = root_from_initialize_params(¶ms, &fallback);
assert_eq!(root, PathBuf::from("/workspace/root-uri"));
Ok(())
}
#[test]
fn initialize_root_falls_back_to_process_cwd_when_no_lsp_root_exists() {
let fallback = PathBuf::from("/fallback");
let params = initialize_params(None, None);
let root = root_from_initialize_params(¶ms, &fallback);
assert_eq!(root, fallback);
}
#[test]
fn initialization_options_override_lsp_analysis_config() {
let mut params = initialize_params(None, None);
params.initialization_options = Some(serde_json::json!({
"baseRef": "origin/release",
"checkMode": "deep",
"includeUnchangedTests": false,
}));
let config =
LspAnalysisConfig::from_initialize_params(¶ms, crate::config::RiprConfig::default());
let input = config.check_input(Path::new("/workspace"));
assert_eq!(config.base_ref.as_deref(), Some("origin/release"));
assert_eq!(config.mode, Mode::Deep);
assert!(!config.include_unchanged_tests);
assert_eq!(input.root, PathBuf::from("/workspace"));
assert_eq!(input.base.as_deref(), Some("origin/release"));
assert_eq!(input.mode, Mode::Deep);
assert!(!input.include_unchanged_tests);
}
#[test]
fn initialization_options_allow_empty_base_ref_and_invalid_mode_falls_back() {
let mut params = initialize_params(None, None);
params.initialization_options = Some(serde_json::json!({
"baseRef": "",
"checkMode": "surprise",
}));
let config =
LspAnalysisConfig::from_initialize_params(¶ms, crate::config::RiprConfig::default());
assert_eq!(config.base_ref, None);
assert_eq!(config.mode, Mode::Draft);
assert!(config.include_unchanged_tests);
}
#[test]
fn initialization_options_accept_all_analysis_mode_labels() {
let cases = [
("instant", Mode::Instant),
("draft", Mode::Draft),
("fast", Mode::Fast),
("deep", Mode::Deep),
("ready", Mode::Ready),
];
for (label, expected) in cases {
let mut params = initialize_params(None, None);
params.initialization_options = Some(serde_json::json!({
"checkMode": label,
}));
let config = LspAnalysisConfig::from_initialize_params(
¶ms,
crate::config::RiprConfig::default(),
);
assert_eq!(config.mode, expected);
}
}
#[test]
fn default_lsp_analysis_config_matches_check_input_defaults() {
let config = LspAnalysisConfig::default();
let input = config.check_input(Path::new("/workspace"));
assert_eq!(input.root, PathBuf::from("/workspace"));
assert_eq!(input.base.as_deref(), Some("origin/main"));
assert_eq!(input.mode, Mode::Draft);
assert!(input.include_unchanged_tests);
assert!(config.enable_seam_diagnostics);
}
#[test]
fn initialize_stores_lsp_analysis_config() -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let mut params = initialize_params(None, None);
params.initialization_options = Some(serde_json::json!({
"baseRef": "upstream/main",
"checkMode": "fast",
}));
backend
.initialize(params)
.await
.map_err(|err| format!("initialize failed: {err}"))?;
let Some(config) = backend.analysis_config() else {
return Err("expected backend analysis config".to_string());
};
assert_eq!(config.base_ref.as_deref(), Some("upstream/main"));
assert_eq!(config.mode, Mode::Fast);
Ok(())
})
}
#[test]
fn backend_starts_with_default_lsp_analysis_config() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let Some(config) = backend.analysis_config() else {
return Err("expected backend analysis config".to_string());
};
assert_eq!(config.base_ref.as_deref(), Some("origin/main"));
assert_eq!(config.mode, Mode::Draft);
assert!(config.include_unchanged_tests);
assert!(config.enable_seam_diagnostics);
Ok(())
}
#[test]
fn workspace_diagnostic_batches_uses_default_lsp_analysis_config() {
let missing_root = Path::new("target/ripr/definitely-missing-lsp-root");
assert!(workspace_diagnostic_batches(missing_root).is_err());
}
#[test]
fn file_uri_to_path_decodes_spaces_and_windows_drive_prefix() -> Result<(), String> {
let uri = test_uri(&format!("file:///{}{}", "C%3A", "/path/to/ripr%20repo"))?;
let Some(path) = path_from_file_uri(&uri) else {
return Err("expected path from file URI".to_string());
};
assert_eq!(
path,
PathBuf::from(format!("{}{}", "C:", "/path/to/ripr repo"))
);
Ok(())
}
#[test]
fn file_uri_to_path_returns_none_for_non_file_scheme() -> Result<(), String> {
let uri = test_uri("https://example.com/workspace/src/lib.rs")?;
assert!(path_from_file_uri(&uri).is_none());
Ok(())
}
#[test]
fn file_uri_to_path_decodes_uppercase_hex_escape() -> Result<(), String> {
let uri = test_uri("file:///workspace/src%2Dlib.rs")?;
let Some(path) = path_from_file_uri(&uri) else {
return Err("expected path from file URI".to_string());
};
assert_eq!(path, PathBuf::from("/workspace/src-lib.rs"));
Ok(())
}
#[test]
fn file_uri_for_path_uses_valid_encoded_file_uri() -> Result<(), String> {
let uri = file_uri_for_path(&PathBuf::from("src lib.rs"))?;
assert_eq!(uri.as_str(), "file:///src%20lib.rs");
Ok(())
}
#[test]
fn uri_path_encoding_preserves_path_syntax_and_escapes_spaces() {
assert_eq!(
encode_uri_path("workspace/src lib.rs"),
"workspace/src%20lib.rs"
);
}
fn test_uri(uri: &str) -> Result<tower_lsp_server::ls_types::Uri, String> {
uri.parse::<tower_lsp_server::ls_types::Uri>()
.map_err(|err| format!("failed to parse test URI: {err}"))
}
async fn write_lsp_message<W>(writer: &mut W, message: serde_json::Value) -> Result<(), String>
where
W: AsyncWrite + Unpin,
{
let body = serde_json::to_vec(&message)
.map_err(|err| format!("failed to encode LSP message: {err}"))?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
writer
.write_all(header.as_bytes())
.await
.map_err(|err| format!("failed to write LSP header: {err}"))?;
writer
.write_all(&body)
.await
.map_err(|err| format!("failed to write LSP body: {err}"))?;
writer
.flush()
.await
.map_err(|err| format!("failed to flush LSP message: {err}"))
}
async fn read_lsp_response<R>(reader: &mut R, id: u64) -> Result<serde_json::Value, String>
where
R: AsyncRead + Unpin,
{
loop {
let message = read_lsp_message(reader).await?;
if message.get("id").and_then(serde_json::Value::as_u64) == Some(id) {
return Ok(message);
}
}
}
async fn read_lsp_response_with_notifications<R>(
reader: &mut R,
id: u64,
) -> Result<(serde_json::Value, Vec<serde_json::Value>), String>
where
R: AsyncRead + Unpin,
{
let mut notifications = Vec::new();
loop {
let message = read_lsp_message(reader).await?;
if message.get("id").and_then(serde_json::Value::as_u64) == Some(id) {
return Ok((message, notifications));
}
notifications.push(message);
}
}
fn log_notification_messages(messages: &[serde_json::Value]) -> Vec<String> {
messages
.iter()
.filter(|message| {
message.get("method").and_then(serde_json::Value::as_str) == Some("window/logMessage")
})
.filter_map(|message| {
message
.get("params")
.and_then(|params| params.get("message"))
.and_then(serde_json::Value::as_str)
.map(str::to_string)
})
.collect()
}
async fn read_lsp_message<R>(reader: &mut R) -> Result<serde_json::Value, String>
where
R: AsyncRead + Unpin,
{
let mut header = Vec::new();
loop {
let mut byte = [0_u8; 1];
reader
.read_exact(&mut byte)
.await
.map_err(|err| format!("failed to read LSP header: {err}"))?;
header.push(byte[0]);
if header.ends_with(b"\r\n\r\n") {
break;
}
}
let header =
std::str::from_utf8(&header).map_err(|err| format!("invalid LSP header UTF-8: {err}"))?;
let content_length = header
.lines()
.find_map(|line| line.strip_prefix("Content-Length: "))
.ok_or_else(|| "missing LSP Content-Length header".to_string())?
.parse::<usize>()
.map_err(|err| format!("invalid LSP Content-Length header: {err}"))?;
let mut body = vec![0_u8; content_length];
reader
.read_exact(&mut body)
.await
.map_err(|err| format!("failed to read LSP body: {err}"))?;
serde_json::from_slice(&body).map_err(|err| format!("failed to decode LSP message: {err}"))
}
fn sample_analysis_snapshot(
root: PathBuf,
uri: tower_lsp_server::ls_types::Uri,
diagnostics: Vec<tower_lsp_server::ls_types::Diagnostic>,
findings: Vec<Finding>,
) -> AnalysisSnapshot {
let mut diagnostics_by_uri = BTreeMap::new();
diagnostics_by_uri.insert(uri, diagnostics);
AnalysisSnapshot {
root,
base: Some("origin/main".to_string()),
mode: Mode::Draft,
refresh: RefreshMetadata::generated_now(),
findings,
classified_seams: Vec::new(),
diagnostics_by_uri,
}
}
fn sample_workspace_diagnostics(
root: PathBuf,
uri: tower_lsp_server::ls_types::Uri,
diagnostics: Vec<tower_lsp_server::ls_types::Diagnostic>,
findings: Vec<Finding>,
) -> WorkspaceDiagnostics {
let snapshot = sample_analysis_snapshot(root, uri.clone(), diagnostics.clone(), findings);
WorkspaceDiagnostics {
snapshot,
batches: vec![DiagnosticBatch { uri, diagnostics }],
}
}
fn code_action_params(
diagnostics: Vec<tower_lsp_server::ls_types::Diagnostic>,
) -> Result<CodeActionParams, String> {
Ok(CodeActionParams {
text_document: TextDocumentIdentifier::new(test_uri("file:///workspace/src/pricing.rs")?),
range: Range {
start: Position {
line: 87,
character: 0,
},
end: Position {
line: 87,
character: 120,
},
},
context: CodeActionContext {
diagnostics,
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
}
fn code_action_commands(
actions: &[CodeActionOrCommand],
) -> Result<Vec<(String, String, Vec<serde_json::Value>)>, String> {
let mut commands = Vec::new();
for action in actions {
let CodeActionOrCommand::CodeAction(action) = action else {
return Err("expected code action".to_string());
};
let Some(command) = &action.command else {
return Err(format!("expected command for action {}", action.title));
};
commands.push((
action.title.clone(),
command.command.clone(),
command.arguments.clone().unwrap_or_default(),
));
}
Ok(commands)
}
fn boundary_gap_lsp_fixture_outputs() -> Result<(serde_json::Value, serde_json::Value), String> {
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let fixture_root = repo_root.join("fixtures/boundary_gap/input");
let mut seams = crate::analysis::inventory_classified_seams_at_with_config(
&fixture_root,
&crate::config::RiprConfig::default(),
)?;
seams.sort_by(|left, right| left.seam.id().as_str().cmp(right.seam.id().as_str()));
if seams.len() != 1 {
return Err(format!(
"expected one boundary_gap classified seam, got {}",
seams.len()
));
}
let seam = seams
.into_iter()
.next()
.ok_or_else(|| "expected classified seam".to_string())?;
let diagnostic = diagnostic_for_classified_seam(&fixture_root, &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = file_uri_for_path(&fixture_root.join(seam.seam.file()))?;
let mut snapshot = sample_analysis_snapshot(
fixture_root.clone(),
uri.clone(),
vec![diagnostic.clone()],
Vec::new(),
);
snapshot.mode = Mode::Fast;
snapshot.classified_seams = vec![seam.clone()];
let actions = code_action_response(
&code_action_params_for(
uri.clone(),
diagnostic.range.start.line,
vec![diagnostic.clone()],
)?,
Some(&snapshot),
);
Ok((
serde_json::json!({
"fixture": "boundary_gap",
"diagnostics": [project_diagnostic(&fixture_root, &uri, &diagnostic)?],
}),
serde_json::json!({
"fixture": "boundary_gap",
"actions": project_code_actions(&fixture_root, &actions)?,
}),
))
}
fn assert_json_fixture(name: &str, actual: serde_json::Value) -> Result<(), String> {
let path = Path::new("fixtures")
.join("boundary_gap")
.join("expected")
.join(name);
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join(path);
let text = std::fs::read_to_string(&path).map_err(|err| {
format!(
"failed to read {}: {err}\nactual:\n{}",
path.display(),
pretty_json(&actual)
)
})?;
let expected: serde_json::Value = serde_json::from_str(&text)
.map_err(|err| format!("failed to parse {}: {err}", path.display()))?;
if expected != actual {
return Err(format!(
"{} drifted\nexpected:\n{}\nactual:\n{}",
path.display(),
pretty_json(&expected),
pretty_json(&actual)
));
}
Ok(())
}
fn project_diagnostic(
root: &Path,
uri: &tower_lsp_server::ls_types::Uri,
diagnostic: &tower_lsp_server::ls_types::Diagnostic,
) -> Result<serde_json::Value, String> {
Ok(serde_json::json!({
"uri": relative_uri_path(root, uri)?,
"range": {
"start": {
"line": diagnostic.range.start.line,
"character": diagnostic.range.start.character,
},
"end": {
"line": diagnostic.range.end.line,
"character": diagnostic.range.end.character,
},
},
"severity": diagnostic.severity.map(diagnostic_severity_label),
"code": diagnostic.code.as_ref().map(diagnostic_code_value),
"source": diagnostic.source.clone(),
"message": diagnostic.message,
"data": diagnostic.data.clone(),
}))
}
fn project_code_actions(
root: &Path,
actions: &[CodeActionOrCommand],
) -> Result<Vec<serde_json::Value>, String> {
let commands = code_action_commands(actions)?;
commands
.into_iter()
.map(|(title, command, arguments)| {
let arguments = arguments
.iter()
.map(|argument| normalize_lsp_action_argument(root, argument))
.collect::<Result<Vec<_>, _>>()?;
Ok(serde_json::json!({
"title": title,
"command": command,
"arguments": arguments,
}))
})
.collect()
}
fn normalize_lsp_action_argument(
root: &Path,
argument: &serde_json::Value,
) -> Result<serde_json::Value, String> {
let Some(object) = argument.as_object() else {
return Ok(argument.clone());
};
let mut normalized = serde_json::Map::new();
for (key, value) in object {
if key == "uri"
&& let Some(uri) = value.as_str()
&& uri.starts_with("file://")
{
let parsed = uri
.parse()
.map_err(|err| format!("failed to parse action uri {uri}: {err}"))?;
normalized.insert(
key.clone(),
serde_json::json!(relative_uri_path(root, &parsed)?),
);
} else {
normalized.insert(key.clone(), value.clone());
}
}
Ok(serde_json::Value::Object(normalized))
}
fn code_action_params_for(
uri: tower_lsp_server::ls_types::Uri,
line: u32,
diagnostics: Vec<tower_lsp_server::ls_types::Diagnostic>,
) -> Result<CodeActionParams, String> {
Ok(CodeActionParams {
text_document: TextDocumentIdentifier::new(uri),
range: Range {
start: Position { line, character: 0 },
end: Position {
line,
character: 120,
},
},
context: CodeActionContext {
diagnostics,
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
}
fn diagnostic_severity_label(severity: DiagnosticSeverity) -> &'static str {
match severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
DiagnosticSeverity::INFORMATION => "information",
DiagnosticSeverity::HINT => "hint",
_ => "unknown",
}
}
fn diagnostic_code_value(code: &NumberOrString) -> String {
match code {
NumberOrString::Number(value) => value.to_string(),
NumberOrString::String(value) => value.clone(),
}
}
fn relative_uri_path(root: &Path, uri: &tower_lsp_server::ls_types::Uri) -> Result<String, String> {
let path =
path_from_file_uri(uri).ok_or_else(|| format!("expected file uri: {}", uri.as_str()))?;
relative_path(root, &path)
}
fn relative_path(root: &Path, path: &Path) -> Result<String, String> {
let root = normalize_fixture_path(root);
let path = normalize_fixture_path(path);
if path == root {
return Ok(".".to_string());
}
let prefix = format!("{root}/");
path.strip_prefix(&prefix)
.map(str::to_string)
.ok_or_else(|| format!("path {path} is not under fixture root {root}"))
}
fn normalize_fixture_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn pretty_json(value: &serde_json::Value) -> String {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
fn hover_params(uri: tower_lsp_server::ls_types::Uri, line: u32, character: u32) -> HoverParams {
HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier::new(uri),
position: Position { line, character },
},
work_done_progress_params: Default::default(),
}
}
#[expect(
deprecated,
reason = "Test helper constructs InitializeParams including the deprecated root_path/root_uri fields to exercise fallback handling in capabilities.rs."
)]
fn initialize_params(
workspace_folders: Option<Vec<WorkspaceFolder>>,
root_uri: Option<tower_lsp_server::ls_types::Uri>,
) -> InitializeParams {
InitializeParams {
workspace_folders,
root_uri,
..InitializeParams::default()
}
}
fn sample_finding() -> Finding {
Finding {
id: "probe:pricing:88:predicate".to_string(),
probe: Probe {
id: ProbeId("probe:pricing:88:predicate".to_string()),
location: SourceLocation {
file: PathBuf::from("src/pricing.rs"),
line: 88,
column: 1,
},
owner: None,
family: ProbeFamily::Predicate,
delta: DeltaKind::Control,
before: None,
after: None,
expression: "amount >= threshold".to_string(),
expected_sinks: Vec::new(),
required_oracles: Vec::new(),
},
class: ExposureClass::WeaklyExposed,
ripr: RiprEvidence {
reach: StageEvidence::new(StageState::Yes, Confidence::High, "related tests found"),
infect: StageEvidence::new(
StageState::Yes,
Confidence::High,
"predicate can alter branch behavior",
),
propagate: StageEvidence::new(
StageState::Yes,
Confidence::Medium,
"branch influences return value",
),
reveal: RevealEvidence {
observe: StageEvidence::new(
StageState::Weak,
Confidence::Medium,
"return value asserted",
),
discriminate: StageEvidence::new(
StageState::Weak,
Confidence::Medium,
"boundary value missing",
),
},
},
confidence: 0.75,
evidence: Vec::new(),
missing: Vec::new(),
flow_sinks: Vec::new(),
activation: crate::domain::ActivationEvidence::default(),
stop_reasons: Vec::new(),
related_tests: Vec::new(),
recommended_next_step: Some("Add an exact boundary assertion.".to_string()),
}
}
fn sample_classified_seam() -> crate::analysis::ClassifiedSeam {
use crate::analysis::seams::{
ExpectedSink, RepoSeam, RequiredDiscriminator, SeamGripClass, SeamKind,
};
use crate::analysis::test_grip_evidence::{
RelatedTestGrip, RelationConfidence, RelationReason, TestGripEvidence,
};
use crate::domain::{MissingDiscriminatorFact, ValueContext, ValueFact};
let seam = RepoSeam::new(
"src/pricing.rs",
"pricing::discounted_total",
SeamKind::PredicateBoundary,
42,
88,
"amount >= discount_threshold",
RequiredDiscriminator::BoundaryValue {
description: "amount >= discount_threshold".to_string(),
},
ExpectedSink::ReturnValue,
);
let seam_id = seam.id().clone();
crate::analysis::ClassifiedSeam {
seam,
evidence: TestGripEvidence {
seam_id,
related_tests: vec![RelatedTestGrip {
test_name: "below_threshold_has_no_discount".to_string(),
file: PathBuf::from("tests/pricing.rs"),
line: 12,
oracle_kind: OracleKind::ExactValue,
oracle_strength: OracleStrength::Strong,
evidence_summary: "exact value assertion".to_string(),
relation_reason: RelationReason::DirectOwnerCall,
relation_confidence: RelationConfidence::High,
}],
reach: StageEvidence::new(
StageState::Yes,
Confidence::High,
"related test calls owner",
),
activate: StageEvidence::new(StageState::Yes, Confidence::High, "test reaches branch"),
propagate: StageEvidence::new(StageState::Yes, Confidence::Medium, "return value sink"),
observe: StageEvidence::new(StageState::Yes, Confidence::Medium, "exact assertion"),
discriminate: StageEvidence::new(
StageState::Weak,
Confidence::Medium,
"boundary value missing",
),
observed_values: vec![ValueFact {
line: 12,
text: "discounted_total(50, 100)".to_string(),
value: "50".to_string(),
context: ValueContext::FunctionArgument,
}],
missing_discriminators: vec![MissingDiscriminatorFact {
value: "discount_threshold (equality boundary)".to_string(),
reason: "observed values skip equality boundary".to_string(),
flow_sink: None,
}],
},
class: SeamGripClass::WeaklyGripped,
}
}
fn sample_side_effect_seam_without_related_tests() -> crate::analysis::ClassifiedSeam {
use crate::analysis::seams::{
ExpectedSink, RepoSeam, RequiredDiscriminator, SeamGripClass, SeamKind,
};
use crate::analysis::test_grip_evidence::TestGripEvidence;
let seam = RepoSeam::new(
"src/service.rs",
"service::publish_event",
SeamKind::SideEffect,
7,
14,
"event_bus.publish(event)",
RequiredDiscriminator::Effect {
sink: "event bus publish".to_string(),
},
ExpectedSink::SideEffect,
);
let seam_id = seam.id().clone();
crate::analysis::ClassifiedSeam {
seam,
evidence: TestGripEvidence {
seam_id,
related_tests: Vec::new(),
reach: StageEvidence::new(StageState::No, Confidence::Low, "no related test"),
activate: StageEvidence::new(StageState::No, Confidence::Low, "no activation value"),
propagate: StageEvidence::new(StageState::Unknown, Confidence::Low, "unknown sink"),
observe: StageEvidence::new(StageState::No, Confidence::Low, "no observer"),
discriminate: StageEvidence::new(StageState::No, Confidence::Low, "no discriminator"),
observed_values: Vec::new(),
missing_discriminators: Vec::new(),
},
class: SeamGripClass::Ungripped,
}
}
#[test]
fn finding_hover_response_includes_ripr_evidence_path() -> Result<(), String> {
use super::hover::finding_hover_response;
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let hover = finding_hover_response(&finding, &diagnostic);
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("**ripr** `weakly_exposed`"));
assert!(markup.value.contains("predicate"));
assert!(markup.value.contains("reach yes:"));
assert!(markup.value.contains("infection yes:"));
assert!(markup.value.contains("propagation yes:"));
assert!(markup.value.contains("observation weak:"));
assert!(markup.value.contains("discriminator weak:"));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn finding_hover_response_includes_evidence_details() -> Result<(), String> {
use super::hover::finding_hover_response;
use crate::domain::{
ActivationEvidence, FlowSinkFact, FlowSinkKind, MissingDiscriminatorFact, RelatedTest,
ValueContext, ValueFact,
};
let mut finding = sample_finding();
finding.flow_sinks = vec![FlowSinkFact {
kind: FlowSinkKind::ReturnValue,
text: "total".to_string(),
line: 88,
owner: None,
}];
finding.related_tests = vec![RelatedTest {
name: "discount_boundary_is_exact".to_string(),
file: PathBuf::from("tests/pricing.rs"),
line: 12,
oracle: Some("assert_eq!(total, expected)".to_string()),
oracle_kind: OracleKind::ExactValue,
oracle_strength: OracleStrength::Strong,
}];
finding.activation = ActivationEvidence {
observed_values: vec![ValueFact {
line: 12,
text: "assert_eq!".to_string(),
value: "amount == threshold".to_string(),
context: ValueContext::FunctionArgument,
}],
missing_discriminators: vec![MissingDiscriminatorFact {
value: "amount == threshold".to_string(),
reason: "related tests do not cover the changed boundary value".to_string(),
flow_sink: None,
}],
};
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let hover = finding_hover_response(&finding, &diagnostic);
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("## RIPR Evidence"));
assert!(markup.value.contains("* reach yes: related tests found"));
assert!(
markup
.value
.contains("* infection yes: predicate can alter branch behavior")
);
assert!(
markup
.value
.contains("* propagation yes: branch influences return value")
);
assert!(
markup
.value
.contains("* observation weak: return value asserted")
);
assert!(
markup
.value
.contains("* discriminator weak: boundary value missing")
);
assert!(markup.value.contains("## Related Tests"));
assert!(markup.value.contains("tests/pricing.rs:12"));
assert!(markup.value.contains("discount_boundary_is_exact"));
assert!(
markup
.value
.contains("strong exact_value oracle: assert_eq!(total, expected)")
);
assert!(markup.value.contains("Add an exact boundary assertion."));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn hover_for_position_uses_snapshot_finding_hover() -> Result<(), String> {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let Some(hover) = backend.hover_for_position(&hover_params(uri, 87, 1)) else {
return Err("expected finding hover".to_string());
};
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("**ripr** `weakly_exposed`"));
assert!(markup.value.contains("predicate"));
assert!(markup.value.contains("## RIPR Evidence"));
assert!(markup.value.contains("reach yes:"));
assert!(markup.value.contains("Add an exact boundary assertion."));
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn finding_hover_avoids_mutation_runtime_language() -> Result<(), String> {
use super::hover::finding_hover_response;
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let hover = finding_hover_response(&finding, &diagnostic);
match hover.contents {
HoverContents::Markup(markup) => {
let lower = markup.value.to_lowercase();
let forbidden_terms = vec!["kil", "surv", "prov", "adeq", "untest"];
for term in forbidden_terms {
assert!(
!lower.contains(term),
"hover must use conservative static language"
);
}
Ok(())
}
_ => Err("expected markup hover".to_string()),
}
}
#[test]
fn execute_command_collect_context_returns_packet_for_known_finding() -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let expected_finding = finding.clone();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let params = ExecuteCommandParams {
command: COLLECT_CONTEXT_COMMAND.to_string(),
arguments: vec![serde_json::json!({
"finding_id": "probe:pricing:88:predicate",
"probe_id": "probe:pricing:88:predicate",
"uri": "file:///workspace/src/pricing.rs",
"line": 88,
})],
work_done_progress_params: Default::default(),
};
let result = backend.execute_command(params).await;
let packet = result.map_err(|err| format!("execute_command failed: {err}"))?;
let Some(packet) = packet else {
return Err("expected context packet".to_string());
};
let expected_stop_reasons = expected_finding
.effective_stop_reasons()
.iter()
.map(|reason| reason.as_str().to_string())
.collect();
let expected_context_packet = crate::domain::context_packet::ContextPacket::from_finding(
&expected_finding,
crate::config::DEFAULT_CONTEXT_RELATED_TESTS,
expected_stop_reasons,
);
let expected_json =
crate::output::json::render_context_packet_dto(&expected_context_packet);
let expected_packet: serde_json::Value = serde_json::from_str(&expected_json)
.map_err(|err| format!("failed to parse expected packet: {err}"))?;
assert_eq!(packet, expected_packet);
let packet_str = serde_json::to_string(&packet)
.map_err(|err| format!("failed to serialize packet: {err}"))?;
assert!(packet_str.contains("\"version\""));
assert!(packet_str.contains("\"tool\""));
assert!(packet_str.contains("probe:pricing:88:predicate"));
Ok(())
})
}
#[test]
fn execute_command_collect_context_returns_agent_seam_packet_for_known_seam() -> Result<(), String>
{
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let seam = sample_classified_seam();
let seam_id = seam.seam.id().as_str().to_string();
let diagnostic = diagnostic_for_classified_seam(Path::new("/workspace"), &seam)
.ok_or_else(|| "expected seam diagnostic".to_string())?;
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let mut diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri,
vec![diagnostic],
Vec::new(),
);
diagnostics.snapshot.classified_seams = vec![seam];
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let params = ExecuteCommandParams {
command: COLLECT_CONTEXT_COMMAND.to_string(),
arguments: vec![serde_json::json!({
"seam_id": seam_id,
"uri": "file:///workspace/src/pricing.rs",
"line": 88,
})],
work_done_progress_params: Default::default(),
};
let result = backend.execute_command(params).await;
let packet = result.map_err(|err| format!("execute_command failed: {err}"))?;
let Some(packet) = packet else {
return Err("expected seam packet".to_string());
};
assert_eq!(packet["schema_version"], "0.3");
assert_eq!(packet["packets_total"], 1);
assert_eq!(packet["packets"][0]["seam_id"], seam_id);
assert_eq!(
packet["packets"][0]["assertion_shape"]["kind"],
"exact_return_value"
);
Ok(())
})
}
#[test]
fn execute_command_collect_context_returns_none_for_unknown_finding() -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| format!("failed to start test runtime: {err}"))?;
runtime.block_on(async {
let (service, _socket) = LspService::new(|client| Backend::new(client, PathBuf::from(".")));
let backend = service.inner();
let finding = sample_finding();
let diagnostic = diagnostic_for_finding(Path::new("/workspace"), &finding);
let uri = test_uri("file:///workspace/src/pricing.rs")?;
let diagnostics = sample_workspace_diagnostics(
PathBuf::from("/workspace"),
uri.clone(),
vec![diagnostic.clone()],
vec![finding],
);
let Some(_) = backend.refresh_plan(diagnostics) else {
return Err("expected refresh plan".to_string());
};
let params = ExecuteCommandParams {
command: COLLECT_CONTEXT_COMMAND.to_string(),
arguments: vec![serde_json::json!({
"finding_id": "probe:unknown:1:predicate",
})],
work_done_progress_params: Default::default(),
};
let result = backend.execute_command(params).await;
let packet = result.map_err(|err| format!("execute_command failed: {err}"))?;
assert!(packet.is_none(), "expected None for unknown finding");
Ok(())
})
}
#[test]
fn execute_command_refresh_remains_unchanged() -> Result<(), String> {
let Some(provider) = initialize_result().capabilities.execute_command_provider else {
return Err("expected execute command provider".to_string());
};
assert!(
provider
.commands
.iter()
.any(|command| command == REFRESH_COMMAND)
);
Ok(())
}