//! 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"), } }