Files
IdeA/crates/domain/tests/window.rs
Blomios dcba76b871 feat(chat): livre la CLI custom de chat agent (#147) et corrige Cancel
Implémente la vue chat structurée par cellule agent (toggle TUI/CLI custom,
préférence persistée `preferred_view`, reattach live, composer + pièces
jointes) avec le socle backend AgentSession/ChatBridge (UserPrompt,
cancel_current_turn, routage interrupt_agent, commande cancel_agent_chat).

Corrige le bug bloquant relevé par QA : le bouton Cancel de
CustomAgentChatView interrompait tout le tour via closeAgentChat au lieu
de n'annuler que le tour courant via cancelAgentChat, ce qui tuait la
session contrairement au contrat produit validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 11:59:39 +02:00

140 lines
4.9 KiB
Rust

//! L10 tests for the pure `Workspace::move_tab_to_new_window` operation
//! (ARCHITECTURE §10): a tab is *moved*, never duplicated; an emptied source
//! window is dropped; an active moved tab hands activity back to a sibling.
use domain::{
LayoutError, LayoutNode, LayoutTree, LeafCell, NodeId, PersistedMonitorState,
PersistedWindowKind, PersistedWindowPosition, PersistedWindowSize, PersistedWindowState,
ProjectId, Tab, TabId, Window, WindowId, WindowStateSnapshot, Workspace,
};
use uuid::Uuid;
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 leaf_tree() -> LayoutTree {
LayoutTree::new(LayoutNode::Leaf(LeafCell {
id: NodeId::from_uuid(Uuid::from_u128(900)),
session: None,
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
preferred_view: domain::PreferredView::Tui,
}))
}
fn tab(n: u128) -> Tab {
Tab {
id: tid(n),
project_id: ProjectId::from_uuid(Uuid::from_u128(1000 + n)),
layout: leaf_tree(),
}
}
/// Count how many windows contain a tab with the given id.
fn occurrences(ws: &Workspace, tab: TabId) -> usize {
ws.windows
.iter()
.filter(|w| w.tabs.iter().any(|t| t.id == tab))
.count()
}
#[test]
fn move_tab_from_multi_tab_window_keeps_source_and_creates_new() {
let src = Window::new(wid(1), vec![tab(1), tab(2)], tid(1)).unwrap();
let ws = Workspace { windows: vec![src] };
let next = ws.move_tab_to_new_window(tid(1), wid(99)).unwrap();
assert_eq!(next.windows.len(), 2, "source kept + new window");
// The moved tab appears exactly once (moved, not duplicated).
assert_eq!(occurrences(&next, tid(1)), 1);
// Source window kept tab 2 and fell back its active tab to it.
let source = next.windows.iter().find(|w| w.id == wid(1)).unwrap();
assert_eq!(source.tabs.len(), 1);
assert_eq!(source.active_tab, tid(2));
// New window holds the moved tab, active.
let detached = next.windows.iter().find(|w| w.id == wid(99)).unwrap();
assert_eq!(detached.tabs.len(), 1);
assert_eq!(detached.active_tab, tid(1));
}
#[test]
fn move_only_tab_removes_the_emptied_source_window() {
let src = Window::new(wid(1), vec![tab(1)], tid(1)).unwrap();
let ws = Workspace { windows: vec![src] };
let next = ws.move_tab_to_new_window(tid(1), wid(99)).unwrap();
assert_eq!(next.windows.len(), 1, "emptied source dropped");
assert_eq!(next.windows[0].id, wid(99));
assert_eq!(occurrences(&next, tid(1)), 1);
}
#[test]
fn move_unknown_tab_is_rejected() {
let ws = Workspace {
windows: vec![Window::new(wid(1), vec![tab(1)], tid(1)).unwrap()],
};
assert!(matches!(
ws.move_tab_to_new_window(tid(404), wid(99)).unwrap_err(),
LayoutError::TabNotFound(t) if t == tid(404)
));
}
#[test]
fn move_non_active_tab_leaves_source_active_unchanged() {
let src = Window::new(wid(1), vec![tab(1), tab(2)], tid(1)).unwrap();
let ws = Workspace { windows: vec![src] };
let next = ws.move_tab_to_new_window(tid(2), wid(99)).unwrap();
let source = next.windows.iter().find(|w| w.id == wid(1)).unwrap();
assert_eq!(source.active_tab, tid(1), "active tab unchanged");
assert_eq!(source.tabs.len(), 1);
}
#[test]
fn window_state_snapshot_round_trips_with_camel_case_schema() {
let snapshot = WindowStateSnapshot::new(vec![PersistedWindowState {
label: "view-tickets-0000000000000000000000000000002a".to_owned(),
kind: PersistedWindowKind::View,
panel: Some("tickets".to_owned()),
project_id: Some(ProjectId::from_uuid(Uuid::from_u128(42))),
plugin_layout: None,
url: Some(
"index.html?panel=tickets&project=00000000-0000-0000-0000-00000000002a".to_owned(),
),
visible: true,
maximized: false,
fullscreen: false,
outer_position: Some(PersistedWindowPosition { x: 12, y: 34 }),
outer_size: Some(PersistedWindowSize {
width: 1200,
height: 800,
}),
monitor: Some(PersistedMonitorState {
name: Some("HDMI-1".to_owned()),
scale_factor: Some(1.25),
position: Some(PersistedWindowPosition { x: 0, y: 0 }),
size: Some(PersistedWindowSize {
width: 1920,
height: 1080,
}),
}),
last_focused_at: Some(123),
}]);
let json = serde_json::to_string(&snapshot).unwrap();
assert!(json.contains("\"projectId\""), "json was {json}");
assert!(json.contains("\"outerPosition\""), "json was {json}");
assert!(json.contains("\"scaleFactor\""), "json was {json}");
assert!(!json.contains("project_id"), "no snake_case leak: {json}");
let decoded: WindowStateSnapshot = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, snapshot);
}