//! L10 tests for [`MoveTabToNewWindow`] with a fake [`ProjectStore`]: the tab is //! detached and the workspace is persisted (load returns the new state). use std::sync::{Arc, Mutex}; use async_trait::async_trait; use domain::ids::{ProjectId, TabId, WindowId}; use domain::layout::{LayoutNode, LayoutTree, LeafCell, Tab, Window, Workspace}; use domain::ports::{IdGenerator, ProjectStore, StoreError}; use domain::project::Project; use domain::NodeId; use uuid::Uuid; use application::{MoveTabToNewWindow, MoveTabToNewWindowInput}; /// A `ProjectStore` fake that only implements the workspace persistence the use /// case needs (the project methods are never called here). #[derive(Clone)] struct FakeStore(Arc>); #[async_trait] impl ProjectStore for FakeStore { async fn list_projects(&self) -> Result, StoreError> { unreachable!() } async fn load_project(&self, _id: ProjectId) -> Result { unreachable!() } async fn save_project(&self, _p: &Project) -> Result<(), StoreError> { unreachable!() } async fn save_workspace(&self, ws: &Workspace) -> Result<(), StoreError> { *self.0.lock().unwrap() = ws.clone(); Ok(()) } async fn load_workspace(&self) -> Result { Ok(self.0.lock().unwrap().clone()) } } struct SeqIds(Mutex); impl IdGenerator for SeqIds { fn new_uuid(&self) -> Uuid { let mut n = self.0.lock().unwrap(); let id = Uuid::from_u128(*n); *n += 1; id } } fn tid(n: u128) -> TabId { TabId::from_uuid(Uuid::from_u128(n)) } fn wid(n: u128) -> WindowId { WindowId::from_uuid(Uuid::from_u128(n)) } fn tab(n: u128) -> Tab { Tab { id: tid(n), project_id: ProjectId::from_uuid(Uuid::from_u128(1000 + n)), layout: LayoutTree::new(LayoutNode::Leaf(LeafCell { id: NodeId::from_uuid(Uuid::from_u128(900 + n)), session: None, agent: None, })), } } fn seeded() -> FakeStore { let ws = Workspace { windows: vec![Window::new(wid(1), vec![tab(1), tab(2)], tid(1)).unwrap()], }; FakeStore(Arc::new(Mutex::new(ws))) } #[tokio::test] async fn detaches_tab_and_persists_workspace() { let store = seeded(); // The id generator's first uuid (from_u128(7)) becomes the new window id. let ids = Arc::new(SeqIds(Mutex::new(7))); let uc = MoveTabToNewWindow::new(Arc::new(store.clone()), ids); let out = uc .execute(MoveTabToNewWindowInput { tab_id: tid(1) }) .await .unwrap(); assert_eq!(out.new_window_id, WindowId::from_uuid(Uuid::from_u128(7))); assert_eq!(out.workspace.windows.len(), 2); // Persisted: reloading the store yields the detached layout. let reloaded = store.load_workspace().await.unwrap(); assert_eq!(reloaded, out.workspace); let detached = reloaded .windows .iter() .find(|w| w.id == out.new_window_id) .unwrap(); assert_eq!(detached.tabs.len(), 1); assert_eq!(detached.tabs[0].id, tid(1)); } #[tokio::test] async fn unknown_tab_is_not_found() { let store = seeded(); let uc = MoveTabToNewWindow::new(Arc::new(store), Arc::new(SeqIds(Mutex::new(7)))); let err = uc .execute(MoveTabToNewWindowInput { tab_id: tid(404) }) .await .unwrap_err(); assert_eq!(err.code(), "NOT_FOUND", "got {err:?}"); }