//! A2 tests for the `change_agent_profile` DTO contract (ยง15.1): //! - Request DTO round-trips camelCase JSON `{ projectId, agentId, profileId, rows, cols }`. //! - `From for ChangeAgentProfileDto` maps the agent and //! surfaces `relaunchedSession` only when a live session was swapped (omitted via //! `skip_serializing_if` when `None`). //! - `From for TerminalSessionDto` carries id/cwd/size. use app_tauri_lib::dto::{ChangeAgentProfileDto, ChangeAgentProfileRequestDto}; use application::ChangeAgentProfileOutput; use domain::ids::{AgentId, NodeId, ProfileId, SessionId}; use domain::terminal::{PtySize, SessionKind, SessionStatus, TerminalSession}; use domain::{Agent, AgentOrigin, ProjectPath}; use serde_json::json; use uuid::Uuid; /// Helper: build a minimal validated [`Agent`]. fn make_agent(agent_uuid: u128, profile_uuid: u128) -> Agent { Agent::new( AgentId::from_uuid(Uuid::from_u128(agent_uuid)), "My Agent", "agents/my-agent.md", ProfileId::from_uuid(Uuid::from_u128(profile_uuid)), AgentOrigin::Scratch, false, ) .expect("valid agent") } /// Helper: build a running [`TerminalSession`] for an agent cell. fn make_session(session_uuid: u128, node_uuid: u128, agent_uuid: u128) -> TerminalSession { let session_id = SessionId::from_uuid(Uuid::from_u128(session_uuid)); let node_id = NodeId::from_uuid(Uuid::from_u128(node_uuid)); let agent_id = AgentId::from_uuid(Uuid::from_u128(agent_uuid)); let cwd = ProjectPath::new("/tmp/project".to_owned()).expect("valid path"); let size = PtySize::new(30, 100).unwrap(); let mut session = TerminalSession::starting( session_id, node_id, cwd, SessionKind::Agent { agent_id }, size, ); session.status = SessionStatus::Running; session } // --------------------------------------------------------------------------- // Request DTO deserialisation (camelCase round-trip) // --------------------------------------------------------------------------- #[test] fn change_agent_profile_request_deserialises_camelcase() { let project_id = Uuid::from_u128(1).to_string(); let agent_id = Uuid::from_u128(2).to_string(); let profile_id = Uuid::from_u128(3).to_string(); let raw = json!({ "projectId": project_id, "agentId": agent_id, "profileId": profile_id, "rows": 24, "cols": 80 }); let dto: ChangeAgentProfileRequestDto = serde_json::from_value(raw).unwrap(); assert_eq!(dto.project_id, project_id); assert_eq!(dto.agent_id, agent_id); assert_eq!(dto.profile_id, profile_id); assert_eq!(dto.rows, 24); assert_eq!(dto.cols, 80); } #[test] fn change_agent_profile_request_rejects_snake_case_keys() { // The wire contract is camelCase; snake_case keys must NOT satisfy the struct. let raw = json!({ "project_id": Uuid::from_u128(1).to_string(), "agent_id": Uuid::from_u128(2).to_string(), "profile_id": Uuid::from_u128(3).to_string(), "rows": 24, "cols": 80 }); let res: Result = serde_json::from_value(raw); assert!(res.is_err(), "snake_case keys must not deserialise"); } // --------------------------------------------------------------------------- // From for ChangeAgentProfileDto // --------------------------------------------------------------------------- #[test] fn output_maps_agent_and_omits_session_when_no_relaunch() { let agent = make_agent(5, 6); let out = ChangeAgentProfileOutput { agent: agent.clone(), relaunched: None, }; let dto = ChangeAgentProfileDto::from(out); assert_eq!(dto.agent.0.id, agent.id); assert!(dto.relaunched_session.is_none()); let v = serde_json::to_value(&dto).unwrap(); // The agent is embedded with its camelCase shape. assert_eq!(v["agent"]["id"], agent.id.to_string()); assert_eq!(v["agent"]["profileId"], agent.profile_id.to_string()); // No relaunch โ‡’ field must be OMITTED from the wire (absent, not null). assert!( v.get("relaunchedSession").is_none(), "no relaunch โ‡’ relaunchedSession omitted, got: {v}" ); // No snake_case leak. assert!(v.get("relaunched_session").is_none()); } #[test] fn output_maps_relaunched_session_camelcase_when_present() { let agent = make_agent(7, 8); let session = make_session(11, 12, 7); let out = ChangeAgentProfileOutput { agent: agent.clone(), relaunched: Some(session.clone()), }; let dto = ChangeAgentProfileDto::from(out); assert!(dto.relaunched_session.is_some()); let v = serde_json::to_value(&dto).unwrap(); let rs = v .get("relaunchedSession") .expect("relaunch present โ‡’ relaunchedSession serialised"); assert_eq!(rs["sessionId"], session.id.to_string()); assert_eq!(rs["cwd"], "/tmp/project"); assert_eq!(rs["rows"], 30); assert_eq!(rs["cols"], 100); // No snake_case leak on the nested DTO. assert!(rs.get("session_id").is_none(), "no snake_case leak"); assert!(v.get("relaunched_session").is_none()); } // --------------------------------------------------------------------------- // From for TerminalSessionDto // --------------------------------------------------------------------------- #[test] fn terminal_session_maps_to_dto() { let session = make_session(21, 22, 23); // Exercise the From impl directly through the output mapping. let out = ChangeAgentProfileOutput { agent: make_agent(23, 24), relaunched: Some(session.clone()), }; let dto = ChangeAgentProfileDto::from(out); let rs = dto.relaunched_session.expect("session present"); assert_eq!(rs.session_id, session.id.to_string()); assert_eq!(rs.cwd, "/tmp/project"); assert_eq!(rs.rows, 30); assert_eq!(rs.cols, 100); }