1use crate::application::VersionSelector;
16use crate::blueprint::store::BlueprintId;
17use crate::blueprint::{AgentDef, Blueprint};
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct EnhanceSetting {
24 pub id: String,
26 pub blueprint_id: BlueprintId,
28 pub ttl_secs: u64,
30 #[serde(default)]
33 pub version: VersionSelector,
34 #[serde(default = "default_verifier_axes")]
40 pub verifier_axes: Vec<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub spawner: Option<AgentDef>,
51 #[serde(default)]
53 pub meta: EnhanceSettingMeta,
54}
55
56fn default_verifier_axes() -> Vec<String> {
57 vec![
58 "des".to_string(),
59 "canonical".to_string(),
60 "noop".to_string(),
61 "agent-ref".to_string(),
62 ]
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct EnhanceSettingInput {
70 pub id: String,
72 pub blueprint: Blueprint,
75 pub ttl_secs: u64,
77 #[serde(default)]
79 pub version: VersionSelector,
80 #[serde(default = "default_verifier_axes")]
83 pub verifier_axes: Vec<String>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub spawner: Option<AgentDef>,
92 #[serde(default)]
94 pub meta: EnhanceSettingMeta,
95}
96
97impl EnhanceSettingInput {
98 pub fn into_ref(self) -> (Blueprint, EnhanceSetting) {
102 let blueprint_id = self.blueprint.id.clone();
103 (
104 self.blueprint,
105 EnhanceSetting {
106 id: self.id,
107 blueprint_id,
108 ttl_secs: self.ttl_secs,
109 version: self.version,
110 verifier_axes: self.verifier_axes,
111 spawner: self.spawner,
112 meta: self.meta,
113 },
114 )
115 }
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct EnhanceSettingMeta {}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::enhance::blueprint::default_blueprint;
127
128 #[test]
129 fn default_verifier_axes_has_4_canonical_axes() {
130 let axes = default_verifier_axes();
131 assert_eq!(axes, vec!["des", "canonical", "noop", "agent-ref"]);
132 }
133
134 #[test]
135 fn input_into_ref_splits_blueprint_and_setting() {
136 let bp = default_blueprint();
137 let bp_id = bp.id.clone();
138 let input = EnhanceSettingInput {
139 id: "s1".into(),
140 blueprint: bp,
141 ttl_secs: 60,
142 version: VersionSelector::default(),
143 verifier_axes: default_verifier_axes(),
144 spawner: None,
145 meta: EnhanceSettingMeta::default(),
146 };
147 let (split_bp, setting) = input.into_ref();
148 assert_eq!(setting.id, "s1");
149 assert_eq!(setting.blueprint_id, bp_id);
150 assert_eq!(setting.ttl_secs, 60);
151 assert_eq!(setting.verifier_axes.len(), 4);
152 assert_eq!(split_bp.id, bp_id);
153 }
154
155 #[test]
156 fn setting_serde_roundtrip_preserves_verifier_axes() {
157 let bp_id = BlueprintId::new("bp-xyz".to_string());
158 let s = EnhanceSetting {
159 id: "s2".into(),
160 blueprint_id: bp_id,
161 ttl_secs: 30,
162 version: VersionSelector::default(),
163 verifier_axes: vec!["des".into(), "noop".into()],
164 spawner: None,
165 meta: EnhanceSettingMeta::default(),
166 };
167 let j = serde_json::to_value(&s).unwrap();
168 let s2: EnhanceSetting = serde_json::from_value(j).unwrap();
169 assert_eq!(s2.verifier_axes, vec!["des", "noop"]);
170 assert_eq!(s2.ttl_secs, 30);
171 }
172
173 #[test]
174 fn setting_deserialize_applies_default_verifier_axes_when_omitted() {
175 let json = serde_json::json!({
176 "id": "s3",
177 "blueprint_id": "bp-1",
178 "ttl_secs": 10,
179 });
180 let s: EnhanceSetting = serde_json::from_value(json).unwrap();
181 assert_eq!(s.verifier_axes, default_verifier_axes());
182 }
183
184 #[test]
185 fn setting_deserialize_without_spawner_is_none_and_omits_it_on_serialize() {
186 let json = serde_json::json!({
189 "id": "s4",
190 "blueprint_id": "bp-1",
191 "ttl_secs": 10,
192 });
193 let s: EnhanceSetting = serde_json::from_value(json).unwrap();
194 assert!(s.spawner.is_none());
195 let back = serde_json::to_value(&s).unwrap();
196 assert!(back.get("spawner").is_none());
197 }
198
199 #[test]
200 fn input_into_ref_carries_spawner_override_to_the_setting() {
201 let bp = default_blueprint();
202 let spawner: AgentDef = serde_json::from_value(serde_json::json!({
203 "name": "patch-spawner",
204 "kind": "subprocess",
205 "spec": { "program": "true", "args": [] },
206 }))
207 .unwrap();
208 let input = EnhanceSettingInput {
209 id: "s5".into(),
210 blueprint: bp,
211 ttl_secs: 60,
212 version: VersionSelector::default(),
213 verifier_axes: default_verifier_axes(),
214 spawner: Some(spawner.clone()),
215 meta: EnhanceSettingMeta::default(),
216 };
217 let (split_bp, setting) = input.into_ref();
218 assert_eq!(setting.spawner.as_ref(), Some(&spawner));
219 let bp_spawner = split_bp
222 .agents
223 .iter()
224 .find(|a| a.name == "patch-spawner")
225 .expect("default blueprint declares a patch-spawner agent");
226 assert_ne!(bp_spawner, &spawner);
227 }
228}