use crate::error::{Error, Result};
use crate::server::skills::Skill;
use crate::server::workflow::{DataSource, PromptContent, SequentialWorkflow, WorkflowStep};
use crate::types::{PromptArgumentType, ToolAnnotations};
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ProjectionNotice {
SlugFallback {
original: String,
slug: String,
},
EmptyDescription {
substituted: String,
},
}
const MAX_SLUG_LEN: usize = 64;
fn fallback_description(slug: &str) -> String {
format!("Projected from the {slug} workflow.")
}
fn slugify(name: &str) -> Option<String> {
let mapped: String = name
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let mut slug = mapped
.split('-')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("-");
if slug.is_empty() {
return None;
}
if slug.len() > MAX_SLUG_LEN {
slug.truncate(MAX_SLUG_LEN);
while slug.ends_with('-') {
slug.pop();
}
if slug.is_empty() {
return None;
}
}
Some(slug)
}
fn fallback_slug(original_name: &str) -> String {
let digest = super::sha256_digest_hex(original_name.as_bytes());
let hex: String = digest.chars().skip("sha256:".len()).take(8).collect();
format!("workflow-{hex}")
}
use crate::shared::log_sanitize::sanitize_for_log;
fn yaml_double_quoted(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
'\u{fffe}' | '\u{ffff}' => out.push_str(&format!("\\u{:04x}", c as u32)),
_ if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
_ => out.push(c),
}
}
out.push('"');
out
}
fn render_frontmatter(slug: &str, description: &str) -> String {
format!(
"---\nname: {}\ndescription: {}\n---\n",
yaml_double_quoted(slug),
yaml_double_quoted(description)
)
}
fn render_prompt_content(content: &PromptContent) -> String {
match content {
PromptContent::Text(text) => text.clone(),
PromptContent::Image { mime_type, .. } => format!("(image content: {mime_type})"),
PromptContent::ResourceUri(uri) => format!("Read the resource `{uri}`."),
PromptContent::ResourceHandle(handle) => {
format!("Read the resource `{}`.", handle.uri())
},
PromptContent::ToolHandle(handle) => format!("Uses tool `{}`.", handle.name()),
PromptContent::Multi(parts) => parts
.iter()
.map(|part| render_prompt_content(part))
.collect::<Vec<_>>()
.join("\n\n"),
#[allow(unreachable_patterns)]
_ => "(unsupported instruction content)".to_string(),
}
}
fn render_context(wf: &SequentialWorkflow) -> String {
if wf.instructions().is_empty() {
return String::new();
}
let mut out = String::from("## Context\n\n");
for message in wf.instructions() {
out.push_str(&format!(
"{}: {}\n\n",
message.role,
render_prompt_content(&message.content)
));
}
out
}
fn prompt_argument_type_name(arg_type: PromptArgumentType) -> &'static str {
match arg_type {
PromptArgumentType::String => "string",
PromptArgumentType::Number => "number",
PromptArgumentType::Integer => "integer",
PromptArgumentType::Boolean => "boolean",
}
}
fn render_inputs(wf: &SequentialWorkflow) -> String {
if wf.arguments().is_empty() {
return String::new();
}
let mut out = String::from("## Inputs\n\n");
for (name, spec) in wf.arguments() {
let requiredness = if spec.required {
"required"
} else {
"optional"
};
let type_note = match spec.arg_type {
Some(arg_type) => format!(", type `{}`", prompt_argument_type_name(arg_type)),
None => String::new(),
};
out.push_str(&format!(
"- `{name}` ({requiredness}{type_note}): {}\n",
spec.description
));
}
out.push('\n');
out
}
fn render_data_source(source: &DataSource) -> String {
match source {
DataSource::PromptArg(name) => format!("the `{name}` input"),
DataSource::StepOutput { step, field: None } => format!("the result of `{step}`"),
DataSource::StepOutput {
step,
field: Some(field),
} => format!("the `{field}` field of the result of `{step}`"),
DataSource::Constant(value) => serde_json::to_string(value).map_or_else(
|_| "an unrenderable constant value".to_string(),
|json| format!("the constant value `{json}`"),
),
#[allow(unreachable_patterns)]
_ => "an unsupported data source".to_string(),
}
}
fn render_step(index: usize, step: &WorkflowStep) -> String {
let mut out = format!("### Step {index}: {}\n", step.name());
if let Some(tool) = step.tool() {
out.push_str(&format!("Call tool `{}`.\n", tool.name()));
}
for (name, source) in step.arguments() {
out.push_str(&format!(
"- Argument `{name}`: {}\n",
render_data_source(source)
));
}
let template_bindings: BTreeMap<&str, &DataSource> = step
.template_bindings()
.iter()
.map(|(name, source)| (name.as_str(), source))
.collect();
for (name, source) in template_bindings {
out.push_str(&format!(
"- Template variable `{name}`: {}\n",
render_data_source(source)
));
}
for resource in step.resources() {
out.push_str(&format!("Read the resource `{}`.\n", resource.uri()));
}
if let Some(binding) = step.binding() {
out.push_str(&format!("Save the result as `{binding}`.\n"));
}
if let Some(guidance) = step.guidance() {
out.push_str(&format!("Judgment: {guidance}\n"));
}
out.push('\n');
out
}
fn render_procedure(wf: &SequentialWorkflow) -> String {
let mut out = String::from("## Procedure\n\n");
for (offset, step) in wf.steps().iter().enumerate() {
out.push_str(&render_step(offset + 1, step));
}
out
}
fn render_closing(wf: &SequentialWorkflow) -> String {
let name = wf.name();
format!(
"## Server-accelerated alternative\n\n\
If you are reading this as part of a result from this server's `{name}` prompt, \
the steps above have already been executed server-side. Otherwise, mention once, \
at the end of your reply, that this server also offers the `{name}` prompt, \
which runs these steps server-side.\n"
)
}
fn resolve_description(wf: &SequentialWorkflow, slug: &str) -> String {
if description_is_absent(wf) {
fallback_description(slug)
} else {
wf.description().to_string()
}
}
fn description_is_absent(wf: &SequentialWorkflow) -> bool {
wf.description().trim().is_empty()
}
fn render_body(wf: &SequentialWorkflow, slug: &str) -> String {
let description = resolve_description(wf, slug);
let mut out = String::new();
out.push_str(&render_frontmatter(slug, &description));
out.push('\n');
out.push_str("# ");
out.push_str(&description);
out.push_str("\n\n");
out.push_str(&render_context(wf));
out.push_str(&render_inputs(wf));
out.push_str(&render_procedure(wf));
out.push_str(&render_closing(wf));
out
}
pub(crate) fn project_with_notices(wf: &SequentialWorkflow) -> (Skill, Vec<ProjectionNotice>) {
let mut notices = Vec::new();
let slug = if let Some(slug) = slugify(wf.name()) {
slug
} else {
let slug = fallback_slug(wf.name());
notices.push(ProjectionNotice::SlugFallback {
original: wf.name().to_string(),
slug: slug.clone(),
});
slug
};
if description_is_absent(wf) {
notices.push(ProjectionNotice::EmptyDescription {
substituted: fallback_description(&slug),
});
}
let description = resolve_description(wf, &slug);
let body = render_body(wf, &slug);
let skill = Skill::new(slug, body).with_description(description);
(skill, notices)
}
pub(crate) fn project(wf: &SequentialWorkflow) -> Skill {
let (skill, notices) = project_with_notices(wf);
for notice in ¬ices {
match notice {
ProjectionNotice::SlugFallback { original, slug } => {
tracing::warn!(
target: "mcp.skills",
workflow = %sanitize_for_log(original),
slug = %slug,
"workflow name has no agentskills-legal characters; \
projected skill uses a deterministic fallback slug"
);
},
ProjectionNotice::EmptyDescription { substituted } => {
tracing::warn!(
target: "mcp.skills",
workflow = %sanitize_for_log(wf.name()),
substituted = %substituted,
"workflow description is empty; projected skill uses a \
deterministic fallback description"
);
},
}
}
skill
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProjectionWarningKind {
GuidanceOnSideEffectingStep,
GateCheckUnverifiable,
SlugFallback,
}
#[derive(Debug, Clone)]
pub struct ProjectionWarning {
kind: ProjectionWarningKind,
step: Option<String>,
tool: Option<String>,
message: String,
}
impl ProjectionWarning {
fn new(
kind: ProjectionWarningKind,
step: Option<String>,
tool: Option<String>,
message: String,
) -> Self {
Self {
kind,
step,
tool,
message,
}
}
pub fn kind(&self) -> ProjectionWarningKind {
self.kind
}
pub fn step(&self) -> Option<&str> {
self.step.as_deref()
}
pub fn tool(&self) -> Option<&str> {
self.tool.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
}
fn is_annotated_side_effecting(annotations: &ToolAnnotations) -> bool {
annotations.read_only_hint == Some(false) || annotations.destructive_hint == Some(true)
}
fn gate_check_step(
step: &str,
tool: &str,
tools: &HashMap<String, Option<ToolAnnotations>>,
) -> Option<ProjectionWarning> {
match tools.get(tool) {
Some(Some(annotations)) if is_annotated_side_effecting(annotations) => {
Some(ProjectionWarning::new(
ProjectionWarningKind::GuidanceOnSideEffectingStep,
Some(step.to_string()),
Some(tool.to_string()),
format!(
"step `{step}` carries guidance, but its tool `{tool}` is annotated \
side-effecting; server-side workflow execution runs this step \
regardless of the guidance, so the guidance is a post-hoc judgment \
the executing surface will ignore"
),
))
},
Some(Some(_)) => None,
Some(None) | None => Some(ProjectionWarning::new(
ProjectionWarningKind::GateCheckUnverifiable,
Some(step.to_string()),
Some(tool.to_string()),
format!(
"step `{step}` carries guidance, but the side-effect check could not be \
performed: tool `{tool}` carries no annotations in the supplied tool map, \
so whether server-side execution would run this step regardless of the \
guidance is unknown"
),
)),
}
}
fn gate_check(
wf: &SequentialWorkflow,
tools: Option<&HashMap<String, Option<ToolAnnotations>>>,
) -> Vec<ProjectionWarning> {
let Some(tools) = tools else {
return Vec::new();
};
wf.steps()
.iter()
.filter(|step| step.guidance().is_some())
.filter_map(|step| {
step.tool()
.and_then(|handle| gate_check_step(step.name().as_str(), handle.name(), tools))
})
.collect()
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ProjectionOutput {
pub skill: Skill,
pub warnings: Vec<ProjectionWarning>,
}
impl ProjectionOutput {
#[must_use]
pub fn into_parts(self) -> (Skill, Vec<ProjectionWarning>) {
(self.skill, self.warnings)
}
}
#[derive(Debug)]
pub struct SkillProjection<'w> {
workflow: &'w SequentialWorkflow,
tools: Option<HashMap<String, Option<ToolAnnotations>>>,
}
impl<'w> SkillProjection<'w> {
#[must_use]
pub fn new(workflow: &'w SequentialWorkflow) -> Self {
Self {
workflow,
tools: None,
}
}
#[must_use]
pub fn with_tools(mut self, tools: impl IntoIterator<Item = crate::types::ToolInfo>) -> Self {
self.tools = Some(
tools
.into_iter()
.map(|tool| (tool.name, tool.annotations))
.collect(),
);
self
}
pub fn build(self) -> Result<ProjectionOutput> {
let workflow = self.workflow;
let name = workflow.name();
if slugify(name).is_none() {
return Err(Error::validation(format!(
"workflow {name:?} normalizes to no agentskills-legal skill name; \
rename it to something containing at least one `[a-z0-9]` character, \
or use `SequentialWorkflow::as_skill()`, which substitutes a \
deterministic `workflow-{{8 hex}}` slug instead"
)));
}
if description_is_absent(workflow) {
return Err(Error::validation(format!(
"workflow {name:?} has an empty description, which renders an \
agentskills-illegal frontmatter value; write a description, or use \
`SequentialWorkflow::as_skill()`, which substitutes a deterministic \
legal one instead"
)));
}
let (skill, notices) = project_with_notices(workflow);
debug_assert!(
notices.is_empty(),
"build() rejects both substitution conditions before rendering, so the \
shared seam cannot have substituted anything here"
);
let warnings = gate_check(workflow, self.tools.as_ref());
for warning in &warnings {
tracing::warn!(
target: "mcp.skills",
workflow = %sanitize_for_log(name),
step = %sanitize_for_log(warning.step().unwrap_or_default()),
tool = %sanitize_for_log(warning.tool().unwrap_or_default()),
"{}",
sanitize_for_log(warning.message())
);
}
Ok(ProjectionOutput { skill, warnings })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::log_sanitize::is_unicode_line_separator;
use crate::server::workflow::{
DataSource, InternalPromptMessage, PromptContent, ToolHandle, WorkflowStep,
};
use crate::types::{PromptArgumentType, Role};
use proptest::prelude::*;
use serde_json::json;
use std::collections::BTreeSet;
use std::sync::OnceLock;
const ENCODER_SIGNIFICANT_CHARS: &[char] = &[
'\\', '"', '\n', '\r', '\t', '\u{0}', '\u{7f}', '\u{85}', '\u{2028}', '\u{2029}', ' ', '-',
'.', ':', '#',
];
fn encoder_stressing_text() -> impl Strategy<Value = String> {
proptest::collection::vec(
prop_oneof![
3 => proptest::sample::select(ENCODER_SIGNIFICANT_CHARS),
2 => proptest::char::any(),
],
0..24usize,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn annotated_tool(name: &str, annotations: ToolAnnotations) -> crate::types::ToolInfo {
crate::types::ToolInfo::with_annotations(
name,
None,
json!({ "type": "object" }),
annotations,
)
}
fn tracer_workflow(name: &str, description: &str) -> SequentialWorkflow {
SequentialWorkflow::new(name, description)
.step(WorkflowStep::new("fetch_order", ToolHandle::new("orders_get")).bind("order"))
}
fn body_with_constant(value: serde_json::Value) -> String {
SequentialWorkflow::new("refund_flow", "Process a refund")
.step(
WorkflowStep::new("fetch_order", ToolHandle::new("orders_get"))
.arg("filter", DataSource::constant(value)),
)
.as_skill()
.body()
.to_string()
}
fn kitchen_sink_workflow() -> SequentialWorkflow {
kitchen_sink_workflow_with_execution_mechanics(false)
}
fn kitchen_sink_workflow_with_execution_mechanics(enabled: bool) -> SequentialWorkflow {
SequentialWorkflow::new("refund_flow", "Process a customer refund")
.with_task_support(enabled)
.argument("order_id", "The order to refund", true)
.argument("reason", "Why the customer wants a refund", false)
.instruction(InternalPromptMessage::new(
Role::System,
PromptContent::Text("Never refund more than the original charge.".to_string()),
))
.step(
WorkflowStep::new("fetch_order", ToolHandle::new("orders_get"))
.arg("id", DataSource::prompt_arg("order_id"))
.arg("include_lines", DataSource::constant(json!(true)))
.bind("order")
.retryable(enabled),
)
.step(
WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"))
.arg("amount", DataSource::from_step_field("order", "total"))
.with_guidance("Confirm with the customer before issuing the refund.")
.with_template_binding(
"zeta_region",
DataSource::from_step_field("order", "region"),
)
.with_template_binding("alpha_currency", DataSource::prompt_arg("reason"))
.bind("refund")
.retryable(enabled),
)
.step(
WorkflowStep::fetch_resources("read_policy")
.with_resource("docs://refund-policy")
.expect("valid resource URI"),
)
}
fn body_with_instruction(content: PromptContent) -> String {
SequentialWorkflow::new("refund_flow", "Process a refund")
.instruction(InternalPromptMessage::new(Role::System, content))
.step(WorkflowStep::new("fetch_order", ToolHandle::new("orders_get")).bind("order"))
.as_skill()
.body()
.to_string()
}
fn parsed_frontmatter(body: &str) -> serde_json::Value {
match super::super::parse_frontmatter_value(body) {
super::super::FrontmatterParse::Parsed(value) => value,
other => panic!("frontmatter did not parse cleanly: {other:?}\nbody was:\n{body}"),
}
}
fn assert_description_roundtrips(description: &str) {
let wf = tracer_workflow("refund_flow", description);
let skill = wf.as_skill();
let value = parsed_frontmatter(skill.body());
let obj = value
.as_object()
.expect("frontmatter must parse to a mapping");
assert_eq!(obj.len(), 2, "frontmatter must carry exactly two keys");
assert_eq!(
obj.get("name").and_then(|v| v.as_str()),
Some("refund-flow")
);
assert_eq!(
obj.get("description").and_then(|v| v.as_str()),
Some(description)
);
}
#[test]
fn slugify_maps_underscores_to_hyphens() {
assert_eq!(slugify("refund_flow"), Some("refund-flow".to_string()));
}
#[test]
fn slugify_collapses_and_strips_hyphens() {
assert_eq!(
slugify(" --Refund Flow!!-- "),
Some("refund-flow".to_string())
);
}
#[test]
fn slugify_rejects_a_name_with_nothing_legal() {
assert_eq!(slugify("!!!"), None);
}
#[test]
fn slugify_rejects_the_empty_name() {
assert_eq!(slugify(""), None);
}
#[test]
fn slugify_truncates_to_sixty_four() {
let slug = slugify(&"a".repeat(90)).expect("ascii letters always survive");
assert_eq!(slug.len(), 64);
}
#[test]
fn slugify_restrips_a_hyphen_created_by_truncation() {
let input = format!("{}!x", "a".repeat(63));
let slug = slugify(&input).expect("ascii letters always survive");
assert!(!slug.ends_with('-'), "slug was {slug:?}");
assert_eq!(slug, "a".repeat(63));
}
#[test]
fn fallback_slug_is_shaped_and_stable() {
let first = fallback_slug("!!!");
let second = fallback_slug("!!!");
assert_eq!(first, second, "the fallback must be deterministic");
let hex = first
.strip_prefix("workflow-")
.expect("fallback must be prefixed");
assert_eq!(hex.len(), 8);
assert!(hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
assert_ne!(
fallback_slug("!!!"),
fallback_slug("???"),
"distinct names must not collide onto one slug"
);
}
#[test]
fn sanitize_for_log_replaces_control_characters() {
assert_eq!(sanitize_for_log("a\nb\u{0}c"), "a\u{fffd}b\u{fffd}c");
assert_eq!(sanitize_for_log("plain"), "plain");
}
#[test]
fn sanitize_for_log_replaces_the_unicode_line_separators() {
assert_eq!(sanitize_for_log("a\u{2028}b"), "a\u{fffd}b");
assert_eq!(sanitize_for_log("a\u{2029}b"), "a\u{fffd}b");
assert!(!'\u{2028}'.is_control());
assert!(!'\u{2029}'.is_control());
}
#[test]
fn the_encoder_and_the_log_sanitizer_agree_on_the_line_separators() {
for c in ['\u{2028}', '\u{2029}'] {
assert!(
is_unicode_line_separator(c),
"{c:?} must be classified as a line separator"
);
assert_eq!(
sanitize_for_log(&c.to_string()),
"\u{fffd}",
"the log sanitizer must replace {c:?}"
);
assert!(
!yaml_double_quoted(&c.to_string()).contains(c),
"the encoder must not emit {c:?} verbatim"
);
}
assert!(!is_unicode_line_separator('a'));
assert_eq!(sanitize_for_log("a"), "a");
assert!(yaml_double_quoted("a").contains('a'));
}
#[test]
fn as_skill_name_is_the_slugified_workflow_name() {
let wf = tracer_workflow("refund_flow", "Process a refund");
assert_eq!(wf.as_skill().name(), "refund-flow");
}
#[test]
fn projected_body_shape_satisfies_the_sc5_preconditions() {
let wf = tracer_workflow("refund_flow", "Process a refund");
let skill = wf.as_skill();
let body = skill.body();
assert!(
body.starts_with("---\nname: \"refund-flow\"\n"),
"body began: {:?}",
&body[..body.len().min(60)]
);
assert!(body.ends_with('\n'), "body must end with a newline");
assert!(
!body.ends_with("\n\n"),
"body must end with EXACTLY one newline"
);
assert!(body.len() > 120, "body was only {} bytes", body.len());
assert_eq!(skill.references().count(), 0);
assert_eq!(skill.as_prompt_text(), body);
}
#[test]
fn projected_body_renders_the_procedure() {
let wf = tracer_workflow("refund_flow", "Process a refund");
let skill = wf.as_skill();
let body = skill.body();
assert!(body.contains("## Procedure"), "body was:\n{body}");
assert!(
body.contains("### Step 1: fetch_order"),
"body was:\n{body}"
);
assert!(
body.contains("Call tool `orders_get`."),
"body was:\n{body}"
);
assert!(
body.contains("Save the result as `order`."),
"body was:\n{body}"
);
assert!(
body.contains("## Server-accelerated alternative"),
"body was:\n{body}"
);
assert!(body.contains("`refund_flow`"), "body was:\n{body}");
}
#[test]
fn projected_body_excludes_the_server_execution_mechanics() {
let wf = SequentialWorkflow::new("refund_flow", "Process a refund")
.with_task_support(true)
.step(
WorkflowStep::new("fetch_order", ToolHandle::new("orders_get"))
.bind("order")
.retryable(true),
);
let body = wf.as_skill().body().to_string();
assert!(!body.contains("retryable"), "body was:\n{body}");
assert!(!body.contains("task support"), "body was:\n{body}");
assert!(!body.contains("task_support"), "body was:\n{body}");
}
#[test]
fn frontmatter_survives_a_mapping_indicator() {
assert_description_roundtrips("Refund an order: fast path");
}
#[test]
fn frontmatter_survives_a_comment_indicator() {
assert_description_roundtrips("Handle #123");
}
#[test]
fn frontmatter_survives_a_flow_sequence() {
assert_description_roundtrips("[urgent] refund");
}
#[test]
fn frontmatter_survives_embedded_quotes() {
assert_description_roundtrips("\"quoted\"");
}
#[test]
fn frontmatter_survives_a_leading_dash() {
assert_description_roundtrips("- leading dash");
}
#[test]
fn frontmatter_survives_a_document_delimiter() {
assert_description_roundtrips("---");
}
#[test]
fn frontmatter_survives_a_newline_injection_attempt() {
let description = "Refund orders:\nmetadata: injected";
assert_description_roundtrips(description);
let wf = tracer_workflow("refund_flow", description);
let value = parsed_frontmatter(wf.as_skill().body());
assert!(
value.get("metadata").is_none(),
"a newline injected an extra frontmatter key"
);
}
#[test]
fn frontmatter_survives_a_line_separator_before_a_document_indicator() {
assert_description_roundtrips("a\u{2028}--- b");
assert_description_roundtrips("a\u{2029}--- b");
assert_description_roundtrips("a\u{2028}... b");
assert_description_roundtrips("a\u{2029}... b");
}
#[test]
fn frontmatter_survives_a_line_separator_adjacent_to_blanks() {
assert_description_roundtrips("a\u{2028} b");
assert_description_roundtrips("a \u{2028}b");
assert_description_roundtrips("a\u{2029} b");
assert_description_roundtrips("a \u{2029}b");
}
#[test]
fn frontmatter_survives_a_yaml_boolean_alike_description() {
assert_description_roundtrips("yes");
}
#[test]
fn a_boolean_alike_slug_stays_a_json_string() {
let wf = tracer_workflow("True", "Process a refund");
let skill = wf.as_skill();
assert_eq!(skill.name(), "true");
let value = parsed_frontmatter(skill.body());
let name = value.get("name").expect("name key must be present");
assert!(name.is_string(), "name parsed as {name:?}, not a string");
assert_eq!(name.as_str(), Some("true"));
}
#[test]
fn an_integer_alike_slug_stays_a_json_string() {
let wf = tracer_workflow("123", "Process a refund");
let skill = wf.as_skill();
assert_eq!(skill.name(), "123");
let value = parsed_frontmatter(skill.body());
let name = value.get("name").expect("name key must be present");
assert!(name.is_string(), "name parsed as {name:?}, not a string");
assert_eq!(name.as_str(), Some("123"));
}
#[test]
fn empty_description_gets_the_deterministic_fallback() {
let wf = tracer_workflow("refund_flow", "");
let skill = wf.as_skill();
let value = parsed_frontmatter(skill.body());
assert_eq!(
value.get("description").and_then(|v| v.as_str()),
Some("Projected from the refund-flow workflow.")
);
}
#[test]
fn resolved_description_is_the_raw_workflow_description() {
let description = "Refund an order: fast path with a \" in it";
let wf = tracer_workflow("refund_flow", description);
let skill = wf.as_skill();
assert_eq!(skill.resolved_description(), description);
assert_eq!(skill.resolved_description(), wf.description());
}
#[test]
fn yaml_double_quoted_escapes_in_order() {
assert_eq!(yaml_double_quoted("plain"), "\"plain\"");
assert_eq!(yaml_double_quoted("a\\b"), "\"a\\\\b\"");
assert_eq!(yaml_double_quoted("a\"b"), "\"a\\\"b\"");
assert_eq!(yaml_double_quoted("a\nb"), "\"a\\nb\"");
assert_eq!(yaml_double_quoted("a\rb"), "\"a\\rb\"");
assert_eq!(yaml_double_quoted("a\tb"), "\"a\\tb\"");
assert_eq!(yaml_double_quoted("a\u{0}b"), "\"a\\x00b\"");
assert_eq!(yaml_double_quoted("a\u{7f}b"), "\"a\\x7fb\"");
assert_eq!(yaml_double_quoted("a\u{2028}b"), "\"a\\u2028b\"");
assert_eq!(yaml_double_quoted("a\u{2029}b"), "\"a\\u2029b\"");
assert_eq!(yaml_double_quoted("héllo — 日本"), "\"héllo — 日本\"");
}
#[test]
fn context_renders_instruction_text_verbatim() {
let body = body_with_instruction(PromptContent::Text("Be careful.".to_string()));
assert!(body.contains("## Context"), "body was:\n{body}");
assert!(
body.contains("Be careful."),
"instruction text must render verbatim; body was:\n{body}"
);
assert!(
!body.contains("Text("),
"a Debug-formatted PromptContent leaked into the body:\n{body}"
);
}
#[test]
fn a_workflow_without_instructions_omits_the_context_heading() {
let body = tracer_workflow("refund_flow", "Process a refund")
.as_skill()
.body()
.to_string();
assert!(
!body.contains("## Context"),
"an empty Context section must not be emitted; body was:\n{body}"
);
}
#[test]
fn an_image_instruction_renders_only_its_mime_type() {
let payload = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=";
let body = body_with_instruction(PromptContent::Image {
data: payload.to_string(),
mime_type: "image/png".to_string(),
});
assert!(
body.contains("image/png"),
"the MIME type must render; body was:\n{body}"
);
assert!(
!body.contains(payload),
"the base64 payload must NEVER reach the body (T-126-03); body was:\n{body}"
);
}
#[test]
fn a_multi_instruction_renders_every_part_joined_by_a_blank_line() {
let parts: Vec<Box<PromptContent>> = vec![
Box::new(PromptContent::Text("First part.".to_string())),
Box::new(PromptContent::ResourceUri("docs://policy".to_string())),
];
let body = body_with_instruction(PromptContent::Multi(parts.into()));
assert!(
body.contains("First part.\n\nRead the resource `docs://policy`."),
"Multi parts must join with a blank line; body was:\n{body}"
);
assert!(
!body.contains("Multi("),
"a Debug-formatted PromptContent leaked into the body:\n{body}"
);
}
#[test]
fn a_tool_handle_instruction_renders_the_tool_name_only() {
let body = body_with_instruction(PromptContent::ToolHandle(ToolHandle::new("orders_get")));
assert!(
body.contains("Uses tool `orders_get`."),
"body was:\n{body}"
);
assert!(
!body.contains("ToolHandle("),
"a Debug-formatted PromptContent leaked into the body:\n{body}"
);
}
#[test]
fn inputs_render_name_description_and_requiredness() {
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.argument("order_id", "The order to refund", true)
.step(WorkflowStep::new("fetch_order", ToolHandle::new("orders_get")).bind("order"))
.as_skill()
.body()
.to_string();
assert!(body.contains("## Inputs"), "body was:\n{body}");
assert!(body.contains("`order_id`"), "body was:\n{body}");
assert!(body.contains("The order to refund"), "body was:\n{body}");
assert!(body.contains("required"), "body was:\n{body}");
}
#[test]
fn an_optional_argument_renders_optional_not_required() {
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.argument("reason", "Why the refund is being issued", false)
.step(WorkflowStep::new("fetch_order", ToolHandle::new("orders_get")).bind("order"))
.as_skill()
.body()
.to_string();
assert!(body.contains("optional"), "body was:\n{body}");
assert!(
!body.contains("required"),
"an optional argument must not render as required; body was:\n{body}"
);
}
#[test]
fn a_workflow_without_arguments_omits_the_inputs_heading() {
let body = tracer_workflow("refund_flow", "Process a refund")
.as_skill()
.body()
.to_string();
assert!(
!body.contains("## Inputs"),
"an empty Inputs section must not be emitted; body was:\n{body}"
);
}
#[test]
fn typed_arguments_render_the_lowercase_wire_spellings() {
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.typed_argument("count", "How many units", true, PromptArgumentType::Integer)
.typed_argument(
"dry_run",
"Whether to simulate",
false,
PromptArgumentType::Boolean,
)
.typed_argument(
"rate",
"The exchange rate",
false,
PromptArgumentType::Number,
)
.typed_argument(
"note",
"A free-text note",
false,
PromptArgumentType::String,
)
.step(WorkflowStep::new("fetch_order", ToolHandle::new("orders_get")).bind("order"))
.as_skill()
.body()
.to_string();
for spelling in ["integer", "boolean", "number", "string"] {
assert!(
body.contains(spelling),
"the `{spelling}` wire spelling must render; body was:\n{body}"
);
}
for debug_spelling in ["Integer", "Boolean", "Number", "String"] {
assert!(
!body.contains(debug_spelling),
"`{debug_spelling}` is PromptArgumentType's Debug spelling and must never \
reach the body; body was:\n{body}"
);
}
}
#[test]
fn step_arguments_render_every_name_and_source() {
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.argument("order_id", "The order to refund", true)
.step(
WorkflowStep::new("fetch_order", ToolHandle::new("orders_get"))
.arg("id", DataSource::prompt_arg("order_id"))
.bind("order"),
)
.step(
WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"))
.arg("amount", DataSource::from_step_field("order", "total"))
.arg("whole", DataSource::from_step("order"))
.bind("refund"),
)
.as_skill()
.body()
.to_string();
assert!(body.contains("`id`"), "body was:\n{body}");
assert!(body.contains("`amount`"), "body was:\n{body}");
assert!(body.contains("`whole`"), "body was:\n{body}");
assert!(
body.contains("the `order_id` input"),
"the PromptArg source must render; body was:\n{body}"
);
assert!(
body.contains("the `total` field of the result of `order`"),
"the StepOutput-with-field source must render; body was:\n{body}"
);
assert!(
body.contains("the result of `order`"),
"the whole-output StepOutput source must render; body was:\n{body}"
);
assert!(
!body.contains("StepOutput"),
"a Debug-formatted DataSource leaked into the body:\n{body}"
);
}
#[test]
fn a_constant_renders_in_construction_order() {
let body = body_with_constant(json!({"b": 2, "a": 1}));
assert!(
body.contains(r#"{"b":2,"a":1}"#),
"serde_json is built with `preserve_order`, so keys must emit in \
construction order; body was:\n{body}"
);
}
#[test]
fn constant_key_order_is_digest_significant() {
let ab = body_with_constant(json!({"a": 1, "b": 2}));
let ba = body_with_constant(json!({"b": 2, "a": 1}));
assert_ne!(
ab, ba,
"constant key order is digest-significant by DESIGN (REVIEWS gemini \
finding 4): the rendered constant documents what will actually be SENT, \
and `preserve_order` means the sent bytes are in construction order too. \
Sorting the render here would make the documentation disagree with the call."
);
}
#[test]
fn template_bindings_render_in_sorted_key_order() {
let step = WorkflowStep::fetch_resources("read_guide")
.with_resource("docs://guide")
.expect("valid resource URI")
.with_template_binding("zeta", DataSource::prompt_arg("z"))
.with_template_binding("alpha", DataSource::prompt_arg("a"));
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.step(step)
.as_skill()
.body()
.to_string();
let alpha = body.find("alpha").expect("alpha binding must render");
let zeta = body.find("zeta").expect("zeta binding must render");
assert!(
alpha < zeta,
"template bindings must render in sorted key order, not insertion order; \
body was:\n{body}"
);
}
#[test]
fn a_resource_only_step_renders_its_heading_and_every_resource() {
let step = WorkflowStep::fetch_resources("read_policy")
.with_resource("docs://refund-policy")
.expect("valid resource URI")
.with_resource("docs://escalation-matrix")
.expect("valid resource URI");
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.step(step)
.as_skill()
.body()
.to_string();
assert!(
body.contains("### Step 1: read_policy"),
"body was:\n{body}"
);
assert!(body.contains("docs://refund-policy"), "body was:\n{body}");
assert!(
body.contains("docs://escalation-matrix"),
"body was:\n{body}"
);
assert!(
!body.contains("Call tool"),
"a resource-only step must not render a tool line; body was:\n{body}"
);
}
#[test]
fn guidance_renders_a_judgment_line() {
let body = SequentialWorkflow::new("refund_flow", "Process a refund")
.step(
WorkflowStep::new("confirm", ToolHandle::new("notify"))
.with_guidance("Confirm with the customer first."),
)
.as_skill()
.body()
.to_string();
assert!(
body.contains("Judgment: Confirm with the customer first."),
"body was:\n{body}"
);
}
#[test]
fn sc3_every_workflow_fact_renders() {
let body = kitchen_sink_workflow().as_skill().body().to_string();
assert!(
body.len() > 400,
"SC-3 anti-vacuity: body was only {} bytes",
body.len()
);
assert!(
body.starts_with("---\nname: \"refund-flow\"\n"),
"fact: the slugified workflow name in the frontmatter `name` key; body was:\n{body}"
);
assert!(
body.contains("Process a customer refund"),
"fact: the workflow description; body was:\n{body}"
);
assert!(
body.contains("`order_id`"),
"fact: argument name `order_id`; body was:\n{body}"
);
assert!(
body.contains("The order to refund"),
"fact: the `order_id` description; body was:\n{body}"
);
assert!(
body.contains("`order_id` (required)"),
"fact: the `required` marker on `order_id`; body was:\n{body}"
);
assert!(
body.contains("`reason`"),
"fact: argument name `reason`; body was:\n{body}"
);
assert!(
body.contains("Why the customer wants a refund"),
"fact: the `reason` description; body was:\n{body}"
);
assert!(
body.contains("`reason` (optional)"),
"fact: the `optional` marker on `reason`; body was:\n{body}"
);
assert!(
body.contains("Never refund more than the original charge."),
"fact: the workflow-level instruction text; body was:\n{body}"
);
assert!(
body.contains("### Step 1: fetch_order"),
"fact: step name `fetch_order`; body was:\n{body}"
);
assert!(
body.contains("### Step 2: issue_refund"),
"fact: step name `issue_refund`; body was:\n{body}"
);
assert!(
body.contains("### Step 3: read_policy"),
"fact: step name `read_policy` (the resource-only step); body was:\n{body}"
);
assert!(
body.contains("Call tool `orders_get`."),
"fact: tool name `orders_get`; body was:\n{body}"
);
assert!(
body.contains("Call tool `payments_refund`."),
"fact: tool name `payments_refund`; body was:\n{body}"
);
assert!(
body.contains("- Argument `id`: the `order_id` input"),
"fact: the `id` binding and its PromptArg source; body was:\n{body}"
);
assert!(
body.contains("- Argument `include_lines`: the constant value `true`"),
"fact: the `include_lines` binding and its Constant source; body was:\n{body}"
);
assert!(
body.contains("- Argument `amount`: the `total` field of the result of `order`"),
"fact: the `amount` binding and its StepOutput source; body was:\n{body}"
);
assert!(
body.contains("Save the result as `order`."),
"fact: the `order` result binding; body was:\n{body}"
);
assert!(
body.contains("Save the result as `refund`."),
"fact: the `refund` result binding; body was:\n{body}"
);
assert!(
body.contains("- Template variable `alpha_currency`: the `reason` input"),
"fact: the `alpha_currency` template binding; body was:\n{body}"
);
assert!(
body.contains(
"- Template variable `zeta_region`: the `region` field of the result of `order`"
),
"fact: the `zeta_region` template binding; body was:\n{body}"
);
assert!(
body.contains("Read the resource `docs://refund-policy`."),
"fact: the attached resource URI; body was:\n{body}"
);
assert!(
body.contains("Judgment: Confirm with the customer before issuing the refund."),
"fact: the `with_guidance` line; body was:\n{body}"
);
}
#[test]
fn sc3_excluded_execution_mechanics_change_no_byte() {
let defaults = kitchen_sink_workflow_with_execution_mechanics(false)
.as_skill()
.body()
.to_string();
let enabled = kitchen_sink_workflow_with_execution_mechanics(true)
.as_skill()
.body()
.to_string();
assert!(
defaults.len() > 400,
"D-11 anti-vacuity: the defaults body was only {} bytes",
defaults.len()
);
assert!(
defaults.starts_with("---\nname: \"refund-flow\"\n"),
"D-11 anti-vacuity: the defaults body did not begin with the expected \
frontmatter; body was:\n{defaults}"
);
assert_eq!(
defaults, enabled,
"D-11: `has_task_support` and `is_retryable` are excluded from the \
render on purpose, so setting both to non-default values must not \
change a single byte"
);
}
#[test]
fn sc3_excluded_accessor_names_are_absent() {
let body = kitchen_sink_workflow_with_execution_mechanics(true)
.as_skill()
.body()
.to_string();
for name in [
"retryable",
"is_retryable",
"task_support",
"has_task_support",
"task support",
] {
assert!(
!body.contains(name),
"the excluded accessor `{name}` must not appear in the body:\n{body}"
);
}
}
#[test]
fn sc3_no_tool_schema_or_description_reaches_the_body() {
let body = kitchen_sink_workflow().as_skill().body().to_string();
for marker in ["inputSchema", "input_schema", "properties", "\"type\":"] {
assert!(
!body.contains(marker),
"D-12: `{marker}` suggests a tool schema reached the body:\n{body}"
);
}
}
#[test]
fn sc3_frontmatter_carries_exactly_two_keys() {
let skill = kitchen_sink_workflow().as_skill();
let value = parsed_frontmatter(skill.body());
let obj = value
.as_object()
.expect("frontmatter must parse to a mapping");
assert_eq!(
obj.len(),
2,
"D-13: frontmatter must carry exactly `name` and `description`, got {obj:?}"
);
assert!(obj.contains_key("name"), "D-13: missing `name`");
assert!(
obj.contains_key("description"),
"D-13: missing `description`"
);
}
fn workflow_with_binding_order(order: [&str; 3]) -> SequentialWorkflow {
let mut step = WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"))
.arg("amount", DataSource::from_step_field("order", "total"))
.bind("refund");
for key in order {
step = step.with_template_binding(key, DataSource::prompt_arg(key));
}
SequentialWorkflow::new("refund_flow", "Process a customer refund")
.argument("order_id", "The order to refund", true)
.step(step)
}
#[test]
fn sc2_binding_insertion_order_does_not_change_bytes() {
let forward = workflow_with_binding_order(["zeta", "middle", "alpha"])
.as_skill()
.body()
.to_string();
let reversed = workflow_with_binding_order(["alpha", "middle", "zeta"])
.as_skill()
.body()
.to_string();
assert!(
forward.contains("- Template variable `alpha`:")
&& forward.contains("- Template variable `middle`:")
&& forward.contains("- Template variable `zeta`:"),
"SC-2 anti-vacuity: all three template bindings must actually render; \
body was:\n{forward}"
);
assert_eq!(
forward, reversed,
"SC-2: template-binding INSERTION order must not reach the rendered \
bytes — `template_bindings()` returns a `&HashMap` and is the one \
nondeterministic accessor in the input surface, so `render_step` \
collects it into a `BTreeMap` first"
);
}
fn first_kitchen_sink_body() -> &'static str {
static FIRST: OnceLock<String> = OnceLock::new();
FIRST.get_or_init(|| kitchen_sink_workflow().as_skill().body().to_string())
}
proptest! {
#[test]
fn prop_sc2_rerender_is_byte_equal(_n in 0..100u32) {
let fresh = kitchen_sink_workflow().as_skill().body().to_string();
prop_assert!(fresh.len() > 400, "anti-vacuity: body was {} bytes", fresh.len());
prop_assert_eq!(fresh.as_str(), first_kitchen_sink_body());
}
}
proptest! {
#[test]
fn prop_sc1_slug_is_agentskills_legal(name in encoder_stressing_text()) {
let skill = SequentialWorkflow::new(name.as_str(), "Process a refund").as_skill();
let slug = skill.name();
prop_assert!(!slug.is_empty(), "slug was empty for name {:?}", name);
prop_assert!(
slug.len() <= MAX_SLUG_LEN,
"slug {:?} is {} chars, over the {} bound",
slug,
slug.len(),
MAX_SLUG_LEN
);
prop_assert!(
slug.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'),
"slug {:?} left the [a-z0-9-] alphabet",
slug
);
prop_assert!(!slug.starts_with('-'), "slug {:?} has a leading hyphen", slug);
prop_assert!(!slug.ends_with('-'), "slug {:?} has a trailing hyphen", slug);
prop_assert!(!slug.contains("--"), "slug {:?} has a doubled hyphen", slug);
}
#[test]
fn prop_sc1_slug_is_deterministic(name in ".*") {
let first = SequentialWorkflow::new(name.as_str(), "Process a refund")
.as_skill()
.name()
.to_string();
let second = SequentialWorkflow::new(name.as_str(), "Process a refund")
.as_skill()
.name()
.to_string();
prop_assert_eq!(first, second);
}
#[test]
fn prop_no_panic_on_arbitrary_text(
name in encoder_stressing_text(),
description in encoder_stressing_text(),
guidance in encoder_stressing_text(),
) {
let wf = SequentialWorkflow::new(name.as_str(), description.as_str())
.argument("arg", description.as_str(), true)
.instruction(InternalPromptMessage::new(
Role::User,
PromptContent::Text(description.clone()),
))
.step(
WorkflowStep::new("step", ToolHandle::new("tool"))
.with_guidance(guidance.as_str())
.bind("out"),
);
let skill = wf.as_skill();
prop_assert!(!skill.body().is_empty());
prop_assert!(skill.body().ends_with('\n'));
}
}
#[test]
fn sc5_prompt_text_equals_body() {
let skill = kitchen_sink_workflow().as_skill();
assert_eq!(
skill.references().count(),
0,
"SC-5 holds only for a reference-free skill; `as_prompt_text()` \
appends every reference body"
);
assert!(
skill.body().ends_with('\n'),
"SC-5 needs the body to end in a newline; `as_prompt_text()` appends one"
);
assert!(
!skill.body().ends_with("\n\n"),
"SC-5 needs EXACTLY one trailing newline"
);
assert!(
skill.body().len() > 200,
"SC-5 anti-vacuity: body was only {} bytes",
skill.body().len()
);
assert_eq!(skill.as_prompt_text(), skill.body());
}
fn procedure_section(body: &str) -> &str {
const HEADING: &str = "## Procedure";
let start = body
.find(HEADING)
.expect("the rendered body must carry a Procedure section");
let rest = &body[start + HEADING.len()..];
rest.find("\n## ").map_or(rest, |end| &rest[..end])
}
#[test]
fn sc5_surface_equivalence_is_set_equality() {
let wf = kitchen_sink_workflow();
let skill = wf.as_skill();
let procedure = procedure_section(skill.body());
assert!(
procedure.contains("### Step 1:"),
"anti-vacuity: the Procedure slice must carry steps; slice was:\n{procedure}"
);
let rendered: BTreeSet<&str> = procedure
.lines()
.filter_map(|line| line.strip_prefix("Call tool `"))
.filter_map(|rest| rest.split_once('`').map(|(name, _)| name))
.collect();
let declared: BTreeSet<&str> = wf
.steps()
.iter()
.filter_map(WorkflowStep::tool)
.map(ToolHandle::name)
.collect();
assert!(
!declared.is_empty(),
"anti-vacuity: the fixture must declare at least one tool"
);
assert!(
rendered.is_subset(&declared),
"the render named a tool the workflow never declared: rendered={rendered:?}, \
declared={declared:?}"
);
assert!(
declared.is_subset(&rendered),
"the render dropped a declared tool: rendered={rendered:?}, \
declared={declared:?}"
);
assert_eq!(rendered, declared);
}
#[test]
fn the_stressing_generator_reaches_every_escape_class() {
use proptest::strategy::ValueTree;
use proptest::test_runner::{Config, TestRunner};
let strategy = encoder_stressing_text();
let mut runner = TestRunner::new(Config::default());
let mut corpus = String::new();
for _ in 0..512 {
corpus.push_str(
&strategy
.new_tree(&mut runner)
.expect("the strategy must produce a value")
.current(),
);
}
for (label, needle) in [
("a newline", '\n'),
("LINE SEPARATOR", '\u{2028}'),
("PARAGRAPH SEPARATOR", '\u{2029}'),
("a backslash", '\\'),
("a double quote", '"'),
("a blank", ' '),
] {
assert!(
corpus.contains(needle),
"the generator never produced {label} ({needle:?}) in 512 draws — \
the properties drawing from it are measuring nothing"
);
}
assert!(
corpus.chars().filter(|c| !c.is_ascii()).count() > 0,
"the arbitrary half of the strategy produced no non-ASCII at all"
);
}
proptest! {
#[test]
fn prop_frontmatter_roundtrips(description in encoder_stressing_text()) {
let wf = tracer_workflow("refund_flow", &description);
let skill = wf.as_skill();
let value = parsed_frontmatter(skill.body());
let obj = value.as_object().expect("frontmatter must parse to a mapping");
prop_assert_eq!(obj.len(), 2);
prop_assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("refund-flow"));
let parsed_description = obj.get("description").and_then(|v| v.as_str());
if description.trim().is_empty() {
prop_assert_eq!(
parsed_description,
Some("Projected from the refund-flow workflow.")
);
} else {
prop_assert_eq!(parsed_description, Some(description.as_str()));
}
}
}
#[test]
fn build_and_as_skill_share_one_renderer() {
let wf = kitchen_sink_workflow();
let output = SkillProjection::new(&wf)
.build()
.expect("a legal name and a non-empty description must build");
assert_eq!(
output.skill.body(),
wf.as_skill().body(),
"D-05: the fallible and infallible entry points must share ONE renderer"
);
assert_eq!(output.skill.name(), wf.as_skill().name());
assert_eq!(
output.skill.resolved_description(),
wf.as_skill().resolved_description()
);
}
#[test]
fn build_without_a_tool_map_emits_no_warnings() {
let wf = kitchen_sink_workflow();
let output = SkillProjection::new(&wf).build().expect("legal workflow");
assert!(
output.warnings.is_empty(),
"D-07: with no tool map there is nothing to check, so nothing to warn about; got {:?}",
output.warnings
);
}
#[test]
fn build_rejects_a_name_that_normalizes_to_nothing() {
let wf = tracer_workflow("!!!", "Process a refund");
let err = SkillProjection::new(&wf)
.build()
.expect_err("D-15: the strict path rejects a name with nothing legal in it");
let message = err.to_string();
assert!(
message.contains("!!!"),
"the error must name the offending workflow; got {message:?}"
);
}
#[test]
fn as_skill_falls_back_where_build_rejects_the_name() {
let wf = tracer_workflow("!!!", "Process a refund");
let skill = wf.as_skill();
let hex = skill
.name()
.strip_prefix("workflow-")
.expect("the infallible path substitutes a `workflow-{8 hex}` slug");
assert_eq!(hex.len(), 8, "slug was {:?}", skill.name());
assert!(
hex.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()),
"slug was {:?}",
skill.name()
);
}
#[test]
fn build_rejects_an_empty_description() {
let wf = tracer_workflow("refund_flow", "");
let err = SkillProjection::new(&wf)
.build()
.expect_err("REVIEWS finding 6: an empty description is rejected by the strict path");
let message = err.to_string();
assert!(
message.contains("description"),
"the error must name the empty description; got {message:?}"
);
assert!(
message.contains("refund_flow"),
"the error must name the offending workflow; got {message:?}"
);
}
#[test]
fn build_rejects_a_whitespace_only_description() {
let wf = tracer_workflow("refund_flow", " \t ");
let err = SkillProjection::new(&wf)
.build()
.expect_err("an all-whitespace description is empty after trimming");
assert!(err.to_string().contains("description"));
}
#[test]
fn as_skill_substitutes_where_build_rejects_the_description() {
let wf = tracer_workflow("refund_flow", "");
let skill = wf.as_skill();
assert_eq!(
skill.resolved_description(),
"Projected from the refund-flow workflow.",
"the pinned substitution from plan 126-01 Task 4"
);
let value = parsed_frontmatter(skill.body());
assert_eq!(
value.get("description").and_then(|v| v.as_str()),
Some("Projected from the refund-flow workflow.")
);
}
#[test]
fn as_skill_substitutes_for_a_whitespace_only_description_too() {
for blank in [" \t ", "\n", " "] {
let wf = tracer_workflow("refund_flow", blank);
let skill = wf.as_skill();
assert_eq!(
skill.resolved_description(),
"Projected from the refund-flow workflow.",
"a blank description must take the same substitution `build()` rejects it for; \
input was {blank:?}"
);
let value = parsed_frontmatter(skill.body());
assert_eq!(
value.get("description").and_then(|v| v.as_str()),
Some("Projected from the refund-flow workflow."),
"input was {blank:?}"
);
assert!(
SkillProjection::new(&wf).build().is_err(),
"the anti-vacuity half: `build()` must still REJECT {blank:?}, so the two entry \
points differ in disposition over one shared condition rather than in the \
condition itself"
);
}
let kept = tracer_workflow("refund_flow", " Process a refund ");
assert_eq!(
kept.as_skill().resolved_description(),
" Process a refund "
);
}
#[test]
fn the_bmp_noncharacters_survive_the_frontmatter_round_trip() {
for description in ["Refund \u{FFFF} orders", "Refund \u{FFFE} orders"] {
let wf = tracer_workflow("refund_flow", description);
let body = wf.as_skill().body().to_string();
let frontmatter_block = body
.split("\n---\n")
.next()
.expect("split always yields a first element");
assert!(
!frontmatter_block.contains('\u{FFFE}') && !frontmatter_block.contains('\u{FFFF}'),
"the noncharacter must be ESCAPED in the frontmatter, not emitted raw: \
{frontmatter_block:?}"
);
let value = parsed_frontmatter(&body);
assert_eq!(
value.get("description").and_then(|v| v.as_str()),
Some(description),
"the escaped form must decode back to the author's exact text"
);
}
}
#[test]
fn with_tools_records_a_map_and_still_builds_the_same_bytes() {
let wf = kitchen_sink_workflow();
let tools = vec![annotated_tool(
"orders_get",
ToolAnnotations::new().with_read_only(true),
)];
let output = SkillProjection::new(&wf)
.with_tools(tools)
.build()
.expect("legal workflow");
assert_eq!(
output.skill.body(),
wf.as_skill().body(),
"supplying a tool map must not move a single rendered byte"
);
}
#[test]
fn projection_output_into_parts_returns_both_halves() {
let wf = kitchen_sink_workflow();
let (skill, warnings) = SkillProjection::new(&wf)
.build()
.expect("legal workflow")
.into_parts();
assert_eq!(skill.name(), "refund-flow");
assert!(warnings.is_empty());
}
fn unannotated_tool(name: &str) -> crate::types::ToolInfo {
crate::types::ToolInfo::new(name, None, json!({ "type": "object" }))
}
fn one_step_workflow(guidance: Option<&str>) -> SequentialWorkflow {
let mut step = WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"));
if let Some(text) = guidance {
step = step.with_guidance(text);
}
SequentialWorkflow::new("refund_flow", "Process a refund").step(step)
}
fn warnings_for(
wf: &SequentialWorkflow,
tools: Vec<crate::types::ToolInfo>,
) -> Vec<ProjectionWarning> {
SkillProjection::new(wf)
.with_tools(tools)
.build()
.expect("legal workflow")
.warnings
}
#[test]
fn gate_fires_for_guidance_on_a_destructive_tool() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)],
);
assert_eq!(warnings.len(), 1, "got {warnings:?}");
assert_eq!(
warnings[0].kind(),
ProjectionWarningKind::GuidanceOnSideEffectingStep
);
assert_eq!(warnings[0].step(), Some("issue_refund"));
assert_eq!(warnings[0].tool(), Some("payments_refund"));
assert!(
warnings[0].message().contains("issue_refund"),
"the message must name the step: {:?}",
warnings[0].message()
);
assert!(
warnings[0].message().contains("regardless"),
"the message must say the step runs regardless of the guidance: {:?}",
warnings[0].message()
);
}
#[test]
fn gate_fires_for_guidance_on_an_explicitly_not_read_only_tool() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_read_only(false),
)],
);
assert_eq!(warnings.len(), 1, "got {warnings:?}");
assert_eq!(
warnings[0].kind(),
ProjectionWarningKind::GuidanceOnSideEffectingStep
);
}
#[test]
fn gate_stays_silent_for_guidance_on_a_read_only_tool() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_read_only(true),
)],
);
assert_eq!(warnings.len(), 0, "got {warnings:?}");
}
#[test]
fn an_unannotated_tool_is_unverifiable_not_a_gate_finding() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let warnings = warnings_for(&wf, vec![unannotated_tool("payments_refund")]);
assert_eq!(warnings.len(), 1, "got {warnings:?}");
assert_eq!(
warnings[0].kind(),
ProjectionWarningKind::GateCheckUnverifiable,
"D-08: MCP's literal annotation defaults are NOT followed"
);
assert_eq!(
warnings
.iter()
.filter(|w| w.kind() == ProjectionWarningKind::GuidanceOnSideEffectingStep)
.count(),
0
);
}
#[test]
fn a_tool_absent_from_the_map_is_unverifiable() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let warnings = warnings_for(
&wf,
vec![annotated_tool("orders_get", ToolAnnotations::new())],
);
assert_eq!(warnings.len(), 1, "got {warnings:?}");
assert_eq!(
warnings[0].kind(),
ProjectionWarningKind::GateCheckUnverifiable
);
assert_eq!(warnings[0].tool(), Some("payments_refund"));
}
#[test]
fn a_destructive_tool_without_guidance_produces_nothing() {
let wf = one_step_workflow(None);
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)],
);
assert_eq!(
warnings.len(),
0,
"the trigger is guidance AND a side effect, never a side effect alone; got {warnings:?}"
);
}
#[test]
fn a_resource_only_step_with_guidance_produces_nothing() {
let wf = SequentialWorkflow::new("refund_flow", "Process a refund").step(
WorkflowStep::fetch_resources("read_policy")
.with_resource("docs://refund-policy")
.expect("valid resource URI")
.with_guidance("Read the policy before deciding."),
);
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)],
);
assert_eq!(
warnings.len(),
0,
"a step that calls no tool can carry no annotations; got {warnings:?}"
);
}
#[test]
fn without_a_tool_map_nothing_warns_even_for_a_tripping_workflow() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let with_map = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)],
);
assert_eq!(with_map.len(), 1, "anti-vacuity: the fixture must trip");
let without_map = SkillProjection::new(&wf)
.build()
.expect("legal workflow")
.warnings;
assert_eq!(
without_map.len(),
0,
"D-07: no tool map means the check cannot run at all; got {without_map:?}"
);
}
#[test]
fn two_side_effecting_guidance_bearing_steps_produce_exactly_two_warnings() {
let wf = SequentialWorkflow::new("refund_flow", "Process a refund")
.step(
WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"))
.with_guidance("Confirm with the customer first."),
)
.step(
WorkflowStep::new("close_order", ToolHandle::new("orders_close"))
.with_guidance("Only close once the refund settled."),
)
.step(WorkflowStep::new(
"fetch_order",
ToolHandle::new("orders_get"),
));
let warnings = warnings_for(
&wf,
vec![
annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
),
annotated_tool("orders_close", ToolAnnotations::new().with_read_only(false)),
annotated_tool("orders_get", ToolAnnotations::new().with_read_only(true)),
],
);
assert_eq!(warnings.len(), 2, "got {warnings:?}");
assert!(warnings
.iter()
.all(|w| w.kind() == ProjectionWarningKind::GuidanceOnSideEffectingStep));
let steps: Vec<_> = warnings
.iter()
.filter_map(ProjectionWarning::step)
.collect();
assert_eq!(steps, vec!["issue_refund", "close_order"]);
}
#[test]
fn an_author_supplied_name_is_neutralized_before_it_reaches_a_log_field() {
let hostile = "refund\u{1b}[2Kflow\ninjected: line";
let sanitized = sanitize_for_log(hostile);
assert!(!sanitized.contains('\n'), "newline survived: {sanitized:?}");
assert!(!sanitized.contains('\u{1b}'), "ESC survived: {sanitized:?}");
assert!(sanitized.contains("refund"), "content was destroyed");
assert_eq!(
sanitized.matches('\u{fffd}').count(),
2,
"each control character maps to exactly one U+FFFD: {sanitized:?}"
);
}
#[test]
fn build_returns_warning_kinds_and_counts_as_data() {
let wf = SequentialWorkflow::new("refund_flow", "Process a refund")
.step(
WorkflowStep::new("issue_refund", ToolHandle::new("payments_refund"))
.with_guidance("Confirm with the customer first."),
)
.step(
WorkflowStep::new("archive_order", ToolHandle::new("orders_archive"))
.with_guidance("Only archive settled orders."),
);
let warnings = warnings_for(
&wf,
vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)],
);
assert_eq!(warnings.len(), 2, "got {warnings:?}");
let kinds: Vec<_> = warnings.iter().map(ProjectionWarning::kind).collect();
assert_eq!(
kinds,
vec![
ProjectionWarningKind::GuidanceOnSideEffectingStep,
ProjectionWarningKind::GateCheckUnverifiable,
]
);
}
#[test]
fn the_warning_is_a_builder_capability_and_the_bytes_are_identical_either_way() {
let wf = one_step_workflow(Some("Confirm with the customer first."));
let output = SkillProjection::new(&wf)
.with_tools(vec![annotated_tool(
"payments_refund",
ToolAnnotations::new().with_destructive(true),
)])
.build()
.expect("legal workflow");
assert!(
output
.warnings
.iter()
.any(|w| w.kind() == ProjectionWarningKind::GuidanceOnSideEffectingStep),
"anti-vacuity: the fixture must trip the gate"
);
assert_eq!(
output.skill.body(),
wf.as_skill().body(),
"the warning is a builder-path capability; the bytes are identical either way"
);
}
}