Files
IdeA/crates/application/tests/window_usecases.rs
Blomios e583b2b49f feat(persistence): P8a — cohérence de clé conversation (id de paire stable)
Corrige l'incohérence P6/P7 : la cellule et la persistance partagent désormais
le MÊME id de paire IdeA, déterministe et stable au redémarrage — la clé sous
laquelle P6b sauve log/handoff == celle sous laquelle P7 les charge.

- domaine : ConversationId::for_pair(a,b) pur/déterministe (User↔Agent = uuid
  agent ; Agent↔Agent = XOR commutatif) ; LeafCell gagne engine_session_id
  (cache resumable moteur, additif serde default)
- infrastructure : InMemoryConversationRegistry::resolve utilise for_pair au
  lieu de new_random() → id recalculable sans état, identique après restart
- application : launch_structured persiste l'id de paire sur la cellule (et l'id
  moteur sur engine_session_id) ; cellule neuve ⇒ dérive for_pair(User,agent) ;
  resolve_conversation repli factorisé sur for_pair ; withers layout clone-and-
  mutate (préservent les champs additifs)
- app-tauri : engine_session_id propagé dans les DTO

Suites domain/infra/application/app-tauri vertes (assertion structured_launch_d3
recodée sur le contrat). Test déterminisme/restart dédié à ajouter au retour de
quota agent (binôme Test interrompu par limite de session).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:10:50 +02:00

115 lines
3.5 KiB
Rust

//! 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<Mutex<Workspace>>);
#[async_trait]
impl ProjectStore for FakeStore {
async fn list_projects(&self) -> Result<Vec<Project>, StoreError> {
unreachable!()
}
async fn load_project(&self, _id: ProjectId) -> Result<Project, StoreError> {
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<Workspace, StoreError> {
Ok(self.0.lock().unwrap().clone())
}
}
struct SeqIds(Mutex<u128>);
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,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
})),
}
}
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:?}");
}