Sauvegarde de l'arbre de travail en cours (persistance P8, conversations C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le support de la délégation inter-agents pour les profils Codex. Le round-trip inter-agent question/réponse est couvert sans tokens par les tests loopback existants (state::mcp_e2e_loopback_tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
254 lines
7.5 KiB
Rust
254 lines
7.5 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,
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
);
|
|
}
|