1use crate::ast::*;
5
6fn yaml_safe_string(s: &str) -> String {
11 if s.is_empty() {
12 return "\"\"".to_string();
13 }
14
15 let needs_quoting = s == "-"
17 || s == "~"
18 || s == "null"
19 || s == "true"
20 || s == "false"
21 || s.starts_with('-')
22 || s.starts_with('!')
23 || s.starts_with('&')
24 || s.starts_with('*')
25 || s.starts_with('>')
26 || s.starts_with('|')
27 || s.starts_with('{')
28 || s.starts_with('[')
29 || s.starts_with(':')
30 || s.starts_with('#')
31 || s.starts_with('@')
32 || s.starts_with('`')
33 || s.contains(':')
34 || s.contains('\n')
35 || s.parse::<f64>().is_ok(); if needs_quoting {
38 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
40 } else {
41 s.to_string()
42 }
43}
44
45pub fn ast_to_markdown(ast: &CellstateAst) -> String {
52 let mut output = String::new();
53
54 let mut adapters = Vec::new();
56 let mut memories = Vec::new();
57 let mut policies = Vec::new();
58 let mut injections = Vec::new();
59 let mut providers = Vec::new();
60 let mut caches = Vec::new();
61 let mut trajectories = Vec::new();
62 let mut agents = Vec::new();
63 let mut intents = Vec::new();
64
65 for def in &ast.definitions {
66 match def {
67 Definition::Adapter(a) => adapters.push(a),
68 Definition::Memory(m) => memories.push(m),
69 Definition::Policy(p) => policies.push(p),
70 Definition::Injection(i) => injections.push(i),
71 Definition::Provider(prov) => providers.push(prov),
72 Definition::Cache(c) => caches.push(c),
73 Definition::Trajectory(t) => trajectories.push(t),
74 Definition::Agent(a) => agents.push(a),
75 Definition::Intent(i) => intents.push(i),
76 Definition::Evolution(_) | Definition::SummarizationPolicy(_) => {}
80 }
81 }
82
83 adapters.sort_by(|a, b| a.name.cmp(&b.name));
85 memories.sort_by(|a, b| a.name.cmp(&b.name));
86 policies.sort_by(|a, b| a.name.cmp(&b.name));
87 providers.sort_by(|a, b| a.name.cmp(&b.name));
88 trajectories.sort_by(|a, b| a.name.cmp(&b.name));
89 agents.sort_by(|a, b| a.name.cmp(&b.name));
90 intents.sort_by(|a, b| a.name.cmp(&b.name));
91 caches.sort_by(|a, b| a.backend.cmp(&b.backend)); injections.sort_by(|a, b| {
94 a.source
95 .cmp(&b.source)
96 .then_with(|| a.target.cmp(&b.target))
97 });
98
99 for adapter in adapters {
101 output.push_str(&format!("```adapter {}\n", adapter.name));
102 output.push_str(&format!("adapter_type: {}\n", adapter.adapter_type));
103 output.push_str(&format!(
104 "connection: {}\n",
105 yaml_safe_string(&adapter.connection)
106 ));
107 if !adapter.options.is_empty() {
108 output.push_str("options:\n");
109 for (k, v) in &adapter.options {
110 output.push_str(&format!(" {}: {}\n", k, yaml_safe_string(v)));
111 }
112 }
113 output.push_str("```\n\n");
114 }
115
116 for provider in providers {
117 output.push_str(&format!("```provider {}\n", provider.name));
118 output.push_str(&format!("provider_type: {}\n", provider.provider_type));
119 output.push_str(&format!(
120 "api_key: {}\n",
121 env_value_to_string(&provider.api_key)
122 ));
123 output.push_str(&format!("model: {}\n", yaml_safe_string(&provider.model)));
124 if !provider.options.is_empty() {
125 output.push_str("options:\n");
126 for (k, v) in &provider.options {
127 output.push_str(&format!(" {}: {}\n", k, yaml_safe_string(v)));
128 }
129 }
130 output.push_str("```\n\n");
131 }
132
133 for memory in memories {
134 output.push_str(&format!("```memory {}\n", memory.name));
135 output.push_str(&format!("memory_type: {}\n", memory.memory_type));
136 output.push_str("schema:\n");
137 for field in &memory.schema {
138 output.push_str(&format!(" - name: {}\n", field.name));
139 output.push_str(&format!(" type: {}\n", field.field_type));
140 output.push_str(&format!(" nullable: {}\n", field.nullable));
141 if let Some(default) = &field.default {
142 output.push_str(&format!(" default: {}\n", yaml_safe_string(default)));
143 }
144 }
145 output.push_str(&format!("retention: {}\n", memory.retention));
146 output.push_str(&format!("lifecycle: {}\n", memory.lifecycle));
147 if let Some(parent) = &memory.parent {
148 output.push_str(&format!("parent: {}\n", parent));
149 }
150 if !memory.indexes.is_empty() {
151 output.push_str("indexes:\n");
152 for index in &memory.indexes {
153 output.push_str(&format!(" - field: {}\n", index.field));
154 output.push_str(&format!(" type: {}\n", index.index_type));
155 }
156 }
157 if !memory.inject_on.is_empty() {
158 output.push_str("inject_on:\n");
159 for trigger in &memory.inject_on {
160 output.push_str(&format!(" - {}\n", trigger));
161 }
162 }
163 if !memory.artifacts.is_empty() {
164 output.push_str("artifacts:\n");
165 for artifact in &memory.artifacts {
166 output.push_str(&format!(" - {}\n", artifact));
167 }
168 }
169 if !memory.modifiers.is_empty() {
170 output.push_str("modifiers:\n");
171 for modifier in &memory.modifiers {
172 output.push_str(&format!(" - {}\n", modifier));
173 }
174 }
175 output.push_str("```\n\n");
176 }
177
178 for policy in policies {
179 output.push_str(&format!("```policy {}\n", policy.name));
180 output.push_str("rules:\n");
181 for rule in &policy.rules {
182 output.push_str(&format!(" - trigger: {}\n", rule.trigger));
183 output.push_str(" actions:\n");
184 for action in &rule.actions {
185 output.push_str(&action_to_yaml(action));
186 }
187 }
188 output.push_str("```\n\n");
189 }
190
191 for injection in injections {
192 output.push_str("```injection\n");
193 output.push_str(&format!("source: {}\n", injection.source));
194 output.push_str(&format!("target: {}\n", injection.target));
195 output.push_str(&format!("mode: {}\n", injection.mode));
196 output.push_str(&format!("priority: {}\n", injection.priority));
197 if let Some(max_tokens) = injection.max_tokens {
198 output.push_str(&format!("max_tokens: {}\n", max_tokens));
199 }
200 if let Some(filter) = &injection.filter {
201 output.push_str(&format!("filter: {}\n", filter_expr_to_string(filter)));
202 }
203 output.push_str("```\n\n");
204 }
205
206 for cache in caches {
207 output.push_str("```cache\n");
208 output.push_str(&format!("backend: {}\n", cache.backend));
209 if let Some(path) = &cache.path {
210 output.push_str(&format!("path: {}\n", yaml_safe_string(path)));
211 }
212 output.push_str(&format!("size_mb: {}\n", cache.size_mb));
213 output.push_str(&format!("default_freshness: {}\n", cache.default_freshness));
214 if let Some(max_entries) = cache.max_entries {
215 output.push_str(&format!("max_entries: {}\n", max_entries));
216 }
217 if let Some(ttl) = &cache.ttl {
218 output.push_str(&format!("ttl: {}\n", yaml_safe_string(ttl)));
219 }
220 output.push_str("```\n\n");
221 }
222
223 for trajectory in trajectories {
224 output.push_str(&format!("```trajectory {}\n", trajectory.name));
225 if let Some(description) = &trajectory.description {
226 output.push_str(&format!("description: {}\n", yaml_safe_string(description)));
227 }
228 output.push_str(&format!(
229 "agent_type: {}\n",
230 yaml_safe_string(&trajectory.agent_type)
231 ));
232 output.push_str(&format!("token_budget: {}\n", trajectory.token_budget));
233 output.push_str("memory_refs:\n");
234 for mem_ref in &trajectory.memory_refs {
235 output.push_str(&format!(" - {}\n", yaml_safe_string(mem_ref)));
236 }
237 if let Some(metadata) = &trajectory.metadata {
238 output.push_str(&format!("metadata: {}\n", metadata));
240 }
241 output.push_str("```\n\n");
242 }
243
244 for agent in agents {
245 output.push_str(&format!("```agent {}\n", agent.name));
246 output.push_str("capabilities:\n");
247 for capability in &agent.capabilities {
248 output.push_str(&format!(" - {}\n", yaml_safe_string(capability)));
249 }
250 output.push_str("constraints:\n");
251 output.push_str(&format!(
252 " max_concurrent: {}\n",
253 agent.constraints.max_concurrent
254 ));
255 output.push_str(&format!(" timeout_ms: {}\n", agent.constraints.timeout_ms));
256 output.push_str("permissions:\n");
257 output.push_str(" read:\n");
258 for r in &agent.permissions.read {
259 output.push_str(&format!(" - {}\n", yaml_safe_string(r)));
260 }
261 output.push_str(" write:\n");
262 for w in &agent.permissions.write {
263 output.push_str(&format!(" - {}\n", yaml_safe_string(w)));
264 }
265 output.push_str(" lock:\n");
266 for l in &agent.permissions.lock {
267 output.push_str(&format!(" - {}\n", yaml_safe_string(l)));
268 }
269 output.push_str("```\n\n");
270 }
271
272 for intent in intents {
273 output.push_str(&format!("```intent {}\n", intent.name));
274 if !intent.goals.is_empty() {
275 output.push_str("goals:\n");
276 for goal in &intent.goals {
277 output.push_str(&format!(" - {}\n", yaml_safe_string(goal)));
278 }
279 }
280 output.push_str(&format!(
281 "autonomy_level: {}\n",
282 autonomy_level_to_string(&intent.autonomy_level)
283 ));
284 output.push_str(&format!("drift_threshold: {}\n", intent.drift_threshold));
285 if !intent.delegation_boundaries.authorized_actions.is_empty()
286 || !intent.delegation_boundaries.requires_approval.is_empty()
287 || !intent.delegation_boundaries.forbidden_actions.is_empty()
288 {
289 output.push_str("delegation_boundaries:\n");
290 if !intent.delegation_boundaries.authorized_actions.is_empty() {
291 output.push_str(" authorized_actions:\n");
292 for action in &intent.delegation_boundaries.authorized_actions {
293 output.push_str(&format!(" - {}\n", yaml_safe_string(action)));
294 }
295 }
296 if !intent.delegation_boundaries.requires_approval.is_empty() {
297 output.push_str(" requires_approval:\n");
298 for action in &intent.delegation_boundaries.requires_approval {
299 output.push_str(&format!(" - {}\n", yaml_safe_string(action)));
300 }
301 }
302 if !intent.delegation_boundaries.forbidden_actions.is_empty() {
303 output.push_str(" forbidden_actions:\n");
304 for action in &intent.delegation_boundaries.forbidden_actions {
305 output.push_str(&format!(" - {}\n", yaml_safe_string(action)));
306 }
307 }
308 }
309 if !intent.resolution_rules.is_empty() {
310 output.push_str("resolution_rules:\n");
311 for rule in &intent.resolution_rules {
312 output.push_str(&format!(
313 " - condition: {}\n",
314 yaml_safe_string(&rule.condition)
315 ));
316 output.push_str(" priority:\n");
317 for p in &rule.priority {
318 output.push_str(&format!(" - {}\n", yaml_safe_string(p)));
319 }
320 if let Some(ref escalate) = rule.escalate_to {
321 output.push_str(&format!(
322 " escalate_to: {}\n",
323 yaml_safe_string(escalate)
324 ));
325 }
326 output.push_str(&format!(" max_authority: {}\n", rule.max_authority));
327 }
328 }
329 if !intent.alignment_signals.is_empty() {
330 output.push_str("alignment_signals:\n");
331 for signal in &intent.alignment_signals {
332 output.push_str(&format!(" - name: {}\n", yaml_safe_string(&signal.name)));
333 output.push_str(&format!(
334 " source: {}\n",
335 yaml_safe_string(&signal.source)
336 ));
337 output.push_str(&format!(
338 " metric: {}\n",
339 yaml_safe_string(&signal.metric)
340 ));
341 output.push_str(&format!(
342 " target: {}\n",
343 signal_target_to_yaml(&signal.target)
344 ));
345 }
346 }
347 output.push_str("```\n\n");
348 }
349
350 output
351}
352
353fn env_value_to_string(v: &EnvValue) -> String {
367 match v {
368 EnvValue::Env(var) => format!("env:{}", var),
369 EnvValue::Literal(s) => yaml_safe_string(s),
370 }
371}
372
373fn action_to_yaml(a: &Action) -> String {
375 match a {
376 Action::Summarize(target) => {
377 format!(" - type: summarize\n target: {}\n", target)
378 }
379 Action::ExtractArtifacts(target) => format!(
380 " - type: extract_artifacts\n target: {}\n",
381 target
382 ),
383 Action::Checkpoint(target) => {
384 format!(" - type: checkpoint\n target: {}\n", target)
385 }
386 Action::Prune { target, criteria } => {
387 format!(
388 " - type: prune\n target: {}\n criteria: {}\n",
389 target,
390 filter_expr_to_string(criteria)
391 )
392 }
393 Action::Notify(msg) => format!(" - type: notify\n target: {}\n", msg),
394 Action::Inject { target, mode } => {
395 format!(
396 " - type: inject\n target: {}\n mode: {}\n",
397 target, mode
398 )
399 }
400 Action::AutoSummarize {
401 source_level,
402 target_level,
403 create_edges,
404 } => {
405 format!(" - type: auto_summarize\n source_level: {:?}\n target_level: {:?}\n create_edges: {}\n", source_level, target_level, create_edges)
406 }
407 }
408}
409
410fn filter_expr_to_string(f: &FilterExpr) -> String {
437 match f {
438 FilterExpr::Comparison { field, op, value } => {
439 let op_str = match op {
440 CompareOp::Eq => "==",
441 CompareOp::Ne => "!=",
442 CompareOp::Gt => ">",
443 CompareOp::Lt => "<",
444 CompareOp::Ge => ">=",
445 CompareOp::Le => "<=",
446 CompareOp::Contains => "contains",
447 CompareOp::Regex => "regex",
448 CompareOp::In => "in",
449 };
450 let value_str = match value {
451 FilterValue::String(s) => format!("\"{}\"", s),
452 FilterValue::Number(n) => n.to_string(),
453 FilterValue::Bool(b) => b.to_string(),
454 FilterValue::Null => "null".to_string(),
455 FilterValue::CurrentTrajectory => "current_trajectory".to_string(),
456 FilterValue::CurrentScope => "current_scope".to_string(),
457 FilterValue::Now => "now".to_string(),
458 FilterValue::Array(_) => "[...]".to_string(), };
460 format!("{} {} {}", field, op_str, value_str)
461 }
462 FilterExpr::And(exprs) => {
463 let exprs_str = exprs
464 .iter()
465 .map(filter_expr_to_string)
466 .collect::<Vec<_>>()
467 .join(" AND ");
468 format!("({})", exprs_str)
469 }
470 FilterExpr::Or(exprs) => {
471 let exprs_str = exprs
472 .iter()
473 .map(filter_expr_to_string)
474 .collect::<Vec<_>>()
475 .join(" OR ");
476 format!("({})", exprs_str)
477 }
478 FilterExpr::Not(expr) => format!("NOT {}", filter_expr_to_string(expr)),
479 }
480}
481
482fn autonomy_level_to_string(level: &AutonomyLevel) -> &'static str {
485 match level {
486 AutonomyLevel::Operator => "operator",
487 AutonomyLevel::Collaborator => "collaborator",
488 AutonomyLevel::Consultant => "consultant",
489 AutonomyLevel::Approver => "approver",
490 AutonomyLevel::Observer => "observer",
491 }
492}
493
494fn signal_target_to_yaml(target: &SignalTarget) -> String {
496 match target {
497 SignalTarget::Above(v) => format!("{{type: above, value: {}}}", v),
498 SignalTarget::Below(v) => format!("{{type: below, value: {}}}", v),
499 SignalTarget::Between { min, max } => {
500 format!("{{type: between, min: {}, max: {}}}", min, max)
501 }
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use cellstate_core::intent::SignalTarget;
509
510 #[test]
513 fn yaml_safe_empty_string() {
514 assert_eq!(yaml_safe_string(""), "\"\"");
515 }
516
517 #[test]
518 fn yaml_safe_plain_string() {
519 assert_eq!(yaml_safe_string("hello"), "hello");
520 }
521
522 #[test]
523 fn yaml_safe_quotes_reserved_words() {
524 assert_eq!(yaml_safe_string("true"), "\"true\"");
525 assert_eq!(yaml_safe_string("false"), "\"false\"");
526 assert_eq!(yaml_safe_string("null"), "\"null\"");
527 }
528
529 #[test]
530 fn yaml_safe_quotes_numbers() {
531 assert_eq!(yaml_safe_string("42"), "\"42\"");
532 assert_eq!(yaml_safe_string("3.14"), "\"3.14\"");
533 }
534
535 #[test]
536 fn yaml_safe_quotes_special_prefixes() {
537 assert_eq!(yaml_safe_string("-item"), "\"-item\"");
538 assert_eq!(yaml_safe_string("!tag"), "\"!tag\"");
539 assert_eq!(yaml_safe_string("&anchor"), "\"&anchor\"");
540 assert_eq!(yaml_safe_string("*alias"), "\"*alias\"");
541 assert_eq!(yaml_safe_string(":colon"), "\":colon\"");
542 assert_eq!(yaml_safe_string("#comment"), "\"#comment\"");
543 }
544
545 #[test]
546 fn yaml_safe_quotes_colons_in_middle() {
547 assert_eq!(yaml_safe_string("key:value"), "\"key:value\"");
548 }
549
550 #[test]
551 fn yaml_safe_escapes_internal_quotes() {
552 let result = yaml_safe_string("say \"hi\"");
554 assert_eq!(result, "say \"hi\"");
555 }
556
557 #[test]
560 fn adapter_type_strings() {
561 assert_eq!(AdapterType::Postgres.to_string(), "postgres");
562 assert_eq!(AdapterType::Redis.to_string(), "redis");
563 assert_eq!(AdapterType::Memory.to_string(), "memory");
564 }
565
566 #[test]
567 fn memory_type_strings() {
568 assert_eq!(MemoryType::Ephemeral.to_string(), "ephemeral");
569 assert_eq!(MemoryType::Working.to_string(), "working");
570 assert_eq!(MemoryType::Episodic.to_string(), "episodic");
571 assert_eq!(MemoryType::Semantic.to_string(), "semantic");
572 assert_eq!(MemoryType::Procedural.to_string(), "procedural");
573 assert_eq!(MemoryType::Meta.to_string(), "meta");
574 }
575
576 #[test]
577 fn field_type_strings() {
578 assert_eq!(FieldType::Text.to_string(), "text");
579 assert_eq!(FieldType::Int.to_string(), "int");
580 assert_eq!(FieldType::Float.to_string(), "float");
581 assert_eq!(FieldType::Bool.to_string(), "bool");
582 assert_eq!(FieldType::Json.to_string(), "json");
583 assert_eq!(
584 FieldType::Embedding(Some(1536)).to_string(),
585 "embedding(1536)"
586 );
587 assert_eq!(FieldType::Embedding(None).to_string(), "embedding");
588 }
589
590 #[test]
591 fn retention_strings() {
592 assert_eq!(Retention::Persistent.to_string(), "persistent");
593 assert_eq!(Retention::Session.to_string(), "session");
594 assert_eq!(Retention::Scope.to_string(), "scope");
595 assert!(Retention::Duration("3600s".into())
596 .to_string()
597 .contains("3600s"));
598 }
599
600 #[test]
601 fn lifecycle_strings() {
602 assert_eq!(Lifecycle::Explicit.to_string(), "explicit");
603 assert!(Lifecycle::AutoClose(Trigger::ScopeClose)
604 .to_string()
605 .contains("scope_close"));
606 }
607
608 #[test]
609 fn trigger_strings() {
610 assert_eq!(Trigger::TaskStart.to_string(), "task_start");
611 assert_eq!(Trigger::Manual.to_string(), "manual");
612 assert_eq!(
613 Trigger::Schedule("0 * * * *".into()).to_string(),
614 "schedule:0 * * * *"
615 );
616 }
617
618 #[test]
619 fn index_type_strings() {
620 assert_eq!(IndexType::Btree.to_string(), "btree");
621 assert_eq!(IndexType::Hash.to_string(), "hash");
622 assert_eq!(IndexType::Hnsw.to_string(), "hnsw");
623 }
624
625 #[test]
626 fn injection_mode_strings() {
627 assert_eq!(InjectionMode::Full.to_string(), "full");
628 assert_eq!(InjectionMode::Summary.to_string(), "summary");
629 assert_eq!(InjectionMode::TopK(5).to_string(), "topk:5");
630 assert_eq!(InjectionMode::Relevant(0.8).to_string(), "relevant:0.8");
631 }
632
633 #[test]
634 fn env_value_strings() {
635 assert_eq!(
636 env_value_to_string(&EnvValue::Literal("hello".into())),
637 "hello"
638 );
639 assert_eq!(
640 env_value_to_string(&EnvValue::Env("MY_VAR".into())),
641 "env:MY_VAR"
642 );
643 }
644
645 #[test]
646 fn freshness_strings() {
647 assert_eq!(FreshnessDef::Strict.to_string(), "strict");
648 assert!(FreshnessDef::BestEffort {
649 max_staleness: "60s".into()
650 }
651 .to_string()
652 .contains("60s"));
653 }
654
655 #[test]
658 fn signal_target_above() {
659 let s = signal_target_to_yaml(&SignalTarget::Above(0.9));
660 assert!(s.contains("above"));
661 assert!(s.contains("0.9"));
662 }
663
664 #[test]
665 fn signal_target_below() {
666 let s = signal_target_to_yaml(&SignalTarget::Below(0.1));
667 assert!(s.contains("below"));
668 assert!(s.contains("0.1"));
669 }
670
671 #[test]
672 fn signal_target_between() {
673 let s = signal_target_to_yaml(&SignalTarget::Between { min: 0.3, max: 0.7 });
674 assert!(s.contains("between"));
675 assert!(s.contains("0.3"));
676 assert!(s.contains("0.7"));
677 }
678
679 #[test]
682 fn ast_to_markdown_empty_ast() {
683 let ast = CellstateAst {
684 version: "1.0".into(),
685 definitions: vec![],
686 };
687 let md = ast_to_markdown(&ast);
688 assert!(md.is_empty());
690 }
691
692 #[test]
693 fn ast_to_markdown_with_adapter() {
694 let ast = CellstateAst {
695 version: "1.0".into(),
696 definitions: vec![Definition::Adapter(AdapterDef {
697 name: "test-adapter".into(),
698 adapter_type: AdapterType::Postgres,
699 connection: "postgres://localhost/test".into(),
700 options: vec![],
701 })],
702 };
703 let md = ast_to_markdown(&ast);
704 assert!(md.contains("test-adapter"));
705 }
706
707 #[test]
710 fn provider_type_strings() {
711 assert_eq!(ProviderType::OpenAI.to_string(), "openai");
712 assert_eq!(ProviderType::Anthropic.to_string(), "anthropic");
713 assert_eq!(ProviderType::Local.to_string(), "local");
714 assert_eq!(ProviderType::Custom.to_string(), "custom");
715 }
716
717 #[test]
720 fn cache_backend_strings() {
721 assert_eq!(CacheBackendType::Lmdb.to_string(), "lmdb");
722 assert_eq!(CacheBackendType::Memory.to_string(), "memory");
723 }
724
725 #[test]
728 fn autonomy_level_strings() {
729 assert_eq!(
730 autonomy_level_to_string(&AutonomyLevel::Operator),
731 "operator"
732 );
733 assert_eq!(
734 autonomy_level_to_string(&AutonomyLevel::Collaborator),
735 "collaborator"
736 );
737 assert_eq!(
738 autonomy_level_to_string(&AutonomyLevel::Consultant),
739 "consultant"
740 );
741 assert_eq!(
742 autonomy_level_to_string(&AutonomyLevel::Approver),
743 "approver"
744 );
745 assert_eq!(
746 autonomy_level_to_string(&AutonomyLevel::Observer),
747 "observer"
748 );
749 }
750
751 #[test]
754 fn modifier_embeddable() {
755 let m = ModifierDef::Embeddable {
756 provider: "openai".into(),
757 };
758 assert_eq!(m.to_string(), "embeddable(provider: openai)");
759 }
760
761 #[test]
762 fn modifier_summarizable() {
763 let m = ModifierDef::Summarizable {
764 style: SummaryStyle::Brief,
765 on_triggers: vec![Trigger::TaskEnd, Trigger::TurnEnd],
766 };
767 let s = m.to_string();
768 assert!(s.starts_with("summarizable(style: brief, on: ["));
769 assert!(s.contains("task_end"));
770 assert!(s.contains("turn_end"));
771 }
772
773 #[test]
774 fn modifier_summarizable_detailed() {
775 let m = ModifierDef::Summarizable {
776 style: SummaryStyle::Detailed,
777 on_triggers: vec![],
778 };
779 let s = m.to_string();
780 assert!(s.contains("detailed"));
781 assert!(s.contains("on: []"));
782 }
783
784 #[test]
785 fn modifier_lockable_exclusive() {
786 let m = ModifierDef::Lockable {
787 mode: LockMode::Exclusive,
788 };
789 assert_eq!(m.to_string(), "lockable(mode: exclusive)");
790 }
791
792 #[test]
793 fn modifier_lockable_shared() {
794 let m = ModifierDef::Lockable {
795 mode: LockMode::Shared,
796 };
797 assert_eq!(m.to_string(), "lockable(mode: shared)");
798 }
799
800 #[test]
803 fn action_summarize() {
804 let s = action_to_yaml(&Action::Summarize("scope".into()));
805 assert!(s.contains("type: summarize"));
806 assert!(s.contains("target: scope"));
807 }
808
809 #[test]
810 fn action_extract_artifacts() {
811 let s = action_to_yaml(&Action::ExtractArtifacts("trajectory".into()));
812 assert!(s.contains("type: extract_artifacts"));
813 assert!(s.contains("target: trajectory"));
814 }
815
816 #[test]
817 fn action_checkpoint() {
818 let s = action_to_yaml(&Action::Checkpoint("scope".into()));
819 assert!(s.contains("type: checkpoint"));
820 assert!(s.contains("target: scope"));
821 }
822
823 #[test]
824 fn action_prune() {
825 let s = action_to_yaml(&Action::Prune {
826 target: "artifacts".into(),
827 criteria: FilterExpr::Comparison {
828 field: "age".into(),
829 op: CompareOp::Gt,
830 value: FilterValue::Number(30.0),
831 },
832 });
833 assert!(s.contains("type: prune"));
834 assert!(s.contains("target: artifacts"));
835 assert!(s.contains("criteria: age > 30"));
836 }
837
838 #[test]
839 fn action_notify() {
840 let s = action_to_yaml(&Action::Notify("alert".into()));
841 assert!(s.contains("type: notify"));
842 assert!(s.contains("target: alert"));
843 }
844
845 #[test]
846 fn action_inject() {
847 let s = action_to_yaml(&Action::Inject {
848 target: "context".into(),
849 mode: InjectionMode::TopK(5),
850 });
851 assert!(s.contains("type: inject"));
852 assert!(s.contains("target: context"));
853 assert!(s.contains("mode: topk:5"));
854 }
855
856 #[test]
857 fn action_auto_summarize() {
858 let s = action_to_yaml(&Action::AutoSummarize {
859 source_level: AbstractionLevelParsed::Raw,
860 target_level: AbstractionLevelParsed::Summary,
861 create_edges: true,
862 });
863 assert!(s.contains("type: auto_summarize"));
864 assert!(s.contains("create_edges: true"));
865 }
866
867 #[test]
870 fn filter_comparison_eq_string() {
871 let f = FilterExpr::Comparison {
872 field: "name".into(),
873 op: CompareOp::Eq,
874 value: FilterValue::String("alice".into()),
875 };
876 assert_eq!(filter_expr_to_string(&f), "name == \"alice\"");
877 }
878
879 #[test]
880 fn filter_comparison_ne_bool() {
881 let f = FilterExpr::Comparison {
882 field: "active".into(),
883 op: CompareOp::Ne,
884 value: FilterValue::Bool(false),
885 };
886 assert_eq!(filter_expr_to_string(&f), "active != false");
887 }
888
889 #[test]
890 fn filter_comparison_gt_number() {
891 let f = FilterExpr::Comparison {
892 field: "score".into(),
893 op: CompareOp::Gt,
894 value: FilterValue::Number(42.0),
895 };
896 assert_eq!(filter_expr_to_string(&f), "score > 42");
897 }
898
899 #[test]
900 fn filter_comparison_lt() {
901 let f = FilterExpr::Comparison {
902 field: "x".into(),
903 op: CompareOp::Lt,
904 value: FilterValue::Number(10.0),
905 };
906 assert_eq!(filter_expr_to_string(&f), "x < 10");
907 }
908
909 #[test]
910 fn filter_comparison_ge_le() {
911 let ge = FilterExpr::Comparison {
912 field: "a".into(),
913 op: CompareOp::Ge,
914 value: FilterValue::Number(1.0),
915 };
916 assert_eq!(filter_expr_to_string(&ge), "a >= 1");
917
918 let le = FilterExpr::Comparison {
919 field: "b".into(),
920 op: CompareOp::Le,
921 value: FilterValue::Number(99.0),
922 };
923 assert_eq!(filter_expr_to_string(&le), "b <= 99");
924 }
925
926 #[test]
927 fn filter_comparison_contains() {
928 let f = FilterExpr::Comparison {
929 field: "tags".into(),
930 op: CompareOp::Contains,
931 value: FilterValue::String("rust".into()),
932 };
933 assert_eq!(filter_expr_to_string(&f), "tags contains \"rust\"");
934 }
935
936 #[test]
937 fn filter_comparison_regex() {
938 let f = FilterExpr::Comparison {
939 field: "name".into(),
940 op: CompareOp::Regex,
941 value: FilterValue::String("^test.*".into()),
942 };
943 assert_eq!(filter_expr_to_string(&f), "name regex \"^test.*\"");
944 }
945
946 #[test]
947 fn filter_comparison_in() {
948 let f = FilterExpr::Comparison {
949 field: "status".into(),
950 op: CompareOp::In,
951 value: FilterValue::Array(vec![
952 FilterValue::String("a".into()),
953 FilterValue::String("b".into()),
954 ]),
955 };
956 assert_eq!(filter_expr_to_string(&f), "status in [...]");
957 }
958
959 #[test]
960 fn filter_comparison_null() {
961 let f = FilterExpr::Comparison {
962 field: "deleted_at".into(),
963 op: CompareOp::Eq,
964 value: FilterValue::Null,
965 };
966 assert_eq!(filter_expr_to_string(&f), "deleted_at == null");
967 }
968
969 #[test]
970 fn filter_comparison_special_values() {
971 let ct = FilterExpr::Comparison {
972 field: "traj".into(),
973 op: CompareOp::Eq,
974 value: FilterValue::CurrentTrajectory,
975 };
976 assert_eq!(filter_expr_to_string(&ct), "traj == current_trajectory");
977
978 let cs = FilterExpr::Comparison {
979 field: "scope".into(),
980 op: CompareOp::Eq,
981 value: FilterValue::CurrentScope,
982 };
983 assert_eq!(filter_expr_to_string(&cs), "scope == current_scope");
984
985 let now = FilterExpr::Comparison {
986 field: "ts".into(),
987 op: CompareOp::Lt,
988 value: FilterValue::Now,
989 };
990 assert_eq!(filter_expr_to_string(&now), "ts < now");
991 }
992
993 #[test]
994 fn filter_and_expression() {
995 let f = FilterExpr::And(vec![
996 FilterExpr::Comparison {
997 field: "age".into(),
998 op: CompareOp::Gt,
999 value: FilterValue::Number(18.0),
1000 },
1001 FilterExpr::Comparison {
1002 field: "active".into(),
1003 op: CompareOp::Eq,
1004 value: FilterValue::Bool(true),
1005 },
1006 ]);
1007 assert_eq!(filter_expr_to_string(&f), "(age > 18 AND active == true)");
1008 }
1009
1010 #[test]
1011 fn filter_or_expression() {
1012 let f = FilterExpr::Or(vec![
1013 FilterExpr::Comparison {
1014 field: "role".into(),
1015 op: CompareOp::Eq,
1016 value: FilterValue::String("admin".into()),
1017 },
1018 FilterExpr::Comparison {
1019 field: "role".into(),
1020 op: CompareOp::Eq,
1021 value: FilterValue::String("super".into()),
1022 },
1023 ]);
1024 assert_eq!(
1025 filter_expr_to_string(&f),
1026 "(role == \"admin\" OR role == \"super\")"
1027 );
1028 }
1029
1030 #[test]
1031 fn filter_not_expression() {
1032 let f = FilterExpr::Not(Box::new(FilterExpr::Comparison {
1033 field: "deleted".into(),
1034 op: CompareOp::Eq,
1035 value: FilterValue::Bool(true),
1036 }));
1037 assert_eq!(filter_expr_to_string(&f), "NOT deleted == true");
1038 }
1039
1040 #[test]
1043 fn field_type_uuid_and_timestamp() {
1044 assert_eq!(FieldType::Uuid.to_string(), "uuid");
1045 assert_eq!(FieldType::Timestamp.to_string(), "timestamp");
1046 }
1047
1048 #[test]
1049 fn field_type_enum_variants() {
1050 let f = FieldType::Enum(vec!["Active".into(), "Inactive".into(), "Pending".into()]);
1051 assert_eq!(f.to_string(), "enum(Active, Inactive, Pending)");
1052 }
1053
1054 #[test]
1055 fn field_type_nested_array() {
1056 let f = FieldType::Array(Box::new(FieldType::Array(Box::new(FieldType::Int))));
1057 assert_eq!(f.to_string(), "array(array(int))");
1058 }
1059
1060 #[test]
1063 fn retention_max() {
1064 assert_eq!(Retention::Max(100).to_string(), "max(100)");
1065 }
1066
1067 #[test]
1070 fn trigger_all_variants() {
1071 assert_eq!(Trigger::TaskEnd.to_string(), "task_end");
1072 assert_eq!(Trigger::ScopeClose.to_string(), "scope_close");
1073 assert_eq!(Trigger::TurnEnd.to_string(), "turn_end");
1074 }
1075
1076 #[test]
1079 fn index_type_gin_and_ivfflat() {
1080 assert_eq!(IndexType::Gin.to_string(), "gin");
1081 assert_eq!(IndexType::Ivfflat.to_string(), "ivfflat");
1082 }
1083
1084 #[test]
1087 fn lifecycle_auto_close_with_schedule() {
1088 let l = Lifecycle::AutoClose(Trigger::Schedule("5m".into()));
1089 assert_eq!(l.to_string(), "auto_close(schedule:5m)");
1090 }
1091}