#![warn(missing_docs)]
use mlua_flow_ir::Node as FlowNode;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
pub const CURRENT_SCHEMA_VERSION: &str = "0.1.0";
fn default_schema_version() -> semver::Version {
current_schema_version()
}
pub fn current_schema_version() -> semver::Version {
semver::Version::parse(CURRENT_SCHEMA_VERSION)
.expect("CURRENT_SCHEMA_VERSION must be valid semver")
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(transparent)]
pub struct BlueprintId(String);
impl BlueprintId {
pub const MAIN: &'static str = "main";
pub fn main() -> Self {
Self(Self::MAIN.to_string())
}
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for BlueprintId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for BlueprintId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for BlueprintId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[cfg(test)]
mod blueprint_id_tests {
use super::*;
#[test]
fn blueprint_id_field_schema_stays_a_plain_inline_string() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
let id = &v["properties"]["id"];
assert_eq!(id["type"], "string", "id must stay a plain string: {id}");
assert!(id.get("$ref").is_none(), "id must not become a $ref: {id}");
}
#[test]
fn blueprint_id_serde_is_transparent() {
let id = BlueprintId::new("coding");
assert_eq!(
serde_json::to_value(&id).unwrap(),
serde_json::json!("coding")
);
let back: BlueprintId = serde_json::from_value(serde_json::json!("coding")).unwrap();
assert_eq!(back, id);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Blueprint {
#[serde(default = "default_schema_version")]
#[schemars(with = "String")]
pub schema_version: semver::Version,
#[schemars(with = "String")]
pub id: BlueprintId,
#[schemars(with = "Value")]
pub flow: FlowNode,
#[serde(default)]
pub agents: Vec<AgentDef>,
#[serde(default)]
pub operators: Vec<OperatorDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub metas: Vec<MetaDef>,
#[serde(default)]
pub hints: CompilerHints,
#[serde(default)]
pub strategy: CompilerStrategy,
#[serde(default)]
pub metadata: BlueprintMetadata,
#[serde(default)]
pub spawner_hints: SpawnerHints,
#[serde(default = "default_global_agent_kind")]
pub default_agent_kind: AgentKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_operator_kind: Option<OperatorKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<Value>")]
pub default_init_ctx: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<Value>")]
pub default_agent_ctx: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_context_policy: Option<ContextPolicy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection_placement: Option<ProjectionPlacementSpec>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ContextPolicy {
#[serde(default)]
pub include: Option<Vec<String>>,
#[serde(default)]
pub exclude: Vec<String>,
#[serde(default)]
pub steps: Option<Vec<String>>,
#[serde(default)]
pub steps_exclude: Vec<String>,
}
impl ContextPolicy {
pub fn allows(&self, name: &str) -> bool {
if self.exclude.iter().any(|excluded| excluded == name) {
return false;
}
match &self.include {
Some(list) => list.iter().any(|included| included == name),
None => true,
}
}
pub fn allows_step(&self, name: &str) -> bool {
if self.steps_exclude.iter().any(|excluded| excluded == name) {
return false;
}
match &self.steps {
Some(list) => list.iter().any(|included| included == name),
None => true,
}
}
}
pub fn default_global_agent_kind() -> AgentKind {
AgentKind::Operator
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SpawnerHints {
#[serde(default)]
pub layers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentDef {
pub name: String,
pub kind: AgentKind,
#[serde(default)]
pub spec: Value,
#[serde(default)]
pub profile: Option<AgentProfile>,
#[serde(default)]
pub meta: Option<AgentMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentProfile {
#[serde(default)]
pub system_prompt: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub effort: Option<String>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub extras: Value,
#[serde(default)]
pub version_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_binding: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AgentKind {
Lua,
RustFn,
AgentBlock,
Subprocess,
Operator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OperatorKind {
MainAi,
#[default]
Automate,
Composite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct OperatorDef {
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub kind: Option<OperatorKind>,
#[serde(default)]
pub spec: Value,
#[serde(default)]
pub profile: Option<AgentProfile>,
#[serde(default)]
pub meta: Option<AgentMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MetaDef {
pub name: String,
pub ctx: Value,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProjectionPlacementSpec {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dir_template: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AgentMeta {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(with = "Option<Value>")]
pub ctx: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_policy: Option<ContextPolicy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub meta_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub projection_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CompilerHints {
#[serde(default)]
pub per_agent: HashMap<String, Value>,
#[serde(default)]
pub global: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CompilerStrategy {
#[serde(default = "default_true")]
pub strict_refs: bool,
#[serde(default = "default_true")]
pub strict_kind: bool,
}
fn default_true() -> bool {
true
}
impl Default for CompilerStrategy {
fn default() -> Self {
Self {
strict_refs: true,
strict_kind: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BlueprintMetadata {
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub origin: BlueprintOrigin,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_name_alias: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_run_ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BlueprintOrigin {
#[default]
Inline,
File {
path: String,
},
Algo {
session_id: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_version_default_parses() {
let v = default_schema_version();
assert_eq!(v.to_string(), "0.1.0");
}
#[test]
fn current_schema_version_const_matches() {
assert_eq!(CURRENT_SCHEMA_VERSION, "0.1.0");
}
#[test]
fn blueprint_json_schema_exports_key_properties() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
let props = v["properties"].as_object().expect("object schema");
for key in [
"schema_version",
"id",
"flow",
"agents",
"operators",
"metas",
"hints",
"strategy",
"metadata",
"spawner_hints",
"default_agent_kind",
"default_operator_kind",
"default_init_ctx",
"default_agent_ctx",
"default_context_policy",
"projection_placement",
] {
assert!(props.contains_key(key), "missing property: {key}");
}
assert_eq!(v["properties"]["schema_version"]["type"], "string");
let dump = v.to_string();
assert!(dump.contains("agent_block"), "AgentKind variants in schema");
assert!(dump.contains("main_ai"), "OperatorKind variants in schema");
assert!(dump.contains("AgentDef"), "AgentDef definition in schema");
}
#[test]
fn agent_profile_worker_binding_roundtrips_when_some() {
let profile = AgentProfile {
worker_binding: Some("mse-worker-coder".to_string()),
..Default::default()
};
let json = serde_json::to_value(&profile).expect("serializes");
assert_eq!(json["worker_binding"], "mse-worker-coder");
let back: AgentProfile = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.worker_binding.as_deref(), Some("mse-worker-coder"));
}
#[test]
fn agent_profile_worker_binding_omitted_when_none() {
let profile = AgentProfile::default();
let json = serde_json::to_value(&profile).expect("serializes");
assert!(
json.as_object().unwrap().get("worker_binding").is_none(),
"worker_binding key must be absent when None: {json}"
);
let back: AgentProfile = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.worker_binding, None);
}
fn minimal_bp(default_init_ctx: Option<Value>) -> Blueprint {
Blueprint {
schema_version: current_schema_version(),
id: "bp-init-ctx-ut".into(),
flow: FlowNode::Seq { children: vec![] },
agents: vec![],
operators: vec![],
metas: vec![],
hints: Default::default(),
strategy: Default::default(),
metadata: Default::default(),
spawner_hints: Default::default(),
default_agent_kind: AgentKind::Operator,
default_operator_kind: None,
default_init_ctx,
default_agent_ctx: None,
default_context_policy: None,
projection_placement: None,
}
}
#[test]
fn blueprint_default_init_ctx_roundtrips_when_some() {
let bp = minimal_bp(Some(serde_json::json!({ "seeded": true })));
let json = serde_json::to_string(&bp).expect("serializes");
let back: Blueprint = serde_json::from_str(&json).expect("deserializes");
assert_eq!(
back.default_init_ctx,
Some(serde_json::json!({ "seeded": true }))
);
assert_eq!(bp, back);
}
#[test]
fn blueprint_default_init_ctx_omitted_when_none() {
let bp = minimal_bp(None);
let json = serde_json::to_value(&bp).expect("serializes");
assert!(
json.as_object().unwrap().get("default_init_ctx").is_none(),
"default_init_ctx key must be absent when None: {json}"
);
let back: Blueprint = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.default_init_ctx, None);
assert_eq!(bp, back);
}
#[test]
fn blueprint_json_schema_exports_default_init_ctx_as_nullable_value() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
assert!(
v["properties"]["default_init_ctx"].is_object(),
"default_init_ctx must appear in the exported schema: {v}"
);
}
#[test]
fn blueprint_default_agent_ctx_and_context_policy_roundtrip_when_some() {
let mut bp = minimal_bp(None);
bp.default_agent_ctx = Some(serde_json::json!({ "org_conventions": "x" }));
bp.default_context_policy = Some(ContextPolicy {
include: Some(vec!["project_root".to_string()]),
exclude: vec!["work_dir".to_string()],
..Default::default()
});
let json = serde_json::to_string(&bp).expect("serializes");
let back: Blueprint = serde_json::from_str(&json).expect("deserializes");
assert_eq!(bp, back);
assert_eq!(
back.default_agent_ctx,
Some(serde_json::json!({ "org_conventions": "x" }))
);
assert_eq!(
back.default_context_policy,
Some(ContextPolicy {
include: Some(vec!["project_root".to_string()]),
exclude: vec!["work_dir".to_string()],
..Default::default()
})
);
}
#[test]
fn blueprint_default_agent_ctx_and_context_policy_omitted_when_none() {
let bp = minimal_bp(None);
let json = serde_json::to_value(&bp).expect("serializes");
let obj = json.as_object().unwrap();
assert!(
obj.get("default_agent_ctx").is_none(),
"default_agent_ctx key must be absent when None: {json}"
);
assert!(
obj.get("default_context_policy").is_none(),
"default_context_policy key must be absent when None: {json}"
);
let back: Blueprint = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.default_agent_ctx, None);
assert_eq!(back.default_context_policy, None);
assert_eq!(bp, back);
}
#[test]
fn blueprint_json_schema_exports_agent_ctx_and_context_policy() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
assert!(
v["properties"]["default_agent_ctx"].is_object(),
"default_agent_ctx must appear in the exported schema: {v}"
);
assert!(
v["properties"]["default_context_policy"].is_object(),
"default_context_policy must appear in the exported schema: {v}"
);
}
#[test]
fn blueprint_projection_placement_roundtrips_when_some() {
let mut bp = minimal_bp(None);
bp.projection_placement = Some(ProjectionPlacementSpec {
root: Some("project_root".to_string()),
dir_template: Some("custom/{task_id}/out".to_string()),
});
let json = serde_json::to_string(&bp).expect("serializes");
let back: Blueprint = serde_json::from_str(&json).expect("deserializes");
assert_eq!(bp, back);
assert_eq!(
back.projection_placement,
Some(ProjectionPlacementSpec {
root: Some("project_root".to_string()),
dir_template: Some("custom/{task_id}/out".to_string()),
})
);
}
#[test]
fn blueprint_projection_placement_omitted_when_none() {
let bp = minimal_bp(None);
let json = serde_json::to_value(&bp).expect("serializes");
assert!(
json.as_object()
.unwrap()
.get("projection_placement")
.is_none(),
"projection_placement key must be absent when None: {json}"
);
let back: Blueprint = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.projection_placement, None);
assert_eq!(bp, back);
}
#[test]
fn blueprint_json_schema_exports_projection_placement() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
assert!(
v["properties"]["projection_placement"].is_object(),
"projection_placement must appear in the exported schema: {v}"
);
}
#[test]
fn agent_meta_ctx_and_context_policy_roundtrip_when_some() {
let meta = AgentMeta {
ctx: Some(serde_json::json!({ "k": "v" })),
context_policy: Some(ContextPolicy {
include: None,
exclude: vec!["run_id".to_string()],
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_value(&meta).expect("serializes");
let back: AgentMeta = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, meta);
}
#[test]
fn agent_meta_ctx_and_context_policy_omitted_when_none() {
let meta = AgentMeta::default();
let json = serde_json::to_value(&meta).expect("serializes");
let obj = json.as_object().unwrap();
assert!(
obj.get("ctx").is_none(),
"ctx key must be absent when None: {json}"
);
assert!(
obj.get("context_policy").is_none(),
"context_policy key must be absent when None: {json}"
);
}
#[test]
fn agent_meta_json_schema_exports_ctx_context_policy_and_meta_ref() {
let schema = schemars::schema_for!(AgentMeta);
let v = serde_json::to_value(&schema).expect("schema serializes");
let props = v["properties"].as_object().expect("object schema");
for key in [
"description",
"version",
"tags",
"ctx",
"context_policy",
"meta_ref",
"projection_name",
] {
assert!(props.contains_key(key), "missing property: {key}");
}
}
#[test]
fn meta_def_roundtrips_through_json() {
let def = MetaDef {
name: "heavy-scan".to_string(),
ctx: serde_json::json!({ "work_dir": "/x" }),
};
let json = serde_json::to_value(&def).expect("serializes");
let back: MetaDef = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, def);
}
#[test]
fn blueprint_metas_omitted_when_empty() {
let bp = minimal_bp(None);
let json = serde_json::to_value(&bp).expect("serializes");
assert!(
json.as_object().unwrap().get("metas").is_none(),
"metas key must be absent when empty: {json}"
);
let back: Blueprint = serde_json::from_value(json).expect("deserializes");
assert!(back.metas.is_empty());
assert_eq!(bp, back);
}
#[test]
fn blueprint_metas_roundtrips_when_non_empty() {
let mut bp = minimal_bp(None);
bp.metas = vec![MetaDef {
name: "heavy-scan".to_string(),
ctx: serde_json::json!({ "work_dir": "/x" }),
}];
let json = serde_json::to_string(&bp).expect("serializes");
let back: Blueprint = serde_json::from_str(&json).expect("deserializes");
assert_eq!(bp, back);
assert_eq!(back.metas.len(), 1);
assert_eq!(back.metas[0].name, "heavy-scan");
}
#[test]
fn blueprint_json_schema_exports_metas() {
let schema = schemars::schema_for!(Blueprint);
let v = serde_json::to_value(&schema).expect("schema serializes");
assert!(
v["properties"]["metas"].is_object(),
"metas must appear in the exported schema: {v}"
);
let dump = v.to_string();
assert!(dump.contains("MetaDef"), "MetaDef definition in schema");
}
#[test]
fn agent_meta_meta_ref_roundtrips_when_some() {
let meta = AgentMeta {
meta_ref: Some("heavy-scan".to_string()),
..Default::default()
};
let json = serde_json::to_value(&meta).expect("serializes");
assert_eq!(json["meta_ref"], "heavy-scan");
let back: AgentMeta = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, meta);
}
#[test]
fn agent_meta_meta_ref_omitted_when_none() {
let meta = AgentMeta::default();
let json = serde_json::to_value(&meta).expect("serializes");
assert!(
json.as_object().unwrap().get("meta_ref").is_none(),
"meta_ref key must be absent when None: {json}"
);
let back: AgentMeta = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.meta_ref, None);
}
#[test]
fn agent_meta_projection_name_roundtrips_when_some() {
let meta = AgentMeta {
projection_name: Some("plan".to_string()),
..Default::default()
};
let json = serde_json::to_value(&meta).expect("serializes");
assert_eq!(json["projection_name"], "plan");
let back: AgentMeta = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, meta);
}
#[test]
fn agent_meta_projection_name_omitted_when_none() {
let meta = AgentMeta::default();
let json = serde_json::to_value(&meta).expect("serializes");
assert!(
json.as_object().unwrap().get("projection_name").is_none(),
"projection_name key must be absent when None: {json}"
);
let back: AgentMeta = serde_json::from_value(json).expect("deserializes");
assert_eq!(back.projection_name, None);
assert_eq!(back, meta);
}
#[test]
fn agent_meta_rejects_unknown_field_with_projection_name_present() {
let json = serde_json::json!({
"projection_name": "plan",
"not_a_real_field": true
});
let err = serde_json::from_value::<AgentMeta>(json).unwrap_err();
assert!(
err.to_string().contains("not_a_real_field")
|| err.to_string().contains("unknown field"),
"expected an unknown-field rejection, got: {err}"
);
}
#[test]
fn context_policy_default_allows_everything() {
let policy = ContextPolicy::default();
assert!(policy.allows("project_root"));
assert!(policy.allows("anything"));
}
#[test]
fn context_policy_include_only_allows_listed_names() {
let policy = ContextPolicy {
include: Some(vec!["project_root".to_string()]),
exclude: vec![],
..Default::default()
};
assert!(policy.allows("project_root"));
assert!(!policy.allows("work_dir"));
}
#[test]
fn context_policy_exclude_wins_over_include() {
let policy = ContextPolicy {
include: Some(vec!["project_root".to_string()]),
exclude: vec!["project_root".to_string()],
..Default::default()
};
assert!(!policy.allows("project_root"));
}
#[test]
fn context_policy_roundtrips_through_json() {
let policy = ContextPolicy {
include: Some(vec!["a".to_string(), "b".to_string()]),
exclude: vec!["c".to_string()],
..Default::default()
};
let json = serde_json::to_value(&policy).expect("serializes");
let back: ContextPolicy = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, policy);
}
#[test]
fn context_policy_default_roundtrips_as_empty_object() {
let policy = ContextPolicy::default();
let json = serde_json::to_value(&policy).expect("serializes");
assert_eq!(
json,
serde_json::json!({
"include": null,
"exclude": [],
"steps": null,
"steps_exclude": [],
})
);
let back: ContextPolicy = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, policy);
}
#[test]
fn context_policy_steps_default_allows_every_step() {
let policy = ContextPolicy::default();
assert!(policy.allows_step("planner"));
assert!(policy.allows_step("anything"));
}
#[test]
fn context_policy_steps_include_only_allows_listed_names() {
let policy = ContextPolicy {
steps: Some(vec!["planner".to_string()]),
..Default::default()
};
assert!(policy.allows_step("planner"));
assert!(!policy.allows_step("coder"));
}
#[test]
fn context_policy_steps_empty_list_allows_none() {
let policy = ContextPolicy {
steps: Some(vec![]),
..Default::default()
};
assert!(!policy.allows_step("planner"));
}
#[test]
fn context_policy_steps_exclude_wins_over_steps() {
let policy = ContextPolicy {
steps: Some(vec!["planner".to_string()]),
steps_exclude: vec!["planner".to_string()],
..Default::default()
};
assert!(!policy.allows_step("planner"));
}
#[test]
fn context_policy_steps_roundtrips_through_json() {
let policy = ContextPolicy {
steps: Some(vec!["planner".to_string(), "coder".to_string()]),
steps_exclude: vec!["reviewer".to_string()],
..Default::default()
};
let json = serde_json::to_value(&policy).expect("serializes");
let back: ContextPolicy = serde_json::from_value(json).expect("deserializes");
assert_eq!(back, policy);
}
}