1use crate::PiConvo;
9use crate::provider::session_to_view;
10use crate::reader::PiSession;
11use toolpath::v1::{Graph, GraphIdentity, GraphMeta, Path, PathOrRef};
12use toolpath_convo::DeriveConfig;
13
14pub fn derive_path(session: &PiSession, config: &DeriveConfig) -> Path {
19 toolpath_convo::derive_path(&session_to_view(session), config)
20}
21
22pub fn derive_graph(sessions: &[PiSession], title: Option<&str>, config: &DeriveConfig) -> Graph {
28 let id_suffix = sessions
29 .first()
30 .map(|s| s.header.id.chars().take(8).collect::<String>())
31 .unwrap_or_else(|| "empty".to_string());
32 let graph_id = format!("graph-pi-{}", id_suffix);
33
34 let paths: Vec<PathOrRef> = sessions
35 .iter()
36 .map(|s| PathOrRef::Path(Box::new(derive_path(s, config))))
37 .collect();
38
39 let meta = title.map(|t| GraphMeta {
40 title: Some(t.to_string()),
41 ..Default::default()
42 });
43
44 Graph {
45 graph: GraphIdentity { id: graph_id },
46 paths,
47 meta,
48 }
49}
50
51pub fn derive_project(
53 manager: &PiConvo,
54 project: &str,
55 title: Option<&str>,
56 config: &DeriveConfig,
57) -> crate::Result<Graph> {
58 let sessions = manager.read_all_sessions(project)?;
59 Ok(derive_graph(&sessions, title, config))
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::types::{AgentMessage, Entry, EntryBase, MessageContent, SessionHeader};
66 use std::collections::HashMap;
67 use std::path::PathBuf;
68
69 fn make_session(id: &str) -> PiSession {
70 let header = SessionHeader {
71 version: 3,
72 id: id.into(),
73 timestamp: "2026-04-16T00:00:00Z".into(),
74 cwd: "/tmp/p".into(),
75 parent_session: None,
76 extra: HashMap::new(),
77 };
78 let msg = Entry::Message {
79 base: EntryBase {
80 id: "u1".into(),
81 parent_id: None,
82 timestamp: "2026-04-16T00:00:01Z".into(),
83 },
84 message: AgentMessage::User {
85 content: MessageContent::Text("hi".into()),
86 timestamp: 1,
87 extra: HashMap::new(),
88 },
89 extra: HashMap::new(),
90 };
91 PiSession {
92 header: header.clone(),
93 entries: vec![Entry::Session(header), msg],
94 file_path: PathBuf::from("/tmp/fake.jsonl"),
95 parent: None,
96 }
97 }
98
99 #[test]
100 fn test_derive_path_wraps_provider() {
101 let session = make_session("abcd1234xxxx");
102 let path = derive_path(&session, &DeriveConfig::default());
103 assert_eq!(path.steps.len(), 1);
104 assert!(
105 path.path.id.starts_with("path-pi-"),
106 "got: {}",
107 path.path.id
108 );
109 }
110
111 #[test]
112 fn test_derive_path_respects_config_overrides() {
113 let session = make_session("abcd1234");
114 let cfg = DeriveConfig {
115 path_id: Some("custom-id".into()),
116 ..Default::default()
117 };
118 let path = derive_path(&session, &cfg);
119 assert_eq!(path.path.id, "custom-id");
120 }
121
122 #[test]
123 fn test_derive_graph_empty_sessions() {
124 let g = derive_graph(&[], None, &DeriveConfig::default());
125 assert!(g.paths.is_empty());
126 assert_eq!(g.graph.id, "graph-pi-empty");
127 }
128
129 #[test]
130 fn test_derive_graph_single_session() {
131 let s = make_session("sess-alpha");
132 let g = derive_graph(std::slice::from_ref(&s), None, &DeriveConfig::default());
133 assert_eq!(g.paths.len(), 1);
134 assert!(matches!(&g.paths[0], PathOrRef::Path(_)));
135 }
136
137 #[test]
138 fn test_derive_graph_multiple_sessions() {
139 let s1 = make_session("sess-one");
140 let s2 = make_session("sess-two");
141 let g = derive_graph(&[s1, s2], None, &DeriveConfig::default());
142 assert_eq!(g.paths.len(), 2);
143 }
144
145 #[test]
146 fn test_derive_graph_with_title() {
147 let s = make_session("sess-alpha");
148 let g = derive_graph(&[s], Some("My Release"), &DeriveConfig::default());
149 assert_eq!(
150 g.meta.as_ref().and_then(|m| m.title.as_deref()),
151 Some("My Release")
152 );
153 }
154
155 #[test]
156 fn test_derive_graph_no_title() {
157 let s = make_session("sess-alpha");
158 let g = derive_graph(&[s], None, &DeriveConfig::default());
159 assert!(g.meta.is_none());
160 }
161}