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(cwd: &Path, timeout: Duration) -> Result<Option<Value>> {
108 let cwd = cwd.to_path_buf();
109 let handle = tokio::task::spawn_blocking(move || build_turn_metadata_value(&cwd));
110 match tokio::time::timeout(timeout, handle).await {
111 Ok(join_result) => {
112 let value = join_result.context("Turn metadata task failed")??;
113 if value.is_null() { Ok(None) } else { Ok(Some(value)) }
114 }
115 Err(_) => {
116 tracing::debug!("Turn metadata collection timed out");
117 Ok(None)
118 }
119 }
120}
121
122pub const TURN_METADATA_HEADER: &str = "X-Turn-Metadata";
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::path::PathBuf;
130
131 #[test]
132 fn test_build_turn_metadata_header() {
133 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
134 let metadata = build_turn_metadata_header(&repo_root).unwrap();
135
136 let parsed: Value = serde_json::from_str(&metadata).unwrap();
138 assert!(parsed.get("workspace").is_some());
139 }
140
141 #[test]
142 fn test_build_turn_metadata_value() {
143 let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
144 let value = build_turn_metadata_value(&repo_root).unwrap();
145
146 assert!(!value.is_null());
147 assert!(value.get("workspace").is_some());
148 }
149
150 #[test]
151 fn test_turn_metadata_header_constant() {
152 assert_eq!(TURN_METADATA_HEADER, "X-Turn-Metadata");
153 }
154
155 #[test]
156 fn test_workspace_info_serialization() {
157 let mut remotes = BTreeMap::new();
158 remotes.insert("origin".to_string(), "https://github.com/user/repo.git".to_string());
159
160 let workspace = WorkspaceInfo {
161 remote_urls: remotes,
162 commit_hash: Some("abc1234".to_string()),
163 repo_root: Some("/path/to/repo".to_string()),
164 };
165
166 let json = serde_json::to_string(&workspace).unwrap();
167 assert!(json.contains("origin"));
168 assert!(json.contains("abc1234"));
169 assert!(json.contains("/path/to/repo"));
170 }
171
172 #[test]
173 fn test_empty_remotes_skipped() {
174 let workspace = WorkspaceInfo {
175 remote_urls: BTreeMap::new(),
176 commit_hash: Some("abc1234".to_string()),
177 repo_root: None,
178 };
179
180 let json = serde_json::to_string(&workspace).unwrap();
181 assert!(!json.contains("remote_urls"));
183 assert!(json.contains("commit_hash"));
184 }
185}