use std::collections::HashMap;
use std::sync::Arc;
use greentic_runner_host::RunnerHost;
use greentic_runner_host::identify_hint::IdentifyInstanceHint;
use greentic_runner_host::pack::IdentifyOutcome;
use serde_json::Value;
use crate::endpoint_admit::EndpointAdmit;
use crate::http_routes::RevisionScope;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ResolverOutcome {
HeaderWins(String),
Hit(String),
Miss,
Ambiguous,
NoImpl,
Skipped,
PublicSkipped,
}
impl ResolverOutcome {
pub(crate) fn origin(&self) -> &'static str {
match self {
ResolverOutcome::HeaderWins(_) => "header-wins",
ResolverOutcome::Hit(_) => "hit",
ResolverOutcome::Miss => "miss",
ResolverOutcome::Ambiguous => "ambiguous",
ResolverOutcome::NoImpl => "no-impl",
ResolverOutcome::Skipped => "skipped",
ResolverOutcome::PublicSkipped => "public-skipped",
}
}
pub(crate) fn endpoint_id(&self) -> Option<&str> {
match self {
ResolverOutcome::HeaderWins(eid) | ResolverOutcome::Hit(eid) => Some(eid.as_str()),
_ => None,
}
}
}
fn should_probe(
peer_is_loopback: bool,
header_eid: Option<&str>,
provider_types_count: usize,
) -> Option<ResolverOutcome> {
if let Some(eid) = header_eid {
return Some(ResolverOutcome::HeaderWins(eid.to_string()));
}
if !peer_is_loopback {
return Some(ResolverOutcome::PublicSkipped);
}
if provider_types_count == 0 {
return Some(ResolverOutcome::Skipped);
}
None
}
fn scope_headers_to_hinted<'a>(
headers: Vec<(String, String)>,
hints: &'a HashMap<String, Option<IdentifyInstanceHint>>,
) -> (Vec<(String, String)>, Vec<&'a str>) {
let unhinted: Vec<&'a str> = hints
.iter()
.filter_map(|(provider_type, hint)| hint.is_none().then_some(provider_type.as_str()))
.collect();
if unhinted.is_empty() {
(headers, unhinted)
} else {
(Vec::new(), unhinted)
}
}
pub(crate) async fn resolve<F>(
host: &Arc<RunnerHost>,
tenant: &str,
scope: &RevisionScope,
admit: &EndpointAdmit,
header_eid: Option<&str>,
peer_is_loopback: bool,
build_headers_body: F,
) -> anyhow::Result<ResolverOutcome>
where
F: FnOnce() -> (Vec<(String, String)>, Value),
{
let provider_types: Vec<&str> = admit.provider_types().collect();
if let Some(outcome) = should_probe(peer_is_loopback, header_eid, provider_types.len()) {
return Ok(outcome);
}
let hints = host
.describe_identify_instances_for_revision(
tenant,
scope.deployment_id,
scope.bundle_id.clone(),
scope.revision_id,
&provider_types,
)
.await?;
let (headers, body) = build_headers_body();
let (scoped_headers, unhinted) = scope_headers_to_hinted(headers, &hints);
if !unhinted.is_empty() {
tracing::warn!(
target: "greentic_start::endpoint_resolver",
unhinted_providers = ?unhinted,
"headers stripped for identify-instance probe: provider(s) lack a \
describe-identify-instance hint — add a hint to receive secret-token headers"
);
}
let outcomes = host
.identify_messaging_endpoints_for_revision_scoped(
tenant,
scope.deployment_id,
scope.bundle_id.clone(),
scope.revision_id,
&provider_types,
&scoped_headers,
&body,
)
.await?;
Ok(fold_outcomes(admit, &outcomes))
}
fn fold_outcomes(
admit: &EndpointAdmit,
outcomes: &std::collections::HashMap<String, IdentifyOutcome>,
) -> ResolverOutcome {
let mut hit: Option<String> = None;
let mut poison = false;
let mut any_non_unsupported = false;
for (provider_type, outcome) in outcomes {
match outcome {
IdentifyOutcome::Identified(provider_id) => {
any_non_unsupported = true;
match admit.endpoint_id_for_provider(provider_type, provider_id) {
Some(eid) => {
if hit.is_some() {
poison = true;
}
hit = Some(eid.to_string());
}
None => {
poison = true;
}
}
}
IdentifyOutcome::NoMatch => {
any_non_unsupported = true;
if admit.endpoint_count_for_provider_type(provider_type) >= 2 {
poison = true;
}
}
IdentifyOutcome::Unsupported => {
}
}
}
if poison {
return ResolverOutcome::Ambiguous;
}
if let Some(eid) = hit {
return ResolverOutcome::Hit(eid);
}
if any_non_unsupported {
return ResolverOutcome::Miss;
}
ResolverOutcome::NoImpl
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_fixtures::{endpoint_typed, env_with};
use greentic_deploy_spec::MessagingEndpoint;
use std::collections::HashMap;
fn admit_from(endpoints: Vec<MessagingEndpoint>) -> EndpointAdmit {
EndpointAdmit::from_environment(&env_with(endpoints))
}
#[test]
fn origin_strings_are_stable() {
assert_eq!(
ResolverOutcome::HeaderWins("x".into()).origin(),
"header-wins"
);
assert_eq!(ResolverOutcome::Hit("x".into()).origin(), "hit");
assert_eq!(ResolverOutcome::Miss.origin(), "miss");
assert_eq!(ResolverOutcome::Ambiguous.origin(), "ambiguous");
assert_eq!(ResolverOutcome::NoImpl.origin(), "no-impl");
assert_eq!(ResolverOutcome::Skipped.origin(), "skipped");
assert_eq!(ResolverOutcome::PublicSkipped.origin(), "public-skipped");
}
#[test]
fn endpoint_id_threaded_only_when_resolution_succeeded() {
assert_eq!(
ResolverOutcome::HeaderWins("teams-legal".into()).endpoint_id(),
Some("teams-legal")
);
assert_eq!(
ResolverOutcome::Hit("teams-legal".into()).endpoint_id(),
Some("teams-legal")
);
assert!(ResolverOutcome::Miss.endpoint_id().is_none());
assert!(ResolverOutcome::Ambiguous.endpoint_id().is_none());
assert!(ResolverOutcome::NoImpl.endpoint_id().is_none());
assert!(ResolverOutcome::Skipped.endpoint_id().is_none());
assert!(ResolverOutcome::PublicSkipped.endpoint_id().is_none());
}
#[test]
fn fold_single_identified_yields_hit_with_eid() {
let teams = endpoint_typed("teams", "28:legal-bot", &["legal-bundle"]);
let eid = teams.endpoint_id.to_string();
let admit = admit_from(vec![teams]);
let outcomes = HashMap::from([(
"teams".to_string(),
IdentifyOutcome::Identified("28:legal-bot".to_string()),
)]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Hit(eid));
}
#[test]
fn fold_identified_unknown_provider_id_is_ambiguous() {
let admit = admit_from(vec![endpoint_typed("teams", "28:known", &["b"])]);
let outcomes = HashMap::from([(
"teams".to_string(),
IdentifyOutcome::Identified("28:unknown-drift".to_string()),
)]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Ambiguous);
}
#[test]
fn fold_no_match_with_single_endpoint_of_type_is_miss() {
let admit = admit_from(vec![endpoint_typed("teams", "28:legal", &["b"])]);
let outcomes = HashMap::from([("teams".to_string(), IdentifyOutcome::NoMatch)]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Miss);
}
#[test]
fn fold_no_match_with_two_endpoints_of_type_is_ambiguous() {
let admit = admit_from(vec![
endpoint_typed("teams", "28:legal", &["b1"]),
endpoint_typed("teams", "28:acct", &["b2"]),
]);
let outcomes = HashMap::from([("teams".to_string(), IdentifyOutcome::NoMatch)]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Ambiguous);
}
#[test]
fn fold_multiple_distinct_identified_is_ambiguous() {
let teams_a = endpoint_typed("teams", "28:legal", &["b1"]);
let slack_b = endpoint_typed("slack", "T0LEGAL", &["b1"]);
let admit = admit_from(vec![teams_a, slack_b]);
let outcomes = HashMap::from([
(
"teams".to_string(),
IdentifyOutcome::Identified("28:legal".to_string()),
),
(
"slack".to_string(),
IdentifyOutcome::Identified("T0LEGAL".to_string()),
),
]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Ambiguous);
}
#[test]
fn fold_all_unsupported_is_no_impl() {
let admit = admit_from(vec![endpoint_typed("teams", "28:legal", &["b1"])]);
let outcomes = HashMap::from([("teams".to_string(), IdentifyOutcome::Unsupported)]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::NoImpl);
}
#[test]
fn fold_mixed_identified_plus_unsupported_is_hit() {
let teams = endpoint_typed("teams", "28:legal", &["b1"]);
let slack = endpoint_typed("slack", "T0LEGAL", &["b1"]);
let teams_eid = teams.endpoint_id.to_string();
let admit = admit_from(vec![teams, slack]);
let outcomes = HashMap::from([
(
"teams".to_string(),
IdentifyOutcome::Identified("28:legal".to_string()),
),
("slack".to_string(), IdentifyOutcome::Unsupported),
]);
assert_eq!(
fold_outcomes(&admit, &outcomes),
ResolverOutcome::Hit(teams_eid)
);
}
#[test]
fn fold_mixed_no_match_plus_unsupported_is_miss_when_singletons() {
let admit = admit_from(vec![
endpoint_typed("teams", "28:legal", &["b1"]),
endpoint_typed("slack", "T0LEGAL", &["b1"]),
]);
let outcomes = HashMap::from([
("teams".to_string(), IdentifyOutcome::NoMatch),
("slack".to_string(), IdentifyOutcome::Unsupported),
]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Miss);
}
#[test]
fn fold_known_hit_plus_unknown_identified_is_ambiguous() {
let teams = endpoint_typed("teams", "28:legal", &["b1"]);
let slack = endpoint_typed("slack", "T0LEGAL", &["b1"]);
let admit = admit_from(vec![teams, slack]);
let outcomes = HashMap::from([
(
"teams".to_string(),
IdentifyOutcome::Identified("28:legal".to_string()),
),
(
"slack".to_string(),
IdentifyOutcome::Identified("UNDECLARED-BOT".to_string()),
),
]);
assert_eq!(fold_outcomes(&admit, &outcomes), ResolverOutcome::Ambiguous);
}
#[test]
fn should_probe_header_wins_short_circuits() {
let outcome = should_probe(true, Some("teams-legal"), 3);
assert_eq!(
outcome,
Some(ResolverOutcome::HeaderWins("teams-legal".into()))
);
}
#[test]
fn should_probe_public_peer_without_header_short_circuits() {
let outcome = should_probe(false, None, 3);
assert_eq!(outcome, Some(ResolverOutcome::PublicSkipped));
}
#[test]
fn should_probe_loopback_with_endpoints_proceeds() {
let outcome = should_probe(true, None, 2);
assert!(outcome.is_none());
}
#[test]
fn should_probe_loopback_no_endpoints_skips() {
let outcome = should_probe(true, None, 0);
assert_eq!(outcome, Some(ResolverOutcome::Skipped));
}
#[test]
fn should_probe_public_peer_with_header_still_wins() {
let outcome = should_probe(false, Some("teams-legal"), 3);
assert_eq!(
outcome,
Some(ResolverOutcome::HeaderWins("teams-legal".into()))
);
}
fn make_hint(header_names: &[&str]) -> IdentifyInstanceHint {
use greentic_runner_host::identify_hint::HintSource;
IdentifyInstanceHint {
sources: header_names
.iter()
.map(|name| HintSource::Header {
name: (*name).to_string(),
})
.collect(),
}
}
#[test]
fn scope_headers_passes_through_when_all_provider_types_hinted() {
let headers = vec![(
"x-telegram-bot-api-secret-token".to_string(),
"tok".to_string(),
)];
let hints = HashMap::from([
(
"telegram".to_string(),
Some(make_hint(&["x-telegram-bot-api-secret-token"])),
),
("teams".to_string(), Some(make_hint(&[]))),
]);
let (out, unhinted) = scope_headers_to_hinted(headers.clone(), &hints);
assert!(unhinted.is_empty());
assert_eq!(out, headers, "all-hinted should pass headers through");
}
#[test]
fn scope_headers_drops_all_when_any_provider_type_unhinted() {
let headers = vec![(
"x-telegram-bot-api-secret-token".to_string(),
"tok".to_string(),
)];
let hints = HashMap::from([
(
"telegram".to_string(),
Some(make_hint(&["x-telegram-bot-api-secret-token"])),
),
("slack".to_string(), None), ]);
let (out, unhinted) = scope_headers_to_hinted(headers, &hints);
assert_eq!(unhinted, vec!["slack"]);
assert!(out.is_empty(), "any unhinted type should strip all headers");
}
#[test]
fn scope_headers_drops_all_when_no_hints_at_all() {
let headers = vec![(
"x-telegram-bot-api-secret-token".to_string(),
"tok".to_string(),
)];
let hints = HashMap::from([
("telegram".to_string(), None),
("slack".to_string(), None),
("webex".to_string(), None),
]);
let (out, unhinted) = scope_headers_to_hinted(headers, &hints);
assert_eq!(unhinted.len(), 3);
assert!(
out.is_empty(),
"fully unhinted env should strip all headers"
);
}
}