Files
IdeA/crates/domain/tests/agent_profile_a0.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

255 lines
7.6 KiB
Rust

//! LOT A0 (fondation) — tests purs du domaine :
//! - `Agent::with_profile` ne change **que** `profile_id` ;
//! - `LayoutTree::leaf` retrouve/loupe un node (feuille vs split/grid) ;
//! - variante d'event `DomainEvent::AgentProfileChanged`.
//!
//! Réutilise les fixtures/helpers existants (`helpers::node`, `helpers::session`)
//! et colle au style des tests `entities.rs` / `layout.rs` (ARCHITECTURE §15.4 A0).
mod helpers;
use domain::events::DomainEvent;
use domain::ids::{AgentId, ProfileId, SkillId, TemplateId};
use domain::{
Agent, AgentOrigin, Direction, LayoutNode, LayoutTree, LeafCell, SkillRef, SkillScope,
SplitContainer, TemplateVersion, WeightedChild,
};
use helpers::{node, session};
use uuid::Uuid;
fn agent_id(n: u128) -> AgentId {
AgentId::from_uuid(Uuid::from_u128(n))
}
fn profile_id(n: u128) -> ProfileId {
ProfileId::from_uuid(Uuid::from_u128(n))
}
fn template_id(n: u128) -> TemplateId {
TemplateId::from_uuid(Uuid::from_u128(n))
}
fn skill_ref(n: u128) -> SkillRef {
SkillRef::new(SkillId::from_uuid(Uuid::from_u128(n)), SkillScope::Project)
}
// ---------------------------------------------------------------------------
// Agent::with_profile — ne change QUE profile_id
// ---------------------------------------------------------------------------
/// Un agent issu d'un template, synchronisé, avec des skills : cas le plus riche
/// pour prouver que `with_profile` ne touche à rien d'autre que `profile_id`.
fn rich_agent() -> Agent {
Agent::new(
agent_id(1),
"Architect",
"agents/architect.md",
profile_id(10),
AgentOrigin::FromTemplate {
template_id: template_id(7),
synced_template_version: TemplateVersion::INITIAL,
},
true,
)
.expect("valid rich agent")
.with_skills(vec![skill_ref(100), skill_ref(101)])
}
#[test]
fn with_profile_changes_only_profile_id() {
let before = rich_agent();
let new_profile = profile_id(99);
let after = before.clone().with_profile(new_profile);
// Le seul champ muté :
assert_eq!(after.profile_id, new_profile);
assert_ne!(after.profile_id, before.profile_id);
// Tous les autres champs sont strictement inchangés :
assert_eq!(after.id, before.id);
assert_eq!(after.name, before.name);
assert_eq!(after.context_path, before.context_path);
assert_eq!(after.origin, before.origin);
assert_eq!(after.synchronized, before.synchronized);
assert_eq!(after.skills, before.skills);
}
#[test]
fn with_profile_preserves_scratch_origin_and_unsynced() {
let before = Agent::new(
agent_id(2),
"Scratchy",
"agents/scratchy.md",
profile_id(10),
AgentOrigin::Scratch,
false,
)
.expect("valid scratch agent");
let after = before.clone().with_profile(profile_id(11));
assert_eq!(after.profile_id, profile_id(11));
assert_eq!(after.origin, AgentOrigin::Scratch);
assert!(!after.synchronized);
assert!(after.skills.is_empty());
// Tout sauf le profil identique → comparer un clone "patché" prouve l'égalité.
assert_eq!(after, before.with_profile(profile_id(11)));
}
#[test]
fn with_profile_to_same_profile_is_identity() {
// Idempotence / égalité attendue : re-poser le même profil ⇒ agent inchangé.
let before = rich_agent();
let after = before.clone().with_profile(before.profile_id);
assert_eq!(after, before);
}
#[test]
fn with_profile_is_idempotent_when_repeated() {
let base = rich_agent();
let once = base.clone().with_profile(profile_id(42));
let twice = once.clone().with_profile(profile_id(42));
assert_eq!(once, twice);
}
// ---------------------------------------------------------------------------
// LayoutTree::leaf — retrouve / loupe (feuille vs split/grid)
// ---------------------------------------------------------------------------
fn leaf_cell(id: u128, sess: Option<u128>) -> LeafCell {
LeafCell {
id: node(id),
session: sess.map(session),
agent: None,
conversation_id: None,
engine_session_id: None,
agent_was_running: false,
preferred_view: domain::PreferredView::Tui,
}
}
/// Arbre contenant **au moins un split** : racine = split (id 9) de deux feuilles
/// (id 1 avec session, id 2 sans).
fn split_tree() -> LayoutTree {
LayoutTree::new(LayoutNode::Split(SplitContainer {
id: node(9),
direction: Direction::Row,
children: vec![
WeightedChild {
node: LayoutNode::Leaf(leaf_cell(1, Some(100))),
weight: 1.0,
},
WeightedChild {
node: LayoutNode::Leaf(leaf_cell(2, None)),
weight: 1.0,
},
],
}))
}
#[test]
fn leaf_finds_existing_leaf_in_split_tree() {
let tree = split_tree();
// Feuille 1 : retrouvée, et c'est exactement le LeafCell attendu.
let expected = leaf_cell(1, Some(100));
let found = tree.leaf(node(1)).expect("leaf 1 exists");
assert_eq!(*found, expected);
// Feuille 2 (sans session) aussi.
let found2 = tree.leaf(node(2)).expect("leaf 2 exists");
assert_eq!(*found2, leaf_cell(2, None));
}
#[test]
fn leaf_returns_none_for_absent_node() {
let tree = split_tree();
assert!(tree.leaf(node(404)).is_none());
}
#[test]
fn leaf_returns_none_for_split_node_id() {
// L'id 9 existe mais désigne un split, pas une feuille ⇒ None.
let tree = split_tree();
assert!(tree.leaf(node(9)).is_none());
}
#[test]
fn leaf_returns_none_for_grid_node_id() {
use domain::{GridCell, GridContainer};
// Grille 1x1 (id 50) contenant une feuille (id 3).
let grid = LayoutTree::new(LayoutNode::Grid(GridContainer {
id: node(50),
col_weights: vec![1.0],
row_weights: vec![1.0],
cells: vec![GridCell {
node: LayoutNode::Leaf(leaf_cell(3, None)),
row: 0,
col: 0,
row_span: 1,
col_span: 1,
}],
}));
// L'id de la grille n'est pas une feuille.
assert!(grid.leaf(node(50)).is_none());
// La feuille imbriquée est bien retrouvée.
assert_eq!(
*grid.leaf(node(3)).expect("nested leaf"),
leaf_cell(3, None)
);
}
#[test]
fn leaf_finds_single_root_leaf() {
let tree = LayoutTree::single(leaf_cell(7, Some(70)));
assert_eq!(
*tree.leaf(node(7)).expect("root leaf"),
leaf_cell(7, Some(70))
);
assert!(tree.leaf(node(8)).is_none());
}
// ---------------------------------------------------------------------------
// DomainEvent::AgentProfileChanged — construction & champs
// ---------------------------------------------------------------------------
#[test]
fn agent_profile_changed_carries_agent_and_profile() {
let aid = agent_id(1);
let pid = profile_id(2);
let event = DomainEvent::AgentProfileChanged {
agent_id: aid,
profile_id: pid,
};
match event {
DomainEvent::AgentProfileChanged {
agent_id,
profile_id,
} => {
assert_eq!(agent_id, aid);
assert_eq!(profile_id, pid);
}
other => panic!("expected AgentProfileChanged, got {other:?}"),
}
}
#[test]
fn agent_profile_changed_equality_and_clone() {
let e1 = DomainEvent::AgentProfileChanged {
agent_id: agent_id(1),
profile_id: profile_id(2),
};
// Clone == original ; un profil différent rompt l'égalité.
assert_eq!(e1.clone(), e1);
assert_ne!(
e1,
DomainEvent::AgentProfileChanged {
agent_id: agent_id(1),
profile_id: profile_id(3),
}
);
}