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 assert_eq!(
111 path.meta.as_ref().unwrap().kind.as_deref(),
112 Some(toolpath::v1::PATH_KIND_AGENT_CODING_SESSION)
113 );
114 }
115
116 #[test]
117 fn test_derive_path_respects_config_overrides() {
118 let session = make_session("abcd1234");
119 let cfg = DeriveConfig {
120 path_id: Some("custom-id".into()),
121 ..Default::default()
122 };
123 let path = derive_path(&session, &cfg);
124 assert_eq!(path.path.id, "custom-id");
125 }
126
127 #[test]
128 fn test_derive_graph_empty_sessions() {
129 let g = derive_graph(&[], None, &DeriveConfig::default());
130 assert!(g.paths.is_empty());
131 assert_eq!(g.graph.id, "graph-pi-empty");
132 }
133
134 #[test]
135 fn test_derive_graph_single_session() {
136 let s = make_session("sess-alpha");
137 let g = derive_graph(std::slice::from_ref(&s), None, &DeriveConfig::default());
138 assert_eq!(g.paths.len(), 1);
139 assert!(matches!(&g.paths[0], PathOrRef::Path(_)));
140 }
141
142 #[test]
143 fn test_derive_graph_multiple_sessions() {
144 let s1 = make_session("sess-one");
145 let s2 = make_session("sess-two");
146 let g = derive_graph(&[s1, s2], None, &DeriveConfig::default());
147 assert_eq!(g.paths.len(), 2);
148 }
149
150 #[test]
151 fn test_derive_graph_with_title() {
152 let s = make_session("sess-alpha");
153 let g = derive_graph(&[s], Some("My Release"), &DeriveConfig::default());
154 assert_eq!(
155 g.meta.as_ref().and_then(|m| m.title.as_deref()),
156 Some("My Release")
157 );
158 }
159
160 #[test]
161 fn test_derive_graph_no_title() {
162 let s = make_session("sess-alpha");
163 let g = derive_graph(&[s], None, &DeriveConfig::default());
164 assert!(g.meta.is_none());
165 }
166}