use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::envelope::{CardKind, Spec};
use crate::ids::{CardName, CardUid, SpaceName};
use crate::refs::ReferenceSlotVisitor;
use wyrd_semver::VersionBlock;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub struct CardRef {
pub kind: CardKind,
pub name: CardName,
pub version: VersionBlock,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space: Option<SpaceName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid: Option<CardUid>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CardRefIdentity {
pub kind: CardKind,
pub space: Option<SpaceName>,
pub name: CardName,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case", untagged)]
pub enum Ref {
Ref(CardRef),
Sibling {
sibling: CardRef,
},
#[cfg_attr(feature = "server", schema(value_type = String))]
Path(PathBuf),
}
impl Ref {
#[must_use]
pub fn as_card_ref(&self) -> Option<&CardRef> {
match self {
Self::Ref(card_ref) | Self::Sibling { sibling: card_ref } => Some(card_ref),
Self::Path(_) => None,
}
}
#[must_use]
pub fn as_card_ref_mut(&mut self) -> Option<&mut CardRef> {
match self {
Self::Ref(card_ref) | Self::Sibling { sibling: card_ref } => Some(card_ref),
Self::Path(_) => None,
}
}
#[must_use]
pub fn as_sibling(&self) -> Option<&CardRef> {
match self {
Self::Sibling { sibling } => Some(sibling),
Self::Ref(_) | Self::Path(_) => None,
}
}
}
impl From<CardRef> for Ref {
fn from(card_ref: CardRef) -> Self {
Self::Ref(card_ref)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case", untagged)]
pub enum InlineableRef<T> {
Ref(CardRef),
Sibling {
sibling: CardRef,
},
Inline(Box<T>),
#[cfg_attr(feature = "server", schema(value_type = String))]
Path(PathBuf),
}
impl<T> InlineableRef<T> {
#[must_use]
pub fn as_card_ref(&self) -> Option<&CardRef> {
match self {
Self::Ref(card_ref) | Self::Sibling { sibling: card_ref } => Some(card_ref),
Self::Inline(_) | Self::Path(_) => None,
}
}
#[must_use]
pub fn as_card_ref_mut(&mut self) -> Option<&mut CardRef> {
match self {
Self::Ref(card_ref) | Self::Sibling { sibling: card_ref } => Some(card_ref),
Self::Inline(_) | Self::Path(_) => None,
}
}
#[must_use]
pub fn as_inline(&self) -> Option<&T> {
match self {
Self::Inline(body) => Some(body),
Self::Ref(_) | Self::Sibling { .. } | Self::Path(_) => None,
}
}
#[must_use]
pub fn as_inline_mut(&mut self) -> Option<&mut T> {
match self {
Self::Inline(body) => Some(body),
Self::Ref(_) | Self::Sibling { .. } | Self::Path(_) => None,
}
}
#[must_use]
pub fn as_sibling(&self) -> Option<&CardRef> {
match self {
Self::Sibling { sibling } => Some(sibling),
Self::Ref(_) | Self::Inline(_) | Self::Path(_) => None,
}
}
#[must_use]
pub fn to_durable(&self) -> Option<Ref> {
match self {
Self::Ref(card_ref) => Some(Ref::Ref(card_ref.clone())),
Self::Sibling { sibling } => Some(Ref::Sibling {
sibling: sibling.clone(),
}),
Self::Inline(_) | Self::Path(_) => None,
}
}
}
impl<T> From<CardRef> for InlineableRef<T> {
fn from(card_ref: CardRef) -> Self {
Self::Ref(card_ref)
}
}
impl From<skald_spec::Prompt> for InlineableRef<skald_spec::Prompt> {
fn from(prompt: skald_spec::Prompt) -> Self {
Self::Inline(Box::new(prompt))
}
}
impl From<crate::card::agent::AgentSpec> for InlineableRef<crate::card::agent::AgentSpec> {
fn from(spec: crate::card::agent::AgentSpec) -> Self {
Self::Inline(Box::new(spec))
}
}
impl CardRef {
#[must_use]
pub fn identity_key(&self) -> CardRefIdentity {
CardRefIdentity {
kind: self.kind.clone(),
space: self.space.clone(),
name: self.name.clone(),
version: self.version.as_str().to_owned(),
}
}
#[must_use]
pub fn same_identity(&self, other: &CardRef) -> bool {
self.kind == other.kind
&& self.space == other.space
&& self.name == other.name
&& self.version == other.version
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CardRefScope(Vec<CardRef>);
impl CardRefScope {
#[must_use]
pub fn as_slice(&self) -> &[CardRef] {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn from_root_and_members(
root: &CardRef,
members: impl IntoIterator<Item = CardRef>,
) -> Self {
let mut scope = vec![root.clone()];
for member in members {
match scope
.iter_mut()
.find(|existing| existing.same_identity(&member))
{
Some(existing) => {
if existing.uid.is_none() {
existing.uid = member.uid;
}
}
None => scope.push(member),
}
}
Self(scope)
}
#[must_use]
pub fn own(root: &CardRef) -> Self {
Self::from_root_and_members(root, std::iter::empty())
}
#[must_use]
pub fn permits_root(&self, root: &CardRef) -> bool {
self.is_empty() || self.authorizes(root)
}
#[must_use]
pub fn authorizes(&self, card: &CardRef) -> bool {
self.0.iter().any(|member| member.same_identity(card))
}
#[must_use]
pub fn uids(&self) -> Vec<CardUid> {
self.0
.iter()
.filter_map(|member| member.uid.clone())
.collect()
}
}
impl Serialize for CardRefScope {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.0.iter().map(ToString::to_string))
}
}
impl<'de> Deserialize<'de> for CardRefScope {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let members: Vec<CardRef> = Vec::<String>::deserialize(deserializer)?
.into_iter()
.map(|s| s.parse::<CardRef>().map_err(serde::de::Error::custom))
.collect::<Result<Vec<_>, _>>()?;
Ok(match members.as_slice() {
[] => Self::default(),
[root, rest @ ..] => Self::from_root_and_members(root, rest.iter().cloned()),
})
}
}
#[must_use]
pub fn scope_child_card_refs(spec: &Spec) -> Vec<CardRef> {
let mut spec = spec.clone();
let mut references = Vec::new();
ReferenceSlotVisitor::visit(&mut spec, |slot| {
references.extend(slot.value.as_card_ref().cloned());
});
references
}
#[must_use]
pub fn unresolved_card_ref_paths(spec: &Spec) -> Vec<PathBuf> {
let mut spec = spec.clone();
let mut paths = Vec::new();
ReferenceSlotVisitor::visit(&mut spec, |slot| {
paths.extend(slot.value.as_path().map(std::path::Path::to_path_buf));
});
paths
}
#[must_use]
pub fn registration_only_sibling_refs(spec: &Spec) -> Vec<CardRef> {
let mut spec = spec.clone();
let mut siblings = Vec::new();
ReferenceSlotVisitor::visit(&mut spec, |slot| {
siblings.extend(slot.value.as_sibling().cloned());
});
siblings
}
pub fn bind_scoped_card_ref_uids(
kind: &CardKind,
spec: Spec,
resolved: &[(CardRef, CardUid)],
) -> Result<Spec, crate::envelope::SpecDecodeError> {
if resolved.is_empty() {
return Ok(spec);
}
let mut value =
serde_json::to_value(spec).map_err(|error| crate::envelope::SpecDecodeError {
kind: kind.wire_name().to_owned(),
message: error.to_string(),
})?;
for (card_ref, uid) in resolved {
let mut target =
serde_json::to_value(card_ref).expect("CardRef serialization is infallible");
if let serde_json::Value::Object(object) = &mut target {
object.remove("uid");
}
let uid_value = serde_json::to_value(uid).expect("CardUid serialization is infallible");
bind_serialized_ref(&mut value, &target, &uid_value);
}
Spec::from_kind_and_value(kind, value)
}
fn bind_serialized_ref(
value: &mut serde_json::Value,
target: &serde_json::Value,
uid: &serde_json::Value,
) {
match value {
serde_json::Value::Array(values) => {
for value in values {
bind_serialized_ref(value, target, uid);
}
}
serde_json::Value::Object(object) => {
let mut identity = object.clone();
identity.remove("uid");
if serde_json::Value::Object(identity) == *target {
object.insert("uid".to_owned(), uid.clone());
}
for value in object.values_mut() {
bind_serialized_ref(value, target, uid);
}
}
serde_json::Value::Null
| serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::String(_) => {}
}
}
impl fmt::Display for CardRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}/{}/{}@{}",
self.space
.as_ref()
.map_or("<missing-space>", SpaceName::as_str),
self.kind.wire_name(),
self.name,
self.version
)?;
if let Some(uid) = &self.uid {
write!(f, "#{uid}")?;
}
Ok(())
}
}
impl FromStr for CardRef {
type Err = CardRefParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (head, uid) = match value.split_once('#') {
Some((head, uid)) => (head, Some(uid.parse().map_err(CardRefParseError::Uid)?)),
None => (value, None),
};
let (before_version, version) = head
.rsplit_once('@')
.ok_or(CardRefParseError::MissingVersion)?;
let mut parts = before_version.split('/');
let space = parts
.next()
.ok_or(CardRefParseError::MissingSpace)?
.parse()
.map_err(CardRefParseError::Space)?;
let kind = parts
.next()
.ok_or(CardRefParseError::MissingKind)
.and_then(parse_card_kind)?;
let name = parts
.next()
.ok_or(CardRefParseError::MissingName)?
.parse()
.map_err(CardRefParseError::Name)?;
if parts.next().is_some() {
return Err(CardRefParseError::TooManySegments);
}
Ok(Self {
kind,
name,
version: version.parse().map_err(CardRefParseError::Version)?,
space: Some(space),
uid,
})
}
}
fn parse_card_kind(value: &str) -> Result<CardKind, CardRefParseError> {
CardKind::native()
.into_iter()
.find(|kind| kind.wire_name() == value)
.ok_or_else(|| CardRefParseError::Kind(value.to_owned()))
}
#[derive(Debug, thiserror::Error)]
pub enum CardRefParseError {
#[error("card ref is missing space")]
MissingSpace,
#[error("card ref is missing kind")]
MissingKind,
#[error("card ref is missing name")]
MissingName,
#[error("card ref is missing @version")]
MissingVersion,
#[error("card ref must be space/kind/name@version[#uid]")]
TooManySegments,
#[error("unknown card kind: {0}")]
Kind(String),
#[error("invalid space: {0}")]
Space(crate::ids::IdError),
#[error("invalid name: {0}")]
Name(crate::ids::IdError),
#[error("invalid version: {0}")]
Version(wyrd_semver::VersionError),
#[error("invalid uid: {0}")]
Uid(crate::ids::IdError),
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::card::agent::{AgentRunConfigSpec, AgentSpec};
use crate::card::mcp::McpSpec;
use crate::card::service::{ServiceComponent, ServiceSpec};
fn sample_ref() -> CardRef {
CardRef {
kind: CardKind::Artifact,
name: CardName::new("weights").expect("static name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("prod").expect("static space is valid")),
uid: None,
}
}
#[test]
fn card_ref_display_and_from_str_roundtrip() {
let card_ref = CardRef {
kind: CardKind::Service,
name: CardName::new("billing").expect("static name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("prod").expect("static space is valid")),
uid: None,
};
let text = card_ref.to_string();
let parsed: CardRef = text.parse().expect("card ref parses");
assert_eq!(text, "prod/Service/billing@1.0.0");
assert_eq!(parsed, card_ref);
}
#[test]
fn card_ref_canonical_round_trip() {
let card_ref = sample_ref();
let parsed: CardRef = card_ref.to_string().parse().expect("card ref parses");
assert_eq!(parsed, card_ref);
}
#[test]
fn card_ref_with_uid_round_trip() {
let card_ref = CardRef {
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid"),
),
..sample_ref()
};
let text = card_ref.to_string();
let parsed: CardRef = text.parse().expect("card ref parses");
assert_eq!(
text,
"prod/Artifact/weights@1.0.0#01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11"
);
assert_eq!(parsed, card_ref);
}
#[test]
fn card_ref_display_fromstr_inverse() {
let card_ref = sample_ref();
let parsed: CardRef = card_ref.to_string().parse().expect("card ref parses");
assert_eq!(parsed.to_string(), card_ref.to_string());
}
#[test]
fn card_ref_canonical_bytes_pinned() {
let card_ref = CardRef {
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid"),
),
..sample_ref()
};
let bytes = serde_json::to_vec(&card_ref).expect("card ref serializes");
assert_eq!(
std::str::from_utf8(&bytes).expect("json is utf8"),
r#"{"kind":"Artifact","name":"weights","version":"1.0.0","space":"prod","uid":"01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11"}"#
);
}
#[test]
fn card_ref_serde_roundtrips() {
let card_ref = sample_ref();
let json = serde_json::to_string(&card_ref).expect("serialize");
let parsed: CardRef = serde_json::from_str(&json).expect("deserialize");
assert_eq!(card_ref, parsed);
}
#[test]
fn sibling_ref_uses_explicit_wire_wrapper() {
let reference = Ref::Sibling {
sibling: sample_ref(),
};
let json = serde_json::to_value(&reference).expect("sibling ref serializes");
assert_eq!(json, serde_json::json!({"sibling": sample_ref()}));
let parsed: Ref = serde_json::from_value(json).expect("sibling ref deserializes");
assert_eq!(parsed, reference);
}
#[test]
fn card_ref_skips_none_uid() {
let card_ref = CardRef {
kind: CardKind::Model,
name: CardName::new("churn").expect("static name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("default").expect("static space is valid")),
uid: None,
};
let json = serde_json::to_string(&card_ref).expect("serialize");
assert!(
json.contains(r#""space":"default""#),
"space must serialize"
);
assert!(!json.contains("uid"), "uid=None must skip");
}
#[test]
fn card_ref_scope_serde_preserves_resolved_uids() {
let root = CardRef {
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid"),
),
..sample_ref()
};
let secondary = CardRef {
kind: CardKind::Model,
name: CardName::new("scorer").expect("static name is valid"),
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b22").expect("static uid is valid"),
),
..sample_ref()
};
let scope = CardRefScope::from_root_and_members(&root, [secondary.clone()]);
let json = serde_json::to_string(&scope).expect("scope serializes");
let parsed: CardRefScope = serde_json::from_str(&json).expect("scope deserializes");
assert_eq!(
json,
r#"["prod/Artifact/weights@1.0.0#01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11","prod/Model/scorer@1.0.0#01890f28-7c4a-7cc3-98e7-4f4a3c2d1b22"]"#,
"compact claim encoding carries every resolved uid"
);
assert_eq!(parsed, scope, "round trip preserves the whole scope");
assert_eq!(
parsed.as_slice()[0].uid,
root.uid,
"root uid survives the claim round trip"
);
assert_eq!(
parsed.as_slice()[1].uid,
secondary.uid,
"secondary uid survives the claim round trip"
);
}
#[test]
fn from_root_and_members_adopts_resolved_root_uid() {
let authored = sample_ref();
let resolved = CardRef {
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid"),
),
..sample_ref()
};
let scope = CardRefScope::from_root_and_members(&authored, [resolved.clone()]);
assert_eq!(
scope.len(),
1,
"the resolved duplicate is still deduplicated"
);
assert_eq!(
scope.as_slice()[0].uid,
resolved.uid,
"the resolved uid is adopted onto the retained root"
);
}
#[test]
fn binding_replaces_an_authored_uid_with_the_server_uid() {
let authored_uid =
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid");
let resolved_uid =
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b12").expect("static uid is valid");
let authored_ref = CardRef {
kind: CardKind::Prompt,
name: CardName::new("system-prompt").expect("static name is valid"),
version: VersionBlock::parse("1.0.0").expect("static version is valid"),
space: Some(SpaceName::new("default").expect("static space is valid")),
uid: Some(authored_uid),
};
let lookup_ref = CardRef {
uid: None,
..authored_ref.clone()
};
let spec = Spec::Mcp(McpSpec {
server_name: "tools".to_owned(),
tool_refs: vec![authored_ref.into()],
..McpSpec::default()
});
let bound =
bind_scoped_card_ref_uids(&CardKind::Mcp, spec, &[(lookup_ref, resolved_uid.clone())])
.expect("MCP spec remains valid after UID binding");
let Spec::Mcp(bound) = bound else {
panic!("expected an MCP spec");
};
assert_eq!(
bound.tool_refs[0]
.as_card_ref()
.and_then(|card_ref| card_ref.uid.clone()),
Some(resolved_uid)
);
}
#[test]
fn card_ref_accepts_missing_authored_space() {
let json = r#"{"kind":"Model","name":"churn","version":"1.0.0"}"#;
let card_ref =
serde_json::from_str::<CardRef>(json).expect("space is optional while authored");
assert!(card_ref.space.is_none());
}
#[test]
fn card_ref_identity_ignores_uid_but_includes_version_and_space() {
let first = sample_ref();
let with_uid = CardRef {
uid: Some(
CardUid::new("01890f28-7c4a-7cc3-98e7-4f4a3c2d1b11").expect("static uid is valid"),
),
..first.clone()
};
assert!(first.same_identity(&with_uid));
let different_version = CardRef {
version: VersionBlock::parse("2.0.0").expect("static version is valid"),
..first.clone()
};
assert!(!first.same_identity(&different_version));
let different_space = CardRef {
space: Some(SpaceName::new("staging").expect("static space is valid")),
..first
};
assert!(!with_uid.same_identity(&different_space));
}
fn path_binding(dir: &str) -> crate::card::verifier::VerificationBinding {
crate::card::verifier::VerificationBinding {
verifier: Ref::Path(PathBuf::from(format!("{dir}/verifier.yaml"))),
runs_on: Some(InlineableRef::Path(PathBuf::from(format!(
"{dir}/trigger.yaml"
)))),
on_failure: vec![InlineableRef::Path(PathBuf::from(format!(
"{dir}/operator.yaml"
)))],
}
}
#[test]
fn unresolved_card_ref_paths_include_top_level_slots() {
let mut spec = ServiceSpec::default();
spec.components.push(ServiceComponent {
alias: "model".to_owned(),
card_ref: Ref::Path(PathBuf::from("components/model.yaml")),
verified_by: vec![path_binding("component")],
source: None,
config: BTreeMap::new(),
credential_refs: Vec::new(),
});
spec.verified_by.push(path_binding("service"));
assert_eq!(
unresolved_card_ref_paths(&Spec::Service(spec)),
vec![
PathBuf::from("components/model.yaml"),
PathBuf::from("component/verifier.yaml"),
PathBuf::from("component/trigger.yaml"),
PathBuf::from("component/operator.yaml"),
PathBuf::from("service/verifier.yaml"),
PathBuf::from("service/trigger.yaml"),
PathBuf::from("service/operator.yaml"),
]
);
}
#[test]
fn unresolved_card_ref_paths_include_nested_slots() {
let spec = nested_judge(InlineableRef::Path(PathBuf::from("nested/agent.yaml")));
assert_eq!(
unresolved_card_ref_paths(&spec),
vec![PathBuf::from("nested/agent.yaml")]
);
}
#[test]
fn unresolved_card_ref_paths_are_empty_for_a_resolved_spec() {
assert!(unresolved_card_ref_paths(&Spec::Service(ServiceSpec::default())).is_empty());
}
fn nested_prompt() -> skald_spec::Prompt {
skald_spec::Prompt::new(
skald_spec::ProviderRequest::OpenAiChatCompletion(skald_spec::OpenAiChatRequest {
model: "gpt-test".to_owned(),
messages: vec![skald_spec::OpenAiChatMessage {
role: "user".to_owned(),
content: Some(skald_spec::wire::openai_chat::OpenAiMessageContent::Text(
"hello".to_owned(),
)),
..Default::default()
}],
response_format: None,
stream: None,
stream_options: None,
tools: None,
tool_choice: None,
parallel_tool_calls: None,
settings: skald_spec::OpenAiChatSettings::default(),
}),
"gpt-test",
)
.expect("fixture prompt is valid")
}
fn nested_agent(prompt: InlineableRef<skald_spec::Prompt>) -> AgentSpec {
AgentSpec {
prompt,
execution_route: crate::card::agent::ExecutionRoute::Native {},
tool_names: Vec::new(),
run_config: AgentRunConfigSpec::default(),
verified_by: Vec::new(),
}
}
fn nested_judge(judge_ref: InlineableRef<AgentSpec>) -> Spec {
use crate::card::verifier::{VerifierImplementation, VerifierSpec};
use crate::vala::eval::{ComparisonOperator, EvalSpec, EvalTask, LlmJudgeTask, TaskId};
let task_id = TaskId::new("judge").expect("fixture task id is valid");
let judge = LlmJudgeTask::new(
task_id.clone(),
judge_ref,
ComparisonOperator::Equals,
serde_json::json!(true),
)
.expect("fixture judge is valid");
Spec::Verifier(VerifierSpec {
description: None,
implementation: VerifierImplementation::Eval(
EvalSpec::new(BTreeMap::from([(task_id, EvalTask::LlmJudge(judge))]))
.expect("fixture eval is valid"),
),
})
}
#[test]
fn registration_only_sibling_refs_include_top_level_and_nested_slots() {
let top_level = sample_ref();
let nested = CardRef {
name: CardName::new("nested-prompt").expect("static name is valid"),
..sample_ref()
};
let mut service = ServiceSpec::default();
service.components.push(ServiceComponent {
alias: "top-level".to_owned(),
card_ref: Ref::Sibling {
sibling: top_level.clone(),
},
verified_by: Vec::new(),
source: None,
config: BTreeMap::new(),
credential_refs: Vec::new(),
});
let spec = nested_judge(InlineableRef::Inline(Box::new(nested_agent(
InlineableRef::Sibling {
sibling: nested.clone(),
},
))));
assert_eq!(
registration_only_sibling_refs(&Spec::Service(service)),
vec![top_level]
);
assert_eq!(registration_only_sibling_refs(&spec), vec![nested]);
}
#[test]
fn registration_only_sibling_refs_ignore_other_reference_forms() {
let durable = sample_ref();
let path = PathBuf::from("nested/prompt.yaml");
let mut service = ServiceSpec::default();
service.components.extend([
ServiceComponent {
alias: "durable".to_owned(),
card_ref: Ref::Ref(durable),
verified_by: Vec::new(),
source: None,
config: BTreeMap::new(),
credential_refs: Vec::new(),
},
ServiceComponent {
alias: "path".to_owned(),
card_ref: Ref::Path(path),
verified_by: Vec::new(),
source: None,
config: BTreeMap::new(),
credential_refs: Vec::new(),
},
]);
let judge = nested_judge(InlineableRef::Inline(Box::new(nested_agent(
InlineableRef::Inline(Box::new(nested_prompt())),
))));
assert!(registration_only_sibling_refs(&Spec::Service(service)).is_empty());
assert!(registration_only_sibling_refs(&judge).is_empty());
}
}