vtcode_core/
turn_metadata.rs1use crate::git_info::{self};
8use anyhow::{Context, Result};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::BTreeMap;
12use std::path::Path;
13use std::time::Duration;
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct WorkspaceInfo {
18 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
20 pub remote_urls: BTreeMap<String, String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub commit_hash: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub repo_root: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
31pub struct TurnMetadata {
32 pub workspace: WorkspaceInfo,
34}
35
36pub fn build_turn_metadata_header(cwd: &Path) -> Result<String> {
58 if !git_info::is_git_repo(cwd) {
59 return Ok(String::new());
60 }
61
62 let git_info = git_info::collect_git_info(cwd)?;
63
64 let metadata = TurnMetadata {
65 workspace: WorkspaceInfo {
66 remote_urls: git_info.remotes,
67 commit_hash: git_info.head_commit,
68 repo_root: git_info.repo_root,
69 },
70 };
71
72 let json = serde_json::to_string(&metadata)?;
74 Ok(json)
75}
76
77pub fn build_turn_metadata_value(cwd: &Path) -> Result<Value> {
86 if !git_info::is_git_repo(cwd) {
87 return Ok(Value::Null);
88 }
89
90 let git_info = git_info::collect_git_info(cwd)?;
91
92 let metadata = TurnMetadata {
93 workspace: WorkspaceInfo {
94 remote_urls: git_info.remotes,
95 commit_hash: git_info.head_commit,
96 repo_root: git_info.repo_root,
97 },
98 };
99
100 let value = serde_json::to_value(metadata)?;
101 Ok(value)
102}
103
104pub async fn build_turn_metadata_value_with_timeout(
108 cwd: &Path,
109 timeout: Duration,
110) -> Result<Option<Value>> {
111 if !git_info::is_git_repo(cwd) {
112 return Ok(None);
113 }
114
115 let cwd = cwd.to_path_buf();
116 let handle = tokio::task::spawn_blocking(move || build_turn_metadata_value(&cwd));
117 match tokio::time::timeout(timeout, handle).await {
118 Ok(join_result) => {
119 let value = join_result.context("Turn metadata task failed")??;
120 if value.is_null() {
121 Ok(None)
122 } else {
123 Ok(Some(value))
124 }
125 }
126 Err(_) => {
127 tracing::debug!("Turn metadata collection timed out");
128 Ok(None)
129 }
130 }
131}
132
133pub const TURN_METADATA_HEADER: &str = "X-Turn-Metadata";
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use std::path::PathBuf;
141
142 #[test]
143 fn test_build_turn_metadata_header() {
144 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
145 let metadata = build_turn_metadata_header(&repo_root).unwrap();
146
147 let parsed: Value = serde_json::from_str(&metadata).unwrap();
149 assert!(parsed.get("workspace").is_some());
150 }
151
152 #[test]
153 fn test_build_turn_metadata_value() {
154 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
155 let value = build_turn_metadata_value(&repo_root).unwrap();
156
157 assert!(!value.is_null());
158 assert!(value.get("workspace").is_some());
159 }
160
161 #[test]
162 fn test_turn_metadata_header_constant() {
163 assert_eq!(TURN_METADATA_HEADER, "X-Turn-Metadata");
164 }
165
166 #[test]
167 fn test_workspace_info_serialization() {
168 let mut remotes = BTreeMap::new();
169 remotes.insert(
170 "origin".to_string(),
171 "https://github.com/user/repo.git".to_string(),
172 );
173
174 let workspace = WorkspaceInfo {
175 remote_urls: remotes,
176 commit_hash: Some("abc1234".to_string()),
177 repo_root: Some("/path/to/repo".to_string()),
178 };
179
180 let json = serde_json::to_string(&workspace).unwrap();
181 assert!(json.contains("origin"));
182 assert!(json.contains("abc1234"));
183 assert!(json.contains("/path/to/repo"));
184 }
185
186 #[test]
187 fn test_empty_remotes_skipped() {
188 let workspace = WorkspaceInfo {
189 remote_urls: BTreeMap::new(),
190 commit_hash: Some("abc1234".to_string()),
191 repo_root: None,
192 };
193
194 let json = serde_json::to_string(&workspace).unwrap();
195 assert!(!json.contains("remote_urls"));
197 assert!(json.contains("commit_hash"));
198 }
199}