use crate::binding::{
has_parallel_marker, parse_mentions, resolve_mention, strip_parallel_marker, text_to_bindings,
BindingSpec, Mention, MentionResolutionError, ResolvedMention,
};
use crate::dag::StableDag;
use chrono::{DateTime, Utc};
use petgraph::stable_graph::NodeIndex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub id: String,
pub content: String,
pub role: Role,
pub timestamp: DateTime<Utc>,
}
pub struct ChatWorkflow {
pub dag: StableDag<ChatMessage>,
message_counter: u32,
id_to_index: HashMap<String, NodeIndex>,
}
impl ChatWorkflow {
pub fn new() -> Self {
Self {
dag: StableDag::new(),
message_counter: 0,
id_to_index: HashMap::new(),
}
}
pub fn message_count(&self) -> usize {
self.dag.node_count()
}
pub fn add_message(&mut self, content: &str, role: Role) -> NodeIndex {
self.message_counter += 1;
let id = format!("msg-{:03}", self.message_counter);
let message = ChatMessage {
id: id.clone(),
content: content.to_string(),
role,
timestamp: Utc::now(),
};
let idx = self.dag.add_node(message);
self.id_to_index.insert(id, idx);
if self.message_counter > 1 {
if let Some(prev_idx) = self.get_index_by_number(self.message_counter - 1) {
self.dag.add_edge(prev_idx, idx);
}
}
idx
}
pub fn get_message_by_id(&self, id: &str) -> Option<&ChatMessage> {
self.id_to_index
.get(id)
.and_then(|idx| self.dag.node_weight(*idx))
}
pub fn get_message_by_index(&self, idx: NodeIndex) -> Option<&ChatMessage> {
self.dag.node_weight(idx)
}
pub fn get_message_by_number(&self, n: u32) -> Option<&ChatMessage> {
let id = format!("msg-{:03}", n);
self.get_message_by_id(&id)
}
pub fn get_index_by_number(&self, n: u32) -> Option<NodeIndex> {
let id = format!("msg-{:03}", n);
self.id_to_index.get(&id).copied()
}
pub fn current_message_number(&self) -> u32 {
self.message_counter
}
pub fn last_message(&self) -> Option<&ChatMessage> {
if self.message_counter == 0 {
return None;
}
self.get_message_by_number(self.message_counter)
}
pub fn last_message_index(&self) -> Option<NodeIndex> {
if self.message_counter == 0 {
return None;
}
self.get_index_by_number(self.message_counter)
}
pub fn add_message_parallel(&mut self, content: &str, role: Role) -> NodeIndex {
self.message_counter += 1;
let id = format!("msg-{:03}", self.message_counter);
let clean_content = strip_parallel_marker(content);
let message = ChatMessage {
id: id.clone(),
content: clean_content.to_string(),
role,
timestamp: Utc::now(),
};
let idx = self.dag.add_node(message);
self.id_to_index.insert(id, idx);
idx
}
pub fn add_message_auto(&mut self, content: &str, role: Role) -> NodeIndex {
if has_parallel_marker(content) {
self.add_message_parallel(content, role)
} else {
self.add_message(content, role)
}
}
pub fn get_mentions(&self, idx: NodeIndex) -> Vec<Mention> {
match self.get_message_by_index(idx) {
Some(msg) => parse_mentions(&msg.content),
None => vec![],
}
}
pub fn resolve_mention(
&self,
mention: &Mention,
) -> Result<ResolvedMention, MentionResolutionError> {
resolve_mention(mention, self.message_counter)
}
pub fn get_bindings_for_message(
&self,
idx: NodeIndex,
) -> Result<BindingSpec, MentionResolutionError> {
match self.get_message_by_index(idx) {
Some(msg) => text_to_bindings(&msg.content, self.message_counter),
None => Ok(BindingSpec::default()),
}
}
pub fn is_parallel_message(&self, idx: NodeIndex) -> bool {
match self.get_message_by_index(idx) {
Some(msg) => has_parallel_marker(&msg.content),
None => false,
}
}
pub fn add_edges_from_mentions(
&mut self,
target_idx: NodeIndex,
) -> Result<usize, MentionResolutionError> {
let content = match self.get_message_by_index(target_idx) {
Some(msg) => msg.content.clone(),
None => return Ok(0),
};
let mentions = parse_mentions(&content);
let mut edges_added = 0;
for mention in mentions {
let resolved = resolve_mention(&mention, self.message_counter)?;
match resolved {
ResolvedMention::Single(n) => {
if let Some(source_idx) = self.get_index_by_number(n) {
if source_idx != target_idx {
self.dag.add_edge(source_idx, target_idx);
edges_added += 1;
}
}
}
ResolvedMention::Multiple(indices) => {
for n in indices {
if let Some(source_idx) = self.get_index_by_number(n) {
if source_idx != target_idx {
self.dag.add_edge(source_idx, target_idx);
edges_added += 1;
}
}
}
}
ResolvedMention::Empty => {}
}
}
Ok(edges_added)
}
pub fn add_message_with_mentions(
&mut self,
content: &str,
role: Role,
) -> Result<NodeIndex, MentionResolutionError> {
let idx = self.add_message_auto(content, role);
self.add_edges_from_mentions(idx)?;
Ok(idx)
}
pub fn get_dependencies(&self, idx: NodeIndex) -> Vec<NodeIndex> {
self.dag.incoming_neighbors(idx).collect()
}
pub fn get_dependents(&self, idx: NodeIndex) -> Vec<NodeIndex> {
self.dag.outgoing_neighbors(idx).collect()
}
pub fn all_messages(&self) -> Vec<(NodeIndex, &ChatMessage)> {
(1..=self.message_counter)
.filter_map(|n| {
let idx = self.get_index_by_number(n)?;
let msg = self.get_message_by_index(idx)?;
Some((idx, msg))
})
.collect()
}
pub fn total_messages(&self) -> u32 {
self.message_counter
}
pub fn to_yaml(&self) -> String {
let mut yaml = String::new();
yaml.push_str("# Auto-generated from Nika Chat session\n");
yaml.push_str(&format!(
"# Generated: {}\n",
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
yaml.push_str("# Use `nika run <file>` to execute\n\n");
yaml.push_str("schema: \"nika/workflow@0.12\"\n");
yaml.push_str("provider: claude\n\n");
let mut tasks: Vec<(&str, &str, Vec<String>)> = Vec::new();
for (idx, msg) in self.all_messages() {
match msg.role {
Role::User => {
let mut deps = Vec::new();
for dep_idx in self.get_dependencies(idx) {
if let Some(dep_msg) = self.get_message_by_index(dep_idx) {
if dep_msg.role == Role::User {
deps.push(dep_msg.id.clone());
}
}
}
tasks.push((&msg.id, &msg.content, deps));
}
Role::Assistant | Role::System | Role::Tool => {
}
}
}
if tasks.is_empty() {
yaml.push_str("# No user messages to convert to tasks\n");
yaml.push_str("tasks: []\n");
} else {
yaml.push_str("tasks:\n");
for (id, content, deps) in &tasks {
let escaped = escape_yaml_string(content);
yaml.push_str(&format!(" - id: \"{}\"\n", id));
if !deps.is_empty() {
let dep_list: Vec<String> = deps.iter().map(|d| format!("\"{}\"", d)).collect();
yaml.push_str(&format!(" depends_on: [{}]\n", dep_list.join(", ")));
}
yaml.push_str(&format!(" infer: \"{}\"\n\n", escaped));
}
}
yaml
}
}
fn escape_yaml_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
impl Default for ChatWorkflow {
fn default() -> Self {
Self::new()
}
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ChatWorkflow>();
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_workflow_new_creates_empty_dag() {
let workflow = ChatWorkflow::new();
assert_eq!(workflow.message_count(), 0);
assert_eq!(workflow.dag.node_count(), 0);
}
#[test]
fn test_chat_workflow_default() {
let workflow = ChatWorkflow::default();
assert_eq!(workflow.message_count(), 0);
}
#[test]
fn test_chat_workflow_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<ChatWorkflow>();
assert_sync::<ChatWorkflow>();
}
#[test]
fn test_all_role_variants() {
let roles = [Role::User, Role::Assistant, Role::System, Role::Tool];
assert_eq!(roles.len(), 4);
}
#[test]
fn test_role_equality() {
assert_eq!(Role::User, Role::User);
assert_ne!(Role::User, Role::Assistant);
}
#[test]
fn test_role_clone() {
let role = Role::Assistant;
let cloned = role;
assert_eq!(role, cloned);
}
#[test]
fn test_role_serialization() {
let role = Role::User;
let json = serde_json::to_string(&role).unwrap();
let restored: Role = serde_json::from_str(&json).unwrap();
assert_eq!(role, restored);
}
#[test]
fn test_add_message_creates_node() {
let mut workflow = ChatWorkflow::new();
let idx = workflow.add_message("Hello!", Role::User);
assert_eq!(workflow.message_count(), 1);
assert_eq!(idx.index(), 0); }
#[test]
fn test_add_message_generates_sequential_ids() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
workflow.add_message("Third", Role::User);
let msg1 = workflow.get_message_by_id("msg-001");
let msg2 = workflow.get_message_by_id("msg-002");
let msg3 = workflow.get_message_by_id("msg-003");
assert!(msg1.is_some());
assert!(msg2.is_some());
assert!(msg3.is_some());
assert_eq!(msg1.unwrap().content, "First");
assert_eq!(msg2.unwrap().content, "Second");
assert_eq!(msg3.unwrap().content, "Third");
}
#[test]
fn test_add_message_assigns_correct_role() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("User msg", Role::User);
workflow.add_message("Assistant msg", Role::Assistant);
workflow.add_message("System msg", Role::System);
workflow.add_message("Tool msg", Role::Tool);
assert_eq!(
workflow.get_message_by_id("msg-001").unwrap().role,
Role::User
);
assert_eq!(
workflow.get_message_by_id("msg-002").unwrap().role,
Role::Assistant
);
assert_eq!(
workflow.get_message_by_id("msg-003").unwrap().role,
Role::System
);
assert_eq!(
workflow.get_message_by_id("msg-004").unwrap().role,
Role::Tool
);
}
#[test]
fn test_get_message_by_id_nonexistent() {
let workflow = ChatWorkflow::new();
assert!(workflow.get_message_by_id("msg-999").is_none());
}
#[test]
fn test_get_message_by_index() {
let mut workflow = ChatWorkflow::new();
let idx = workflow.add_message("Test message", Role::User);
let msg = workflow.get_message_by_index(idx);
assert!(msg.is_some());
assert_eq!(msg.unwrap().content, "Test message");
}
#[test]
fn test_get_message_by_number() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let msg1 = workflow.get_message_by_number(1);
let msg2 = workflow.get_message_by_number(2);
let msg3 = workflow.get_message_by_number(3);
assert_eq!(msg1.unwrap().content, "First");
assert_eq!(msg2.unwrap().content, "Second");
assert!(msg3.is_none());
}
#[test]
fn test_get_index_by_number() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("First", Role::User);
let idx2 = workflow.add_message("Second", Role::Assistant);
assert_eq!(workflow.get_index_by_number(1), Some(idx1));
assert_eq!(workflow.get_index_by_number(2), Some(idx2));
assert_eq!(workflow.get_index_by_number(3), None);
}
#[test]
fn test_get_message_by_invalid_index() {
let workflow = ChatWorkflow::new();
let invalid_idx = NodeIndex::new(999);
assert!(workflow.get_message_by_index(invalid_idx).is_none());
}
#[test]
fn test_auto_edge_sequential_messages() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("First", Role::User);
let idx2 = workflow.add_message("Second", Role::Assistant);
let idx3 = workflow.add_message("Third", Role::User);
assert!(workflow.dag.has_edge(idx1, idx2), "Should have edge 1 → 2");
assert!(workflow.dag.has_edge(idx2, idx3), "Should have edge 2 → 3");
assert!(
!workflow.dag.has_edge(idx2, idx1),
"Should NOT have edge 2 → 1"
);
assert!(
!workflow.dag.has_edge(idx3, idx2),
"Should NOT have edge 3 → 2"
);
}
#[test]
fn test_first_message_has_no_incoming_edge() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("First message", Role::User);
assert_eq!(
workflow.dag.edge_count(),
0,
"First message should have no edges"
);
let _idx2 = workflow.add_message("Second message", Role::Assistant);
assert_eq!(
workflow.dag.edge_count(),
1,
"Should have exactly 1 edge after 2 messages"
);
assert!(workflow.dag.node_weight(idx1).is_some());
}
#[test]
fn test_current_message_number_empty() {
let workflow = ChatWorkflow::new();
assert_eq!(workflow.current_message_number(), 0);
}
#[test]
fn test_current_message_number_increments() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
assert_eq!(workflow.current_message_number(), 1);
workflow.add_message("Second", Role::Assistant);
assert_eq!(workflow.current_message_number(), 2);
workflow.add_message("Third", Role::User);
assert_eq!(workflow.current_message_number(), 3);
}
#[test]
fn test_last_message_empty() {
let workflow = ChatWorkflow::new();
assert!(workflow.last_message().is_none());
}
#[test]
fn test_last_message_returns_most_recent() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
assert_eq!(workflow.last_message().unwrap().content, "First");
workflow.add_message("Second", Role::Assistant);
assert_eq!(workflow.last_message().unwrap().content, "Second");
workflow.add_message("Third", Role::User);
assert_eq!(workflow.last_message().unwrap().content, "Third");
}
#[test]
fn test_last_message_index() {
let mut workflow = ChatWorkflow::new();
assert!(workflow.last_message_index().is_none());
let idx1 = workflow.add_message("First", Role::User);
assert_eq!(workflow.last_message_index(), Some(idx1));
let idx2 = workflow.add_message("Second", Role::Assistant);
assert_eq!(workflow.last_message_index(), Some(idx2));
}
#[test]
fn test_concurrent_access_with_mutex() {
use parking_lot::Mutex;
use std::sync::Arc;
use std::thread;
let workflow = Arc::new(Mutex::new(ChatWorkflow::new()));
let mut handles = vec![];
for i in 0..4 {
let wf = Arc::clone(&workflow);
handles.push(thread::spawn(move || {
for j in 0..5 {
let mut guard = wf.lock();
let role = if (i + j) % 2 == 0 {
Role::User
} else {
Role::Assistant
};
guard.add_message(&format!("Thread {} msg {}", i, j), role);
}
}));
}
for h in handles {
h.join().unwrap();
}
let guard = workflow.lock();
assert_eq!(guard.message_count(), 20);
assert_eq!(guard.current_message_number(), 20);
assert_eq!(guard.dag.edge_count(), 19); }
#[test]
fn test_send_sync_bounds_with_arc() {
use std::sync::Arc;
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Arc<parking_lot::Mutex<ChatWorkflow>>>();
let workflow = Arc::new(parking_lot::Mutex::new(ChatWorkflow::new()));
let _cloned = Arc::clone(&workflow);
}
#[test]
fn test_add_message_parallel_no_auto_edge() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message_parallel("// Independent task", Role::User);
assert_eq!(workflow.message_count(), 2);
assert_eq!(workflow.dag.edge_count(), 0);
}
#[test]
fn test_add_message_parallel_strips_marker() {
let mut workflow = ChatWorkflow::new();
let idx = workflow.add_message_parallel("// Independent task", Role::User);
let msg = workflow.get_message_by_index(idx).unwrap();
assert_eq!(msg.content, "Independent task");
}
#[test]
fn test_add_message_auto_detects_parallel() {
let mut workflow = ChatWorkflow::new();
workflow.add_message_auto("First", Role::User);
workflow.add_message_auto("Second", Role::Assistant);
workflow.add_message_auto("// Parallel", Role::User);
assert_eq!(workflow.message_count(), 3);
assert_eq!(workflow.dag.edge_count(), 1); }
#[test]
fn test_add_message_auto_sequential() {
let mut workflow = ChatWorkflow::new();
workflow.add_message_auto("First", Role::User);
workflow.add_message_auto("Second", Role::Assistant);
assert_eq!(workflow.message_count(), 2);
assert_eq!(workflow.dag.edge_count(), 1);
}
#[test]
fn test_get_mentions_parses_content() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First message", Role::User);
workflow.add_message("Second message", Role::Assistant);
let idx = workflow.add_message("Based on @1 and @2", Role::User);
let mentions = workflow.get_mentions(idx);
assert_eq!(mentions.len(), 2);
assert_eq!(mentions[0], Mention::Number(1));
assert_eq!(mentions[1], Mention::Number(2));
}
#[test]
fn test_get_mentions_empty_for_no_mentions() {
let mut workflow = ChatWorkflow::new();
let idx = workflow.add_message("No mentions here", Role::User);
let mentions = workflow.get_mentions(idx);
assert!(mentions.is_empty());
}
#[test]
fn test_resolve_mention_uses_message_count() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
workflow.add_message("Third", Role::User);
let result = workflow.resolve_mention(&Mention::Last);
assert_eq!(result, Ok(ResolvedMention::Single(3)));
let result = workflow.resolve_mention(&Mention::All);
assert_eq!(result, Ok(ResolvedMention::Multiple(vec![1, 2, 3])));
}
#[test]
fn test_get_bindings_for_message() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx = workflow.add_message("Based on @1", Role::User);
let spec = workflow.get_bindings_for_message(idx).unwrap();
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_1"));
assert_eq!(spec["ref_1"].path, "msg-001.output");
}
#[test]
fn test_get_bindings_for_message_with_last() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx = workflow.add_message("Continue from @last", Role::User);
let spec = workflow.get_bindings_for_message(idx).unwrap();
assert_eq!(spec.len(), 1);
assert!(spec.contains_key("ref_3")); }
#[test]
fn test_is_parallel_message() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("Normal", Role::User);
let idx2 = workflow.add_message("// Parallel", Role::User);
assert!(!workflow.is_parallel_message(idx1));
assert!(workflow.is_parallel_message(idx2));
}
#[test]
fn test_add_edges_from_mentions_single() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx3 = workflow.add_message("Based on @1", Role::User);
let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
assert_eq!(edges_added, 1);
let deps = workflow.get_dependencies(idx3);
assert_eq!(deps.len(), 2); }
#[test]
fn test_add_edges_from_mentions_multiple() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx3 = workflow.add_message("Based on @1 and @2", Role::User);
let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
assert_eq!(edges_added, 2);
}
#[test]
fn test_add_edges_from_mentions_range() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
workflow.add_message("Third", Role::User);
let idx4 = workflow.add_message("Summarize @1..3", Role::User);
let edges_added = workflow.add_edges_from_mentions(idx4).unwrap();
assert_eq!(edges_added, 3); }
#[test]
fn test_add_edges_from_mentions_no_self_loop() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx3 = workflow.add_message("Self reference @3", Role::User);
let edges_added = workflow.add_edges_from_mentions(idx3).unwrap();
assert_eq!(edges_added, 0); }
#[test]
fn test_add_edges_from_mentions_no_mentions() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
let idx2 = workflow.add_message("No mentions here", Role::User);
let edges_added = workflow.add_edges_from_mentions(idx2).unwrap();
assert_eq!(edges_added, 0);
}
#[test]
fn test_add_message_with_mentions() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx3 = workflow
.add_message_with_mentions("Based on @1", Role::User)
.unwrap();
let deps = workflow.get_dependencies(idx3);
assert_eq!(deps.len(), 2);
}
#[test]
fn test_add_message_with_mentions_parallel() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
workflow.add_message("Second", Role::Assistant);
let idx3 = workflow
.add_message_with_mentions("// Based on @1", Role::User)
.unwrap();
let deps = workflow.get_dependencies(idx3);
assert_eq!(deps.len(), 1);
}
#[test]
fn test_add_message_with_mentions_error() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("First", Role::User);
let result = workflow.add_message_with_mentions("Reference @10", Role::User);
assert!(result.is_err());
}
#[test]
fn test_get_dependencies() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("First", Role::User);
let idx2 = workflow.add_message("Second", Role::Assistant);
let idx3 = workflow.add_message("Third", Role::User);
let deps = workflow.get_dependencies(idx3);
assert_eq!(deps.len(), 1);
assert_eq!(deps[0], idx2);
let deps = workflow.get_dependencies(idx1);
assert!(deps.is_empty());
}
#[test]
fn test_get_dependents() {
let mut workflow = ChatWorkflow::new();
let idx1 = workflow.add_message("First", Role::User);
let idx2 = workflow.add_message("Second", Role::Assistant);
workflow.add_message("Third", Role::User);
let dependents = workflow.get_dependents(idx1);
assert_eq!(dependents.len(), 1);
assert_eq!(dependents[0], idx2);
}
#[test]
fn test_complex_mention_dag() {
let mut workflow = ChatWorkflow::new();
workflow
.add_message_with_mentions("What is Rust?", Role::User)
.unwrap();
workflow
.add_message_with_mentions("Rust is a systems programming language...", Role::Assistant)
.unwrap();
workflow
.add_message_with_mentions("// What is Go?", Role::User)
.unwrap();
workflow
.add_message_with_mentions("Go is a programming language...", Role::Assistant)
.unwrap();
let idx5 = workflow
.add_message_with_mentions("Compare @2 and @4", Role::User)
.unwrap();
let deps = workflow.get_dependencies(idx5);
assert_eq!(deps.len(), 3);
assert!(workflow.dag.edge_count() >= 4);
}
#[test]
fn test_to_yaml_empty_workflow() {
let workflow = ChatWorkflow::new();
let yaml = workflow.to_yaml();
assert!(yaml.contains("# Auto-generated from Nika Chat session"));
assert!(yaml.contains("schema: \"nika/workflow@0.12\""));
assert!(yaml.contains("provider: claude"));
assert!(yaml.contains("tasks: []"));
assert!(!yaml.contains("flows:"));
}
#[test]
fn test_to_yaml_single_user_message() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("What is Rust?", Role::User);
let yaml = workflow.to_yaml();
assert!(yaml.contains("tasks:"));
assert!(yaml.contains("- id: \"msg-001\""));
assert!(yaml.contains("infer: \"What is Rust?\""));
}
#[test]
fn test_to_yaml_skips_non_user_messages() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("What is Rust?", Role::User); workflow.add_message("Rust is a systems language", Role::Assistant); workflow.add_message("System initialized", Role::System); workflow.add_message("Tool output", Role::Tool); workflow.add_message("Tell me more", Role::User);
let yaml = workflow.to_yaml();
assert!(yaml.contains("- id: \"msg-001\""));
assert!(yaml.contains("- id: \"msg-005\""));
assert!(!yaml.contains("- id: \"msg-002\""));
assert!(!yaml.contains("- id: \"msg-003\""));
assert!(!yaml.contains("- id: \"msg-004\""));
}
#[test]
fn test_to_yaml_escapes_quotes() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("Generate \"hello world\" program", Role::User);
let yaml = workflow.to_yaml();
assert!(yaml.contains(r#"infer: "Generate \"hello world\" program""#));
}
#[test]
fn test_to_yaml_escapes_multiline() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("Line 1\nLine 2", Role::User);
let yaml = workflow.to_yaml();
assert!(yaml.contains("Line 1\\nLine 2"));
}
#[test]
fn test_to_yaml_with_mention_creates_depends_on() {
let mut workflow = ChatWorkflow::new();
workflow
.add_message_with_mentions("What is Rust?", Role::User)
.unwrap(); workflow
.add_message_with_mentions("Rust is...", Role::Assistant)
.unwrap(); workflow
.add_message_with_mentions("Expand on @1", Role::User)
.unwrap();
let yaml = workflow.to_yaml();
assert!(yaml.contains("depends_on:"));
assert!(yaml.contains("\"msg-001\""));
assert!(!yaml.contains("flows:"));
}
#[test]
fn test_to_yaml_multiple_mentions_create_depends_on() {
let mut workflow = ChatWorkflow::new();
workflow
.add_message_with_mentions("First question", Role::User)
.unwrap(); workflow
.add_message_with_mentions("Answer 1", Role::Assistant)
.unwrap(); workflow
.add_message_with_mentions("Second question", Role::User)
.unwrap(); workflow
.add_message_with_mentions("Answer 2", Role::Assistant)
.unwrap(); workflow
.add_message_with_mentions("Compare @1 and @3", Role::User)
.unwrap();
let yaml = workflow.to_yaml();
assert!(yaml.contains("depends_on:"));
assert!(yaml.contains("\"msg-001\""));
assert!(yaml.contains("\"msg-003\""));
assert!(!yaml.contains("flows:"));
}
#[test]
fn test_escape_yaml_string_simple() {
assert_eq!(escape_yaml_string("hello"), "hello");
}
#[test]
fn test_escape_yaml_string_with_quotes() {
assert_eq!(escape_yaml_string(r#"say "hi""#), r#"say \"hi\""#);
}
#[test]
fn test_escape_yaml_string_with_newline() {
assert_eq!(escape_yaml_string("line1\nline2"), "line1\\nline2");
}
#[test]
fn test_escape_yaml_string_with_backslash() {
assert_eq!(escape_yaml_string(r"path\to\file"), r"path\\to\\file");
}
#[test]
fn test_escape_yaml_string_complex() {
let input = "He said \"hello\"\nThen walked away";
let expected = r#"He said \"hello\"\nThen walked away"#;
assert_eq!(escape_yaml_string(input), expected);
}
#[test]
fn test_to_yaml_round_trip_parseable() {
let mut workflow = ChatWorkflow::new();
workflow.add_message("Question 1", Role::User);
workflow.add_message("Answer 1", Role::Assistant);
workflow.add_message("Question 2", Role::User);
let yaml = workflow.to_yaml();
let parsed: serde_json::Value = crate::serde_yaml::from_str(&yaml).expect("Valid YAML");
assert!(parsed["schema"].as_str().is_some());
assert!(parsed["tasks"].is_array()); assert!(parsed.get("flows").is_none());
}
}