1use async_trait::async_trait;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use crate::error::{WorkspaceManagerError, WorkspaceManagerResult};
6use crate::local::LocalWorkspace;
7use crate::manager::{
8 CreateWorkspaceRequest, DeleteWorkspaceRequest, ListWorkspacesRequest, WorkspaceCreateStrategy,
9 WorkspaceManager,
10};
11use crate::utils::VcsUtils;
12use crate::workspace_registry::WorkspaceRegistry;
13use crate::{
14 EnvironmentId, RepoId, RepoInfo, VcsKind, Workspace, WorkspaceId, WorkspaceInfo,
15 WorkspaceStatus,
16};
17
18use super::git;
19use super::jj;
20use super::layout::WorkspaceLayout;
21use uuid::Uuid;
22
23#[derive(Debug, Clone)]
24pub struct LocalWorkspaceManager {
25 layout: WorkspaceLayout,
26 environment_id: EnvironmentId,
27 registry: Arc<WorkspaceRegistry>,
28}
29
30impl LocalWorkspaceManager {
31 pub async fn new(root: PathBuf) -> WorkspaceManagerResult<Self> {
32 let registry = WorkspaceRegistry::open(&root).await?;
33 let manager = Self {
34 layout: WorkspaceLayout::new(root),
35 environment_id: EnvironmentId::local(),
36 registry: Arc::new(registry),
37 };
38 Ok(manager)
39 }
40
41 pub fn environment_id(&self) -> EnvironmentId {
42 self.environment_id
43 }
44
45 fn repo_id_for_path(path: &Path) -> RepoId {
46 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
47 let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, canonical.to_string_lossy().as_bytes());
48 RepoId::from_uuid(uuid)
49 }
50}
51
52#[async_trait]
53impl WorkspaceManager for LocalWorkspaceManager {
54 async fn resolve_workspace(&self, path: &Path) -> WorkspaceManagerResult<WorkspaceInfo> {
55 let vcs_info = VcsUtils::collect_vcs_info(path)
56 .ok_or_else(|| WorkspaceManagerError::NotSupported("No VCS detected".to_string()))?;
57
58 match vcs_info.kind {
59 VcsKind::Jj => {
60 let workspace_root = vcs_info.root;
61 if let Some(existing) = self.registry.find_by_path(&workspace_root).await? {
62 return Ok(existing);
63 }
64
65 let (workspace, _repo) = jj::load_jj_workspace(&workspace_root)?;
66 let repo_path = workspace.repo_path().to_path_buf();
67 let repo_id = Self::repo_id_for_path(&repo_path);
68
69 let repo_info = if let Some(existing) = self.registry.fetch_repo(repo_id).await? {
70 existing
71 } else {
72 let info = RepoInfo {
73 repo_id,
74 environment_id: self.environment_id,
75 root_path: workspace_root.clone(),
76 vcs_kind: Some(VcsKind::Jj),
77 };
78 self.registry.upsert_repo(&info).await?;
79 info
80 };
81
82 let workspace_name = workspace.workspace_name().as_str().to_string();
83 let info = WorkspaceInfo {
84 workspace_id: WorkspaceId::new(),
85 environment_id: self.environment_id,
86 repo_id: repo_info.repo_id,
87 parent_workspace_id: None,
88 name: Some(workspace_name),
89 path: workspace_root,
90 };
91 self.registry.insert_workspace(&info).await?;
92
93 Ok(info)
94 }
95 VcsKind::Git => {
96 let workspace_root = vcs_info.root;
97 if let Some(existing) = self.registry.find_by_path(&workspace_root).await? {
98 return Ok(existing);
99 }
100
101 let repo_id = git::repo_id_for_path(&workspace_root)?;
102 let repo_info = if let Some(existing) = self.registry.fetch_repo(repo_id).await? {
103 existing
104 } else {
105 let info = RepoInfo {
106 repo_id,
107 environment_id: self.environment_id,
108 root_path: workspace_root.clone(),
109 vcs_kind: Some(VcsKind::Git),
110 };
111 self.registry.upsert_repo(&info).await?;
112 info
113 };
114
115 let info = WorkspaceInfo {
116 workspace_id: WorkspaceId::new(),
117 environment_id: self.environment_id,
118 repo_id: repo_info.repo_id,
119 parent_workspace_id: None,
120 name: WorkspaceLayout::default_workspace_name_for_path(&workspace_root),
121 path: workspace_root,
122 };
123 self.registry.insert_workspace(&info).await?;
124
125 Ok(info)
126 }
127 }
128 }
129
130 async fn create_workspace(
131 &self,
132 request: CreateWorkspaceRequest,
133 ) -> WorkspaceManagerResult<WorkspaceInfo> {
134 match request.strategy {
135 WorkspaceCreateStrategy::JjWorkspace => {
136 let repo_info = self
137 .registry
138 .fetch_repo(request.repo_id)
139 .await?
140 .ok_or_else(|| {
141 WorkspaceManagerError::NotFound(format!(
142 "Repo not found: {}",
143 request.repo_id.as_uuid()
144 ))
145 })?;
146 if repo_info.vcs_kind != Some(VcsKind::Jj) {
147 return Err(WorkspaceManagerError::NotSupported(
148 "Workspace orchestration is only supported for jj repositories".to_string(),
149 ));
150 }
151
152 let jj_root = jj::ensure_jj_workspace_root(&repo_info.root_path)?;
153 let workspace_id = WorkspaceId::new();
154 let requested_name = request
155 .name
156 .unwrap_or_else(|| WorkspaceLayout::default_workspace_name(workspace_id));
157 let jj_name = WorkspaceLayout::sanitize_name(&requested_name);
158
159 let parent_dir = self.layout.workspace_parent_dir(repo_info.repo_id);
160 std::fs::create_dir_all(&parent_dir)?;
161 let workspace_path = WorkspaceLayout::ensure_unique_path(&parent_dir, &jj_name);
162 std::fs::create_dir_all(&workspace_path)?;
163
164 {
165 let (workspace, repo) = jj::load_jj_workspace(&jj_root)?;
166 jj::ensure_workspace_name_available(&repo, &jj_name)?;
167
168 jj::init_workspace_with_existing_repo(
169 &workspace_path,
170 workspace.repo_path(),
171 &repo,
172 jj_name.as_str(),
173 )?;
174 }
175
176 let info = WorkspaceInfo {
177 workspace_id,
178 environment_id: self.environment_id,
179 repo_id: repo_info.repo_id,
180 parent_workspace_id: request.parent_workspace_id,
181 name: Some(requested_name),
182 path: workspace_path,
183 };
184 self.registry.insert_workspace(&info).await?;
185
186 Ok(info)
187 }
188 WorkspaceCreateStrategy::GitWorktree => {
189 let repo_info = self
190 .registry
191 .fetch_repo(request.repo_id)
192 .await?
193 .ok_or_else(|| {
194 WorkspaceManagerError::NotFound(format!(
195 "Repo not found: {}",
196 request.repo_id.as_uuid()
197 ))
198 })?;
199 if repo_info.vcs_kind != Some(VcsKind::Git) {
200 return Err(WorkspaceManagerError::NotSupported(
201 "Git worktree creation is only supported for git repositories".to_string(),
202 ));
203 }
204
205 let workspace_id = WorkspaceId::new();
206 let requested_name = request
207 .name
208 .unwrap_or_else(|| WorkspaceLayout::default_workspace_name(workspace_id));
209 let sanitized_name = WorkspaceLayout::sanitize_name(&requested_name);
210 let parent_dir = self.layout.workspace_parent_dir(repo_info.repo_id);
211 std::fs::create_dir_all(&parent_dir)?;
212 let workspace_path =
213 WorkspaceLayout::ensure_unique_path(&parent_dir, &sanitized_name);
214 let names = git::worktree_names(workspace_id, &sanitized_name, &workspace_path);
215
216 git::create_worktree(
217 &repo_info.root_path,
218 &workspace_path,
219 &names.worktree_name,
220 &names.branch_name,
221 )?;
222 let workspace_path =
223 std::fs::canonicalize(&workspace_path).unwrap_or(workspace_path);
224
225 let info = WorkspaceInfo {
226 workspace_id,
227 environment_id: self.environment_id,
228 repo_id: repo_info.repo_id,
229 parent_workspace_id: request.parent_workspace_id,
230 name: Some(requested_name),
231 path: workspace_path,
232 };
233 self.registry.insert_workspace(&info).await?;
234
235 Ok(info)
236 }
237 }
238 }
239
240 async fn list_workspaces(
241 &self,
242 request: ListWorkspacesRequest,
243 ) -> WorkspaceManagerResult<Vec<WorkspaceInfo>> {
244 if request.environment_id != self.environment_id {
245 return Err(WorkspaceManagerError::NotFound(
246 request.environment_id.as_uuid().to_string(),
247 ));
248 }
249 self.registry.list_workspaces(request.environment_id).await
250 }
251
252 async fn open_workspace(
253 &self,
254 workspace_id: WorkspaceId,
255 ) -> WorkspaceManagerResult<Arc<dyn Workspace>> {
256 let info = self
257 .registry
258 .fetch_workspace(workspace_id)
259 .await?
260 .ok_or_else(|| WorkspaceManagerError::NotFound(workspace_id.as_uuid().to_string()))?;
261
262 let workspace = LocalWorkspace::with_path(info.path.clone())
263 .await
264 .map_err(|e| WorkspaceManagerError::Other(e.to_string()))?;
265 Ok(Arc::new(workspace))
266 }
267
268 async fn get_workspace_status(
269 &self,
270 workspace_id: WorkspaceId,
271 ) -> WorkspaceManagerResult<WorkspaceStatus> {
272 let info = self
273 .registry
274 .fetch_workspace(workspace_id)
275 .await?
276 .ok_or_else(|| WorkspaceManagerError::NotFound(workspace_id.as_uuid().to_string()))?;
277
278 if let Ok(jj_root) = jj::ensure_jj_workspace_root(&info.path)
279 && let Ok((mut workspace, repo)) = jj::load_jj_workspace(&jj_root)
280 {
281 let _ = jj::snapshot_working_copy(&mut workspace, repo).await;
282 }
283
284 let vcs = VcsUtils::collect_vcs_info(&info.path);
285 Ok(WorkspaceStatus {
286 workspace_id: info.workspace_id,
287 environment_id: info.environment_id,
288 repo_id: info.repo_id,
289 path: info.path,
290 vcs,
291 })
292 }
293
294 async fn delete_workspace(
295 &self,
296 request: DeleteWorkspaceRequest,
297 ) -> WorkspaceManagerResult<()> {
298 let info = self
299 .registry
300 .fetch_workspace(request.workspace_id)
301 .await?
302 .ok_or_else(|| {
303 WorkspaceManagerError::NotFound(request.workspace_id.as_uuid().to_string())
304 })?;
305 let repo_info = self
306 .registry
307 .fetch_repo(info.repo_id)
308 .await?
309 .ok_or_else(|| {
310 WorkspaceManagerError::NotFound(format!(
311 "Repo not found: {}",
312 info.repo_id.as_uuid()
313 ))
314 })?;
315
316 let managed_root = self.layout.workspace_parent_dir(info.repo_id);
317 let managed_root =
318 std::fs::canonicalize(&managed_root).unwrap_or_else(|_| managed_root.clone());
319 let info_path = std::fs::canonicalize(&info.path).unwrap_or_else(|_| info.path.clone());
320 if !info_path.starts_with(&managed_root) {
321 return Err(WorkspaceManagerError::InvalidRequest(
322 "Only managed workspaces can be deleted".to_string(),
323 ));
324 }
325
326 match repo_info.vcs_kind {
327 Some(VcsKind::Jj) => {
328 let jj_root = jj::ensure_jj_workspace_root(&info.path)?;
329 let (mut workspace, repo) = jj::load_jj_workspace(&jj_root)?;
330 let workspace_name = workspace.workspace_name().to_owned();
331 let repo = jj::snapshot_working_copy(&mut workspace, repo).await?;
332 let mut tx = repo.start_transaction();
333 tx.repo_mut()
334 .remove_wc_commit(&workspace_name)
335 .map_err(|e| {
336 WorkspaceManagerError::Other(format!("Failed to remove jj workspace: {e}"))
337 })?;
338 if tx.repo_mut().has_rewrites() {
339 tx.repo_mut().rebase_descendants().map_err(|e| {
340 WorkspaceManagerError::Other(format!(
341 "Failed to rebase jj descendants after workspace removal: {e}"
342 ))
343 })?;
344 }
345 let workspace_name_ref: &jj_lib::ref_name::WorkspaceName = workspace_name.as_ref();
346 tx.commit(format!(
347 "forget workspace '{}'",
348 workspace_name_ref.as_str()
349 ))
350 .map_err(|e| {
351 WorkspaceManagerError::Other(format!(
352 "Failed to commit jj workspace removal: {e}"
353 ))
354 })?;
355
356 std::fs::remove_dir_all(&info.path)?;
357 }
358 Some(VcsKind::Git) => {
359 git::remove_worktree(&repo_info.root_path, &info.path)?;
360 if info.path.exists() {
361 std::fs::remove_dir_all(&info.path)?;
362 }
363 }
364 None => {
365 return Err(WorkspaceManagerError::NotSupported(
366 "Workspace orchestration requires a VCS".to_string(),
367 ));
368 }
369 }
370
371 self.registry.delete_workspace(request.workspace_id).await?;
372
373 Ok(())
374 }
375}
376
377#[async_trait]
378impl crate::manager::RepoManager for LocalWorkspaceManager {
379 async fn resolve_repo(
380 &self,
381 environment_id: EnvironmentId,
382 path: &Path,
383 ) -> WorkspaceManagerResult<RepoInfo> {
384 if environment_id != self.environment_id {
385 return Err(WorkspaceManagerError::NotFound(
386 environment_id.as_uuid().to_string(),
387 ));
388 }
389
390 let vcs_info = VcsUtils::collect_vcs_info(path)
391 .ok_or_else(|| WorkspaceManagerError::NotSupported("No VCS detected".to_string()))?;
392
393 match vcs_info.kind {
394 VcsKind::Git => {
395 let repo_root = vcs_info.root;
396 let repo_id = git::repo_id_for_path(&repo_root)?;
397 if let Some(existing) = self.registry.fetch_repo(repo_id).await? {
398 return Ok(existing);
399 }
400 let info = RepoInfo {
401 repo_id,
402 environment_id: self.environment_id,
403 root_path: repo_root,
404 vcs_kind: Some(VcsKind::Git),
405 };
406 self.registry.upsert_repo(&info).await?;
407 Ok(info)
408 }
409 VcsKind::Jj => {
410 let workspace_root = vcs_info.root;
411 let (workspace, _repo) = jj::load_jj_workspace(&workspace_root)?;
412 let repo_path = workspace.repo_path().to_path_buf();
413 let repo_id = Self::repo_id_for_path(&repo_path);
414 if let Some(existing) = self.registry.fetch_repo(repo_id).await? {
415 return Ok(existing);
416 }
417 let info = RepoInfo {
418 repo_id,
419 environment_id: self.environment_id,
420 root_path: workspace_root,
421 vcs_kind: Some(VcsKind::Jj),
422 };
423 self.registry.upsert_repo(&info).await?;
424 Ok(info)
425 }
426 }
427 }
428
429 async fn list_repos(
430 &self,
431 environment_id: EnvironmentId,
432 ) -> WorkspaceManagerResult<Vec<RepoInfo>> {
433 if environment_id != self.environment_id {
434 return Err(WorkspaceManagerError::NotFound(
435 environment_id.as_uuid().to_string(),
436 ));
437 }
438 self.registry.list_repos(environment_id).await
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use jj_lib::repo::Repo;
446 use jj_lib::repo_path::RepoPathBuf;
447 use tempfile::TempDir;
448
449 #[tokio::test]
450 async fn test_no_default_workspace_registered() {
451 let temp = TempDir::new().unwrap();
452 let manager = LocalWorkspaceManager::new(temp.path().to_path_buf())
453 .await
454 .unwrap();
455
456 let workspaces = manager
457 .list_workspaces(ListWorkspacesRequest {
458 environment_id: manager.environment_id(),
459 })
460 .await
461 .unwrap();
462 assert!(workspaces.is_empty());
463 }
464
465 #[tokio::test]
466 async fn test_create_workspace_requires_jj_repo() {
467 let temp = TempDir::new().unwrap();
468 let manager = LocalWorkspaceManager::new(temp.path().to_path_buf())
469 .await
470 .unwrap();
471
472 let result = manager
473 .create_workspace(CreateWorkspaceRequest {
474 repo_id: RepoId::new(),
475 name: Some("child".to_string()),
476 parent_workspace_id: None,
477 strategy: WorkspaceCreateStrategy::JjWorkspace,
478 })
479 .await;
480
481 assert!(matches!(result, Err(WorkspaceManagerError::NotFound(_))));
482 }
483
484 #[test]
485 fn test_delete_workspace_snapshots_and_preserves_revision() {
486 let temp_dir = tempfile::tempdir().unwrap();
487 let settings = {
488 let mut config = jj_lib::config::StackedConfig::with_defaults();
489 let overrides = jj_lib::config::ConfigLayer::parse(
490 jj_lib::config::ConfigSource::CommandArg,
491 r#"
492user.name = "Test User"
493user.email = "test@example.com"
494operation.hostname = "test-host"
495operation.username = "test-user"
496signing.behavior = "drop"
497debug.randomness-seed = 0
498debug.commit-timestamp = "2001-01-01T00:00:00Z"
499debug.operation-timestamp = "2001-01-01T00:00:00Z"
500"#,
501 )
502 .unwrap();
503 config.add_layer(overrides);
504 jj_lib::settings::UserSettings::from_config(config).unwrap()
505 };
506 let (workspace, _repo) =
507 jj_lib::workspace::Workspace::init_simple(&settings, temp_dir.path()).unwrap();
508 let repo_path = workspace.repo_path();
509 let config_path = repo_path.join("config.toml");
510 std::fs::write(config_path, r#"snapshot.auto-track = "all()""#).unwrap();
511 drop(workspace);
512
513 let runtime = tokio::runtime::Runtime::new().unwrap();
514 let manager_root = tempfile::tempdir().unwrap();
515 let manager = runtime
516 .block_on(LocalWorkspaceManager::new(
517 manager_root.path().to_path_buf(),
518 ))
519 .unwrap();
520
521 let base_info = runtime
522 .block_on(manager.resolve_workspace(temp_dir.path()))
523 .unwrap();
524 let child_info = runtime
525 .block_on(manager.create_workspace(CreateWorkspaceRequest {
526 repo_id: base_info.repo_id,
527 name: Some("subagent-test".to_string()),
528 parent_workspace_id: Some(base_info.workspace_id),
529 strategy: WorkspaceCreateStrategy::JjWorkspace,
530 }))
531 .unwrap();
532
533 assert!(child_info.path.exists());
534 let managed_root =
535 std::fs::canonicalize(manager.layout.workspace_parent_dir(child_info.repo_id))
536 .unwrap_or_else(|_| manager.layout.workspace_parent_dir(child_info.repo_id));
537 let child_path =
538 std::fs::canonicalize(&child_info.path).unwrap_or_else(|_| child_info.path.clone());
539 assert!(
540 child_path.starts_with(&managed_root),
541 "workspace path should be managed (path: {child_path:?}, managed_root: {managed_root:?})"
542 );
543 std::fs::write(child_info.path.join("subagent.txt"), "content").unwrap();
544
545 runtime
546 .block_on(manager.delete_workspace(DeleteWorkspaceRequest {
547 workspace_id: child_info.workspace_id,
548 }))
549 .unwrap();
550
551 assert!(!child_info.path.exists());
552
553 let (_workspace, repo) = jj::load_jj_workspace(temp_dir.path()).unwrap();
554 let repo_path = RepoPathBuf::from_internal_string("subagent.txt").unwrap();
555 let found = repo.view().heads().iter().any(|commit_id| {
556 let commit = repo.store().get_commit(commit_id).unwrap();
557 commit
558 .tree()
559 .unwrap()
560 .path_value(repo_path.as_ref())
561 .unwrap()
562 .is_present()
563 });
564
565 assert!(
566 found,
567 "snapshot commit should remain after workspace cleanup"
568 );
569 }
570
571 #[tokio::test]
572 async fn test_git_worktree_create_and_delete() {
573 let temp_dir = TempDir::new().unwrap();
574 let repo_root = temp_dir.path().join("repo");
575 std::fs::create_dir_all(&repo_root).unwrap();
576
577 let repo = gix::init(&repo_root).unwrap();
578 let signature = gix::actor::Signature {
579 name: "Test User".into(),
580 email: "test@example.com".into(),
581 time: gix::actor::date::Time::default(),
582 };
583 let mut time_buf = gix::actor::date::parse::TimeBuf::default();
584 let sig_ref = signature.to_ref(&mut time_buf);
585 let head_id = repo
586 .commit_as(
587 sig_ref,
588 sig_ref,
589 "HEAD",
590 "initial",
591 repo.empty_tree().id,
592 Vec::<gix::ObjectId>::new(),
593 )
594 .unwrap();
595 let head_oid = head_id.detach();
596
597 let manager_root = TempDir::new().unwrap();
598 let manager = LocalWorkspaceManager::new(manager_root.path().to_path_buf())
599 .await
600 .unwrap();
601
602 let base = manager.resolve_workspace(&repo_root).await.unwrap();
603 let created = manager
604 .create_workspace(CreateWorkspaceRequest {
605 repo_id: base.repo_id,
606 name: Some("git-subagent".to_string()),
607 parent_workspace_id: Some(base.workspace_id),
608 strategy: WorkspaceCreateStrategy::GitWorktree,
609 })
610 .await
611 .unwrap();
612
613 assert!(created.path.exists());
614 let gitfile = created.path.join(".git");
615 assert!(gitfile.is_file());
616 let private_git_dir = gix::discover::path::from_gitdir_file(&gitfile).unwrap();
617 assert!(private_git_dir.is_dir());
618 assert!(private_git_dir.join("refs").is_dir());
619
620 let orig_head = std::fs::read_to_string(private_git_dir.join("ORIG_HEAD")).unwrap();
621 assert_eq!(orig_head.trim(), head_oid.to_string());
622 let head_log = std::fs::read_to_string(private_git_dir.join("logs").join("HEAD")).unwrap();
623 assert!(head_log.contains(&head_oid.to_string()));
624 assert!(head_log.contains("checkout: moving from (initial)"));
625
626 let worktree_repo = gix::discover(&created.path).unwrap();
627 assert!(matches!(
628 worktree_repo.kind(),
629 gix::repository::Kind::WorkTree { is_linked: true }
630 ));
631
632 let vcs_info = VcsUtils::collect_vcs_info(&created.path).unwrap();
633 assert_eq!(vcs_info.kind, VcsKind::Git);
634 if let crate::VcsStatus::Git(status) = vcs_info.status {
635 assert!(status.error.is_none());
636 } else {
637 panic!("expected git status");
638 }
639
640 let main_repo = gix::discover(&repo_root).unwrap();
641 let worktrees = main_repo.worktrees().unwrap();
642 let mut listed = false;
643 for proxy in worktrees {
644 if let Ok(base) = proxy.base()
645 && base == created.path
646 {
647 listed = true;
648 break;
649 }
650 }
651 assert!(listed, "worktree should be listed");
652
653 manager
654 .delete_workspace(DeleteWorkspaceRequest {
655 workspace_id: created.workspace_id,
656 })
657 .await
658 .unwrap();
659
660 assert!(!created.path.exists());
661 assert!(!private_git_dir.exists());
662 }
663
664 #[tokio::test]
665 async fn test_git_worktree_delete_when_worktree_missing() {
666 let temp_dir = TempDir::new().unwrap();
667 let repo_root = temp_dir.path().join("repo");
668 std::fs::create_dir_all(&repo_root).unwrap();
669
670 let repo = gix::init(&repo_root).unwrap();
671 let signature = gix::actor::Signature {
672 name: "Test User".into(),
673 email: "test@example.com".into(),
674 time: gix::actor::date::Time::default(),
675 };
676 let mut time_buf = gix::actor::date::parse::TimeBuf::default();
677 let sig_ref = signature.to_ref(&mut time_buf);
678 repo.commit_as(
679 sig_ref,
680 sig_ref,
681 "HEAD",
682 "initial",
683 repo.empty_tree().id,
684 Vec::<gix::ObjectId>::new(),
685 )
686 .unwrap();
687
688 let manager_root = TempDir::new().unwrap();
689 let manager = LocalWorkspaceManager::new(manager_root.path().to_path_buf())
690 .await
691 .unwrap();
692
693 let base = manager.resolve_workspace(&repo_root).await.unwrap();
694 let created = manager
695 .create_workspace(CreateWorkspaceRequest {
696 repo_id: base.repo_id,
697 name: Some("git-subagent-missing".to_string()),
698 parent_workspace_id: Some(base.workspace_id),
699 strategy: WorkspaceCreateStrategy::GitWorktree,
700 })
701 .await
702 .unwrap();
703
704 let gitfile = created.path.join(".git");
705 let private_git_dir = gix::discover::path::from_gitdir_file(&gitfile).unwrap();
706
707 std::fs::remove_dir_all(&created.path).unwrap();
708
709 manager
710 .delete_workspace(DeleteWorkspaceRequest {
711 workspace_id: created.workspace_id,
712 })
713 .await
714 .unwrap();
715
716 assert!(!private_git_dir.exists());
717 }
718}