toolpath_gemini/
derive.rs1use crate::provider::to_view;
10use crate::types::Conversation;
11use toolpath::v1::Path;
12
13#[derive(Debug, Clone, Default)]
15pub struct DeriveConfig {
16 pub project_path: Option<String>,
18 pub include_thinking: bool,
20}
21
22pub fn derive_path(conversation: &Conversation, config: &DeriveConfig) -> Path {
24 let view = to_view(conversation);
25 let prefix: String = view.id.chars().take(8).collect();
26 let base_uri = config.project_path.as_ref().map(|p| {
27 if p.starts_with('/') {
28 format!("file://{}", p)
29 } else {
30 p.clone()
31 }
32 });
33 let cfg = toolpath_convo::DeriveConfig {
34 base_uri,
35 title: Some(format!("Gemini session: {}", prefix)),
36 include_thinking: config.include_thinking,
37 ..Default::default()
38 };
39 toolpath_convo::derive_path(&view, &cfg)
40}
41
42pub fn derive_project(conversations: &[Conversation], config: &DeriveConfig) -> Vec<Path> {
44 conversations
45 .iter()
46 .map(|c| derive_path(c, config))
47 .collect()
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::types::{ChatFile, Conversation};
54 use toolpath::v1::Graph;
55
56 fn make_convo() -> Conversation {
57 let main_json = r#"{
58 "sessionId": "sess-1",
59 "messages": [
60 {"id":"u1","timestamp":"2026-04-17T15:23:55Z","type":"user","content":[{"text":"make a pickle"}]},
61 {"id":"g1","timestamp":"2026-04-17T15:23:57Z","type":"gemini","content":"done","model":"gemini-3-flash-preview"}
62 ]
63 }"#;
64 let main: ChatFile = serde_json::from_str(main_json).unwrap();
65 Conversation {
66 session_uuid: "abcdef01-2345-6789-abcd-ef0123456789".into(),
67 main,
68 sub_agents: vec![],
69 project_path: Some("/tmp/proj".into()),
70 started_at: None,
71 last_activity: None,
72 }
73 }
74
75 #[test]
76 fn derive_path_basic_shape() {
77 let convo = make_convo();
78 let path = derive_path(&convo, &DeriveConfig::default());
79 assert!(path.path.id.starts_with("path-gemini-cli-"));
80 let base = path.path.base.as_ref().expect("base");
81 assert_eq!(base.uri, "file:///tmp/proj");
82 }
83
84 #[test]
85 fn derive_path_producer_in_meta_extra() {
86 let convo = make_convo();
87 let path = derive_path(&convo, &DeriveConfig::default());
88 let producer = path.meta.as_ref().unwrap().extra.get("producer").unwrap();
89 assert_eq!(producer["name"], "gemini-cli");
90 }
91
92 #[test]
93 fn derive_path_actors_populated() {
94 let convo = make_convo();
95 let path = derive_path(&convo, &DeriveConfig::default());
96 let actors = path.meta.as_ref().unwrap().actors.as_ref().unwrap();
97 assert!(actors.contains_key("human:user"));
98 assert!(actors.contains_key("agent:gemini-3-flash-preview"));
99 }
100
101 #[test]
102 fn derive_path_validates_as_single_path_graph() {
103 let convo = make_convo();
104 let path = derive_path(&convo, &DeriveConfig::default());
105 let doc = Graph::from_path(path);
106 let json = doc.to_json().unwrap();
107 let parsed = Graph::from_json(&json).unwrap();
108 let pp = parsed.single_path().expect("single-path graph");
109 let anc = toolpath::v1::query::ancestors(&pp.steps, &pp.path.head);
110 assert_eq!(anc.len(), pp.steps.len(), "all steps on head ancestry");
111 }
112}