Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).
- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
(persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
(statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
avant le Resume auto, jamais sur le chemin reattach
fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents
fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
311 lines
10 KiB
Rust
311 lines
10 KiB
Rust
//! Tests for the layout-management (#4) and per-cell-agent (#3) DTOs:
|
|
//! camelCase wire shape, `parse_layout_id` error behaviour, `ListLayoutsDto` /
|
|
//! `CreateLayoutResultDto` / `DeleteLayoutResultDto` mappings, and
|
|
//! `setCellAgent` operation deserialisation.
|
|
|
|
use app_tauri_lib::dto::{
|
|
parse_layout_id, CreateLayoutRequestDto, CreateLayoutResultDto, DeleteLayoutRequestDto,
|
|
DeleteLayoutResultDto, LayoutInfoDto, LayoutOperationDto, ListLayoutsDto, RenameLayoutRequestDto,
|
|
SetActiveLayoutRequestDto,
|
|
};
|
|
use application::{
|
|
CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutKind, ListLayoutsOutput,
|
|
};
|
|
use domain::ids::{AgentId, LayoutId, NodeId};
|
|
use serde_json::json;
|
|
use uuid::Uuid;
|
|
|
|
fn lid(n: u128) -> LayoutId {
|
|
LayoutId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
fn nid(n: u128) -> NodeId {
|
|
NodeId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
fn aid(n: u128) -> AgentId {
|
|
AgentId::from_uuid(Uuid::from_u128(n))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LayoutInfoDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn layout_info_dto_serialises_camelcase() {
|
|
let info = LayoutInfo {
|
|
id: lid(1),
|
|
name: "Default".to_owned(),
|
|
kind: LayoutKind::Terminal,
|
|
};
|
|
let dto = LayoutInfoDto::from(info);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert_eq!(v["id"], lid(1).to_string());
|
|
assert_eq!(v["name"], "Default");
|
|
assert_eq!(v["kind"], "terminal");
|
|
}
|
|
|
|
#[test]
|
|
fn layout_info_dto_git_graph_kind() {
|
|
let info = LayoutInfo {
|
|
id: lid(2),
|
|
name: "Graph".to_owned(),
|
|
kind: LayoutKind::GitGraph,
|
|
};
|
|
let dto = LayoutInfoDto::from(info);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert_eq!(v["kind"], "gitGraph");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ListLayoutsDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn list_layouts_dto_from_output() {
|
|
let out = ListLayoutsOutput {
|
|
layouts: vec![
|
|
LayoutInfo { id: lid(1), name: "Default".to_owned(), kind: LayoutKind::Terminal },
|
|
LayoutInfo { id: lid(2), name: "Backend".to_owned(), kind: LayoutKind::GitGraph },
|
|
],
|
|
active_id: lid(1),
|
|
};
|
|
let dto = ListLayoutsDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
|
|
let layouts = v["layouts"].as_array().unwrap();
|
|
assert_eq!(layouts.len(), 2);
|
|
assert_eq!(layouts[0]["name"], "Default");
|
|
assert_eq!(layouts[0]["kind"], "terminal");
|
|
assert_eq!(layouts[1]["name"], "Backend");
|
|
assert_eq!(layouts[1]["kind"], "gitGraph");
|
|
assert_eq!(v["activeId"], lid(1).to_string(), "camelCase activeId");
|
|
assert!(v.get("active_id").is_none(), "no snake_case leak");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CreateLayoutResultDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn create_layout_result_dto_from_output() {
|
|
let out = CreateLayoutOutput { layout_id: lid(42) };
|
|
let dto = CreateLayoutResultDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert_eq!(v["layoutId"], lid(42).to_string(), "camelCase layoutId");
|
|
assert!(v.get("layout_id").is_none(), "no snake_case leak");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DeleteLayoutResultDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn delete_layout_result_dto_from_output() {
|
|
let out = DeleteLayoutOutput { active_id: lid(1) };
|
|
let dto = DeleteLayoutResultDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert_eq!(v["activeId"], lid(1).to_string(), "camelCase activeId");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Request DTO deserialisation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn create_layout_request_deserialises_camelcase() {
|
|
let project_id = Uuid::from_u128(10).to_string();
|
|
let raw = json!({ "projectId": project_id, "name": "Backend" });
|
|
let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.project_id, project_id);
|
|
assert_eq!(dto.name, "Backend");
|
|
assert!(dto.kind.is_none(), "kind defaults to None when absent");
|
|
}
|
|
|
|
#[test]
|
|
fn create_layout_request_with_git_graph_kind() {
|
|
let project_id = Uuid::from_u128(11).to_string();
|
|
let raw = json!({ "projectId": project_id, "name": "Graph", "kind": "gitGraph" });
|
|
let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.kind.as_deref(), Some("gitGraph"));
|
|
let kind = dto.parse_kind().unwrap();
|
|
assert_eq!(kind, application::LayoutKind::GitGraph);
|
|
}
|
|
|
|
#[test]
|
|
fn create_layout_request_unknown_kind_is_invalid() {
|
|
let project_id = Uuid::from_u128(12).to_string();
|
|
let raw = json!({ "projectId": project_id, "name": "X", "kind": "unknown" });
|
|
let dto: CreateLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
let err = dto.parse_kind().unwrap_err();
|
|
assert_eq!(err.code, "INVALID");
|
|
}
|
|
|
|
#[test]
|
|
fn rename_layout_request_deserialises_camelcase() {
|
|
let raw = json!({
|
|
"projectId": Uuid::nil().to_string(),
|
|
"layoutId": Uuid::nil().to_string(),
|
|
"name": "Renamed"
|
|
});
|
|
let dto: RenameLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.name, "Renamed");
|
|
}
|
|
|
|
#[test]
|
|
fn delete_layout_request_deserialises_camelcase() {
|
|
let project_id = Uuid::from_u128(1).to_string();
|
|
let layout_id = Uuid::from_u128(2).to_string();
|
|
let raw = json!({ "projectId": project_id, "layoutId": layout_id });
|
|
let dto: DeleteLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.project_id, project_id);
|
|
assert_eq!(dto.layout_id, layout_id);
|
|
}
|
|
|
|
#[test]
|
|
fn set_active_layout_request_deserialises_camelcase() {
|
|
let raw = json!({
|
|
"projectId": Uuid::nil().to_string(),
|
|
"layoutId": Uuid::nil().to_string()
|
|
});
|
|
let _dto: SetActiveLayoutRequestDto = serde_json::from_value(raw).unwrap();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parse_layout_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn parse_layout_id_accepts_uuid() {
|
|
let id = Uuid::from_u128(5);
|
|
assert_eq!(
|
|
parse_layout_id(&id.to_string()).unwrap(),
|
|
LayoutId::from_uuid(id)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_layout_id_rejects_garbage() {
|
|
let err = parse_layout_id("not-a-uuid").expect_err("garbage rejected");
|
|
assert_eq!(err.code, "INVALID");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// setCellAgent operation deserialisation (#3)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn set_cell_agent_op_deserialises_with_agent() {
|
|
let target = nid(1);
|
|
let agent = aid(99);
|
|
let raw = json!({
|
|
"type": "setCellAgent",
|
|
"target": target.to_string(),
|
|
"agent": agent.to_string()
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellAgent { target: t, agent: a } => {
|
|
assert_eq!(t, target);
|
|
assert_eq!(a, Some(agent));
|
|
}
|
|
_ => panic!("expected SetCellAgent"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_cell_agent_op_deserialises_with_null_agent() {
|
|
let target = nid(1);
|
|
let raw = json!({
|
|
"type": "setCellAgent",
|
|
"target": target.to_string(),
|
|
"agent": null
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellAgent { target: t, agent: None } => {
|
|
assert_eq!(t, target);
|
|
}
|
|
_ => panic!("expected SetCellAgent with None agent"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_cell_agent_op_deserialises_with_absent_agent_defaults_to_none() {
|
|
let target = nid(2);
|
|
// omitting `agent` should default to None
|
|
let raw = json!({
|
|
"type": "setCellAgent",
|
|
"target": target.to_string()
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellAgent { agent: None, .. } => {}
|
|
_ => panic!("expected SetCellAgent with None agent"),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// setCellConversation operation deserialisation (T4b)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn set_cell_conversation_op_deserialises_with_id() {
|
|
let target = nid(1);
|
|
let raw = json!({
|
|
"type": "setCellConversation",
|
|
"target": target.to_string(),
|
|
"conversationId": "conv-42"
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellConversation {
|
|
target: t,
|
|
conversation_id: Some(id),
|
|
} => {
|
|
assert_eq!(t, target);
|
|
assert_eq!(id, "conv-42");
|
|
}
|
|
_ => panic!("expected SetCellConversation with id"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_cell_conversation_op_deserialises_with_null_id() {
|
|
let target = nid(2);
|
|
let raw = json!({
|
|
"type": "setCellConversation",
|
|
"target": target.to_string(),
|
|
"conversationId": null
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellConversation {
|
|
target: t,
|
|
conversation_id: None,
|
|
} => assert_eq!(t, target),
|
|
_ => panic!("expected SetCellConversation with None id"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_cell_conversation_op_deserialises_with_absent_id_defaults_to_none() {
|
|
let target = nid(3);
|
|
let raw = json!({
|
|
"type": "setCellConversation",
|
|
"target": target.to_string()
|
|
});
|
|
let dto: LayoutOperationDto = serde_json::from_value(raw).unwrap();
|
|
let op = dto.into_operation().unwrap();
|
|
match op {
|
|
application::LayoutOperation::SetCellConversation {
|
|
conversation_id: None,
|
|
..
|
|
} => {}
|
|
_ => panic!("expected SetCellConversation with None id"),
|
|
}
|
|
}
|