feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
326
crates/app-tauri/tests/dto.rs
Normal file
326
crates/app-tauri/tests/dto.rs
Normal file
@ -0,0 +1,326 @@
|
||||
//! L1 tests for the IPC DTO (de)serialisation contract: camelCase on the wire,
|
||||
//! stable `ErrorDto.code`, and the `DomainEvent -> DomainEventDto` mapping with
|
||||
//! its tagged, camelCase JSON shape.
|
||||
|
||||
use app_tauri_lib::dto::{
|
||||
parse_node_id, parse_session_id, ErrorDto, HealthRequestDto, HealthResponseDto, LayoutDto,
|
||||
LayoutOperationDto, OpenTerminalRequestDto, ResizeTerminalRequestDto, TerminalClosedDto,
|
||||
WriteTerminalRequestDto,
|
||||
};
|
||||
use application::{CloseTerminalOutput, LayoutOperation, LoadLayoutOutput, OpenTerminalInput};
|
||||
use domain::{Direction, LayoutNode, LayoutTree, LeafCell, NodeId};
|
||||
use app_tauri_lib::events::{DomainEventDto, DOMAIN_EVENT};
|
||||
|
||||
use application::{AppError, HealthInput};
|
||||
use domain::events::DomainEvent;
|
||||
use domain::ids::{AgentId, SessionId};
|
||||
use domain::ProjectId;
|
||||
use domain::TemplateVersion;
|
||||
use domain::TemplateId;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn health_response_serialises_camel_case() {
|
||||
let dto = HealthResponseDto {
|
||||
version: "1.2.3".into(),
|
||||
alive: true,
|
||||
time_millis: 42,
|
||||
correlation_id: "abc".into(),
|
||||
note: Some("hi".into()),
|
||||
};
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
json!({
|
||||
"version": "1.2.3",
|
||||
"alive": true,
|
||||
"timeMillis": 42,
|
||||
"correlationId": "abc",
|
||||
"note": "hi",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_request_deserialises_camel_case_and_defaults() {
|
||||
let dto: HealthRequestDto = serde_json::from_value(json!({ "note": "x" })).unwrap();
|
||||
assert_eq!(HealthInput::from(dto).note.as_deref(), Some("x"));
|
||||
|
||||
// Missing note defaults to None.
|
||||
let empty: HealthRequestDto = serde_json::from_value(json!({})).unwrap();
|
||||
assert_eq!(HealthInput::from(empty).note, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_dto_carries_stable_code_and_message() {
|
||||
let dto = ErrorDto::from(AppError::NotFound("project".into()));
|
||||
assert_eq!(dto.code, "NOT_FOUND");
|
||||
assert!(dto.message.contains("project"));
|
||||
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["code"], "NOT_FOUND");
|
||||
assert!(v.get("message").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_event_relay_name_is_frozen() {
|
||||
assert_eq!(DOMAIN_EVENT, "domain://event");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_event_dto_is_tagged_and_camel_case() {
|
||||
let pid = ProjectId::from_uuid(Uuid::nil());
|
||||
let dto = DomainEventDto::from(&DomainEvent::ProjectCreated { project_id: pid });
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
json!({ "type": "projectCreated", "projectId": pid.to_string() })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_event_dto_maps_agent_launched() {
|
||||
let aid = AgentId::from_uuid(Uuid::nil());
|
||||
let sid = SessionId::from_uuid(Uuid::nil());
|
||||
let dto = DomainEventDto::from(&DomainEvent::AgentLaunched {
|
||||
agent_id: aid,
|
||||
session_id: sid,
|
||||
});
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["type"], "agentLaunched");
|
||||
assert_eq!(v["agentId"], aid.to_string());
|
||||
assert_eq!(v["sessionId"], sid.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_event_dto_maps_template_updated_version() {
|
||||
let tid = TemplateId::from_uuid(Uuid::nil());
|
||||
let dto = DomainEventDto::from(&DomainEvent::TemplateUpdated {
|
||||
template_id: tid,
|
||||
version: TemplateVersion::INITIAL,
|
||||
});
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["type"], "templateUpdated");
|
||||
assert_eq!(v["templateId"], tid.to_string());
|
||||
assert!(v["version"].is_u64());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_event_dto_maps_pty_output_bytes() {
|
||||
let sid = SessionId::from_uuid(Uuid::nil());
|
||||
let dto = DomainEventDto::from(&DomainEvent::PtyOutput {
|
||||
session_id: sid,
|
||||
bytes: vec![1, 2, 3],
|
||||
});
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["type"], "ptyOutput");
|
||||
assert_eq!(v["bytes"], json!([1, 2, 3]));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Terminal DTOs (L3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn open_terminal_request_deserialises_camel_case_with_defaults() {
|
||||
// Minimal payload: command/args omitted → None / empty.
|
||||
let dto: OpenTerminalRequestDto =
|
||||
serde_json::from_value(json!({ "cwd": "/p", "rows": 24, "cols": 80 })).unwrap();
|
||||
let input = OpenTerminalInput::from(dto);
|
||||
assert_eq!(input.cwd, "/p");
|
||||
assert_eq!(input.rows, 24);
|
||||
assert_eq!(input.cols, 80);
|
||||
assert_eq!(input.command, None);
|
||||
assert!(input.args.is_empty());
|
||||
assert_eq!(input.node_id, None);
|
||||
|
||||
// Full payload with explicit command + args.
|
||||
let full: OpenTerminalRequestDto = serde_json::from_value(json!({
|
||||
"cwd": "/q", "rows": 10, "cols": 20,
|
||||
"command": "/bin/zsh", "args": ["-l"],
|
||||
}))
|
||||
.unwrap();
|
||||
let input = OpenTerminalInput::from(full);
|
||||
assert_eq!(input.command.as_deref(), Some("/bin/zsh"));
|
||||
assert_eq!(input.args, vec!["-l".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_terminal_request_deserialises_session_id_and_data() {
|
||||
let sid = SessionId::from_uuid(Uuid::nil());
|
||||
let dto: WriteTerminalRequestDto = serde_json::from_value(json!({
|
||||
"sessionId": sid.to_string(),
|
||||
"data": [104, 105],
|
||||
}))
|
||||
.unwrap();
|
||||
let input = dto.into_input().expect("valid session id");
|
||||
assert_eq!(input.session_id, sid);
|
||||
assert_eq!(input.data, vec![104u8, 105]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_terminal_request_rejects_bad_session_id() {
|
||||
let dto: WriteTerminalRequestDto =
|
||||
serde_json::from_value(json!({ "sessionId": "not-a-uuid", "data": [] })).unwrap();
|
||||
let err = dto.into_input().expect_err("malformed id rejected");
|
||||
assert_eq!(err.code, "INVALID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_terminal_request_deserialises_camel_case() {
|
||||
let sid = SessionId::from_uuid(Uuid::nil());
|
||||
let dto: ResizeTerminalRequestDto = serde_json::from_value(json!({
|
||||
"sessionId": sid.to_string(), "rows": 40, "cols": 120,
|
||||
}))
|
||||
.unwrap();
|
||||
let input = dto.into_input().expect("valid");
|
||||
assert_eq!(input.session_id, sid);
|
||||
assert_eq!(input.rows, 40);
|
||||
assert_eq!(input.cols, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_closed_dto_serialises_code_camel_case() {
|
||||
let dto = TerminalClosedDto::from(CloseTerminalOutput { code: Some(0) });
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v, json!({ "code": 0 }));
|
||||
|
||||
// Signalled (None) round-trips as null.
|
||||
let none = TerminalClosedDto::from(CloseTerminalOutput { code: None });
|
||||
assert_eq!(serde_json::to_value(&none).unwrap(), json!({ "code": null }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_session_id_accepts_uuid_and_rejects_garbage() {
|
||||
let sid = SessionId::from_uuid(Uuid::nil());
|
||||
assert_eq!(parse_session_id(&sid.to_string()).unwrap(), sid);
|
||||
assert_eq!(parse_session_id("nope").unwrap_err().code, "INVALID");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Layout (L4)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn nid(n: u128) -> NodeId {
|
||||
NodeId::from_uuid(Uuid::from_u128(n))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_dto_serialises_camelcase_tagged_tree() {
|
||||
// A single-leaf tree → transparent over LayoutTree, tagged `{type,node}`.
|
||||
let tree = LayoutTree::single(LeafCell {
|
||||
id: nid(1),
|
||||
session: None,
|
||||
agent: None,
|
||||
});
|
||||
let dto = LayoutDto::from(LoadLayoutOutput {
|
||||
layout_id: domain::LayoutId::new_random(),
|
||||
layout: tree,
|
||||
});
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(v["root"]["type"], "leaf", "enum tagged on `type`");
|
||||
assert_eq!(v["root"]["node"]["id"], nid(1).to_string());
|
||||
// Empty session is skipped (skip_serializing_if).
|
||||
assert!(v["root"]["node"].get("session").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_operation_dto_split_deserialises_camelcase() {
|
||||
let json = json!({
|
||||
"type": "split",
|
||||
"target": nid(1).to_string(),
|
||||
"direction": "row",
|
||||
"newLeaf": nid(2).to_string(),
|
||||
"container": nid(9).to_string(),
|
||||
});
|
||||
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||
match dto.into_operation().unwrap() {
|
||||
LayoutOperation::Split {
|
||||
target,
|
||||
direction,
|
||||
new_leaf,
|
||||
container,
|
||||
} => {
|
||||
assert_eq!(target, nid(1));
|
||||
assert_eq!(direction, Direction::Row);
|
||||
assert_eq!(new_leaf, nid(2));
|
||||
assert_eq!(container, nid(9));
|
||||
}
|
||||
other => panic!("expected Split, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_operation_dto_set_session_accepts_null_session() {
|
||||
// `session: null` → detach (None).
|
||||
let json = json!({
|
||||
"type": "setSession",
|
||||
"target": nid(1).to_string(),
|
||||
"session": null,
|
||||
});
|
||||
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||
match dto.into_operation().unwrap() {
|
||||
LayoutOperation::SetSession { target, session } => {
|
||||
assert_eq!(target, nid(1));
|
||||
assert!(session.is_none());
|
||||
}
|
||||
other => panic!("expected SetSession, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_operation_dto_resize_carries_weights() {
|
||||
let json = json!({
|
||||
"type": "resize",
|
||||
"container": nid(9).to_string(),
|
||||
"weights": [3.0, 1.0],
|
||||
});
|
||||
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||
match dto.into_operation().unwrap() {
|
||||
LayoutOperation::Resize { container, weights } => {
|
||||
assert_eq!(container, nid(9));
|
||||
assert_eq!(weights, vec![3.0, 1.0]);
|
||||
}
|
||||
other => panic!("expected Resize, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_operation_dto_rejects_malformed_uuid_as_invalid() {
|
||||
let json = json!({
|
||||
"type": "merge",
|
||||
"container": "not-a-uuid",
|
||||
"keepIndex": 0,
|
||||
});
|
||||
let dto: LayoutOperationDto = serde_json::from_value(json).unwrap();
|
||||
let err = dto.into_operation().expect_err("malformed uuid rejected");
|
||||
assert_eq!(err.code, "INVALID", "got {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_node_id_accepts_uuid_and_rejects_garbage() {
|
||||
assert_eq!(parse_node_id(&nid(7).to_string()).unwrap(), nid(7));
|
||||
assert_eq!(parse_node_id("garbage").unwrap_err().code, "INVALID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_dto_round_trips_a_split_tree_shape() {
|
||||
// Build a split tree, serialise via the DTO, and confirm the tagged shape
|
||||
// re-parses into an equivalent LayoutTree.
|
||||
let tree = LayoutTree::single(LeafCell {
|
||||
id: nid(1),
|
||||
session: None,
|
||||
agent: None,
|
||||
})
|
||||
.split(nid(1), Direction::Column, LeafCell { id: nid(2), session: None, agent: None }, nid(9))
|
||||
.unwrap();
|
||||
let dto = LayoutDto::from(LoadLayoutOutput {
|
||||
layout_id: domain::LayoutId::new_random(),
|
||||
layout: tree.clone(),
|
||||
});
|
||||
let v = serde_json::to_value(&dto).unwrap();
|
||||
let back: LayoutTree = serde_json::from_value(v).unwrap();
|
||||
assert_eq!(back, tree, "DTO serialisation round-trips the tree");
|
||||
assert!(matches!(back.root, LayoutNode::Split(_)));
|
||||
}
|
||||
Reference in New Issue
Block a user