Skip to main content

steer_workspace/local/
manager.rs

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}