Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/api/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ pub struct RelatedAdr {
pub scope: String,
pub relevance_reason: String,
pub confidence: f64,
/// Fully-formed, environment-aware deep link back to this ADR in the
/// Decisions UI. The server assembles it, so it is used verbatim and never
/// reconstructed here. Nullable on the wire (an interpreter-layer result
/// can carry `null` before the link is populated) and absent from older
/// servers, so both cases deserialize to `None`.
#[serde(default)]
pub url: Option<String>,
}

/// Outcome of a single poll of `GET /v1/advisor/query/:id`.
Expand Down Expand Up @@ -1019,7 +1026,8 @@ mod tests {
"related_adrs": [{
"id": "a1", "name": "n1", "title": "t1", "policy": "p1",
"instructions": "i1", "scope": "s1", "relevance_reason": "r1",
"confidence": 0.75
"confidence": 0.75,
"url": "https://app.example.com/decisions/r1?tab=active&decision=abc1234"
}]
}
},
Expand All @@ -1031,5 +1039,33 @@ mod tests {
assert_eq!(out.summary, "top-level summary");
assert_eq!(out.interpreter.related_adrs.len(), 1);
assert_eq!(out.interpreter.related_adrs[0].confidence, 0.75);
// The server's deep link is captured verbatim.
assert_eq!(
out.interpreter.related_adrs[0].url.as_deref(),
Some("https://app.example.com/decisions/r1?tab=active&decision=abc1234")
);
}

#[test]
fn test_related_adr_url_absent_or_null_is_none() {
// `url` absent (older server) and `url: null` (interpreter-layer result
// before the link is populated) both deserialize to None.
let json = r#"{
"summary": "s",
"interpreter": {
"summary": "i",
"related_adrs": [
{ "id": "a1", "name": "n1", "title": "t1", "policy": "p1",
"instructions": "i1", "scope": "s1", "relevance_reason": "r1",
"confidence": 0.5 },
{ "id": "a2", "name": "n2", "title": "t2", "policy": "p2",
"instructions": "i2", "scope": "s2", "relevance_reason": "r2",
"confidence": 0.6, "url": null }
]
}
}"#;
let out: AdvisorOutput = serde_json::from_str(json).unwrap();
assert_eq!(out.interpreter.related_adrs[0].url, None);
assert_eq!(out.interpreter.related_adrs[1].url, None);
}
}
35 changes: 25 additions & 10 deletions src/cli/commands/advisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ fn print_answer(output: &AdvisorOutput) {
adr.scope,
adr.confidence * 100.0
);
// Render the server-provided deep link (used verbatim) on its own
// line; skip a null or empty url so the ADR still prints cleanly.
if let Some(url) = adr.url.as_deref().filter(|u| !u.is_empty()) {
println!(" {url}");
}
}
}
}
Expand Down Expand Up @@ -309,20 +314,30 @@ mod tests {

#[test]
fn test_print_answer_with_and_without_adrs() {
let adr = |url: Option<&str>| crate::api::types::RelatedAdr {
id: "a".to_string(),
name: "n".to_string(),
title: "T".to_string(),
policy: "p".to_string(),
instructions: "i".to_string(),
scope: "s".to_string(),
relevance_reason: "r".to_string(),
confidence: 0.5,
url: url.map(|u| u.to_string()),
};
// Cover all three url arms: a populated link renders, while a null or
// an empty link is skipped without breaking the ADR line.
let with = AdvisorOutput {
summary: "S".to_string(),
interpreter: crate::api::types::AdvisorInterpreter {
summary: "i".to_string(),
related_adrs: vec![crate::api::types::RelatedAdr {
id: "a".to_string(),
name: "n".to_string(),
title: "T".to_string(),
policy: "p".to_string(),
instructions: "i".to_string(),
scope: "s".to_string(),
relevance_reason: "r".to_string(),
confidence: 0.5,
}],
related_adrs: vec![
adr(Some(
"https://app.example.com/decisions/r1?tab=active&decision=abc1234",
)),
adr(None),
adr(Some("")),
],
},
};
print_answer(&with);
Expand Down
Loading