380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
//! L7 tests for template & sync DTO (de)serialisation contract: camelCase on
|
|
//! the wire, `TemplateDto`/`TemplateListDto` shapes, `AgentDriftDto`/
|
|
//! `AgentDriftListDto`, `SyncResultDto`, request DTO deserialisation, and
|
|
//! `parse_template_id` error behaviour. No Tauri runtime required.
|
|
|
|
use app_tauri_lib::dto::{
|
|
parse_template_id, AgentDriftDto, AgentDriftListDto, CreateAgentFromTemplateRequestDto,
|
|
CreateTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
|
UpdateTemplateRequestDto,
|
|
};
|
|
use application::{
|
|
AgentDrift, CreateTemplateOutput, DetectAgentDriftOutput, ListTemplatesOutput,
|
|
SyncAgentWithTemplateOutput, UpdateTemplateOutput,
|
|
};
|
|
use domain::ids::{AgentId, ProfileId, TemplateId};
|
|
use domain::markdown::MarkdownDoc;
|
|
use domain::template::{AgentTemplate, TemplateVersion};
|
|
use domain::{McpToolPolicy, NetworkPolicy, SystemPermissionSet};
|
|
use serde_json::json;
|
|
use uuid::Uuid;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn make_template(template_uuid: u128, profile_uuid: u128) -> AgentTemplate {
|
|
AgentTemplate::new(
|
|
TemplateId::from_uuid(Uuid::from_u128(template_uuid)),
|
|
"My Template",
|
|
MarkdownDoc::new("# Hello".to_owned()),
|
|
ProfileId::from_uuid(Uuid::from_u128(profile_uuid)),
|
|
)
|
|
.expect("valid template")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TemplateDto serialisation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn template_dto_serialises_camelcase() {
|
|
let tmpl = make_template(1, 2);
|
|
let dto = TemplateDto(tmpl.clone());
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
|
|
assert_eq!(v["id"], tmpl.id.to_string());
|
|
assert_eq!(v["name"], "My Template");
|
|
// contentMd is the camelCase field on AgentTemplate; MarkdownDoc is transparent → plain string
|
|
assert_eq!(v["contentMd"], "# Hello");
|
|
// version is a transparent number
|
|
assert_eq!(v["version"], 1u64);
|
|
assert_eq!(
|
|
v["defaultProfileId"],
|
|
ProfileId::from_uuid(Uuid::from_u128(2)).to_string()
|
|
);
|
|
assert!(v.get("defaultAgentName").is_none());
|
|
assert!(v.get("defaultSystemPermissions").is_none());
|
|
assert!(v.get("defaultMcpToolPermissions").is_none());
|
|
assert!(v.get("defaultMcpToolPolicy").is_none());
|
|
// no snake_case leak
|
|
assert!(
|
|
v.get("content_md").is_none(),
|
|
"no snake_case leak for contentMd"
|
|
);
|
|
assert!(
|
|
v.get("default_profile_id").is_none(),
|
|
"no snake_case leak for defaultProfileId"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn template_dto_version_is_number() {
|
|
let tmpl = make_template(3, 4);
|
|
let dto = TemplateDto(tmpl);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert!(v["version"].is_number(), "version should be a JSON number");
|
|
assert_eq!(v["version"].as_u64().unwrap(), 1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TemplateListDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn template_list_dto_is_transparent_array() {
|
|
let out = ListTemplatesOutput {
|
|
templates: vec![make_template(1, 2), make_template(3, 4)],
|
|
};
|
|
let dto = TemplateListDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
let arr = v.as_array().expect("transparent array");
|
|
assert_eq!(arr.len(), 2);
|
|
assert_eq!(arr[0]["name"], "My Template");
|
|
assert_eq!(arr[1]["name"], "My Template");
|
|
}
|
|
|
|
#[test]
|
|
fn template_list_dto_empty() {
|
|
let out = ListTemplatesOutput { templates: vec![] };
|
|
let dto = TemplateListDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert!(v.as_array().unwrap().is_empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// From<CreateTemplateOutput> / From<UpdateTemplateOutput>
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn create_template_output_maps_to_template_dto() {
|
|
let tmpl = make_template(5, 6);
|
|
let out = CreateTemplateOutput {
|
|
template: tmpl.clone(),
|
|
};
|
|
let dto = TemplateDto::from(out);
|
|
assert_eq!(dto.0.id, tmpl.id);
|
|
}
|
|
|
|
#[test]
|
|
fn update_template_output_maps_to_template_dto() {
|
|
let tmpl = make_template(7, 8);
|
|
let bumped = tmpl.with_updated_content(MarkdownDoc::new("# Updated".to_owned()));
|
|
let out = UpdateTemplateOutput {
|
|
template: bumped.clone(),
|
|
};
|
|
let dto = TemplateDto::from(out);
|
|
assert_eq!(dto.0.version, TemplateVersion(2));
|
|
assert_eq!(dto.0.id, bumped.id);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AgentDriftDto / AgentDriftListDto serialisation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn agent_drift_dto_serialises_camelcase() {
|
|
let agent_id = AgentId::from_uuid(Uuid::from_u128(10));
|
|
let drift = AgentDrift {
|
|
agent_id,
|
|
from: TemplateVersion(1),
|
|
to: TemplateVersion(3),
|
|
};
|
|
let dto = AgentDriftDto::from(drift);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
|
|
assert_eq!(v["agentId"], agent_id.to_string());
|
|
assert_eq!(v["from"], 1u64);
|
|
assert_eq!(v["to"], 3u64);
|
|
// no snake_case leak
|
|
assert!(
|
|
v.get("agent_id").is_none(),
|
|
"no snake_case leak for agentId"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn agent_drift_list_dto_is_transparent_array() {
|
|
let agent_id = AgentId::from_uuid(Uuid::from_u128(11));
|
|
let out = DetectAgentDriftOutput {
|
|
drifts: vec![AgentDrift {
|
|
agent_id,
|
|
from: TemplateVersion(2),
|
|
to: TemplateVersion(5),
|
|
}],
|
|
};
|
|
let dto = AgentDriftListDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
let arr = v.as_array().expect("transparent array");
|
|
assert_eq!(arr.len(), 1);
|
|
assert_eq!(arr[0]["from"], 2u64);
|
|
assert_eq!(arr[0]["to"], 5u64);
|
|
}
|
|
|
|
#[test]
|
|
fn agent_drift_list_dto_empty() {
|
|
let out = DetectAgentDriftOutput { drifts: vec![] };
|
|
let dto = AgentDriftListDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
assert!(v.as_array().unwrap().is_empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SyncResultDto
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn sync_result_dto_synced_with_version() {
|
|
let out = SyncAgentWithTemplateOutput {
|
|
synced: true,
|
|
version: Some(TemplateVersion(4)),
|
|
};
|
|
let dto = SyncResultDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
|
|
assert_eq!(v["synced"], true);
|
|
assert_eq!(v["version"], 4u64);
|
|
}
|
|
|
|
#[test]
|
|
fn sync_result_dto_not_synced_version_null() {
|
|
let out = SyncAgentWithTemplateOutput {
|
|
synced: false,
|
|
version: None,
|
|
};
|
|
let dto = SyncResultDto::from(out);
|
|
let v = serde_json::to_value(&dto).unwrap();
|
|
|
|
assert_eq!(v["synced"], false);
|
|
assert!(v["version"].is_null(), "version should be null when None");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Request DTO deserialisation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn create_template_request_deserialises_camelcase() {
|
|
let profile_id = Uuid::from_u128(20).to_string();
|
|
let raw = json!({
|
|
"name": "Backend Template",
|
|
"content": "# My context",
|
|
"defaultProfileId": profile_id
|
|
});
|
|
let dto: CreateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.name, "Backend Template");
|
|
assert_eq!(dto.content, "# My context");
|
|
assert_eq!(dto.default_profile_id, profile_id);
|
|
assert!(dto.default_agent_name.is_none());
|
|
assert!(dto.default_system_permissions.is_none());
|
|
assert!(dto.default_mcp_tool_policy.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn create_template_request_deserialises_template_defaults() {
|
|
let profile_id = Uuid::from_u128(21).to_string();
|
|
let raw = json!({
|
|
"name": "Backend Template",
|
|
"content": "# My context",
|
|
"defaultProfileId": profile_id,
|
|
"defaultAgentName": "Backend",
|
|
"defaultSystemPermissions": { "network": "allow" },
|
|
"defaultMcpToolPermissions": { "allowedTools": ["idea_template_read"] }
|
|
});
|
|
let dto: CreateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.default_agent_name.as_deref(), Some("Backend"));
|
|
assert_eq!(
|
|
dto.default_system_permissions,
|
|
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow)))
|
|
);
|
|
assert_eq!(
|
|
dto.default_mcp_tool_policy,
|
|
Some(McpToolPolicy {
|
|
allowed_tools: vec!["idea_template_read".to_owned()]
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_template_request_deserialises_frontend_mcp_tool_permissions_key() {
|
|
let profile_id = Uuid::from_u128(22).to_string();
|
|
let raw = json!({
|
|
"name": "Backend Template",
|
|
"content": "# My context",
|
|
"defaultProfileId": profile_id,
|
|
"mcpToolPermissions": { "allowedTools": ["idea_ticket_read"] }
|
|
});
|
|
let dto: CreateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(
|
|
dto.default_mcp_tool_policy,
|
|
Some(McpToolPolicy {
|
|
allowed_tools: vec!["idea_ticket_read".to_owned()]
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn update_template_request_deserialises_camelcase() {
|
|
let template_id = Uuid::from_u128(30).to_string();
|
|
let raw = json!({
|
|
"templateId": template_id,
|
|
"content": "# Updated content"
|
|
});
|
|
let dto: UpdateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.template_id, template_id);
|
|
assert_eq!(dto.content.as_deref(), Some("# Updated content"));
|
|
assert!(dto.default_profile_id.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn update_template_request_deserialises_metadata_and_nullable_clears() {
|
|
let template_id = Uuid::from_u128(31).to_string();
|
|
let profile_id = Uuid::from_u128(32).to_string();
|
|
let raw = json!({
|
|
"templateId": template_id,
|
|
"name": "Renamed",
|
|
"defaultProfileId": profile_id,
|
|
"defaultAgentName": null,
|
|
"defaultSystemPermissions": { "network": "deny" },
|
|
"defaultMcpToolPermissions": null
|
|
});
|
|
let dto: UpdateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.name.as_deref(), Some("Renamed"));
|
|
assert_eq!(dto.default_profile_id.as_deref(), Some(profile_id.as_str()));
|
|
assert_eq!(dto.default_agent_name, Some(None));
|
|
assert_eq!(
|
|
dto.default_system_permissions,
|
|
Some(Some(SystemPermissionSet::new(Some(NetworkPolicy::Deny))))
|
|
);
|
|
assert_eq!(dto.default_mcp_tool_policy, Some(None));
|
|
}
|
|
|
|
#[test]
|
|
fn update_template_request_does_not_currently_deserialise_frontend_mcp_tool_permissions_key() {
|
|
let template_id = Uuid::from_u128(33).to_string();
|
|
let raw = json!({
|
|
"templateId": template_id,
|
|
"mcpToolPermissions": { "allowedTools": ["idea_ticket_read"] }
|
|
});
|
|
let dto: UpdateTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(
|
|
dto.default_mcp_tool_policy,
|
|
Some(Some(McpToolPolicy {
|
|
allowed_tools: vec!["idea_ticket_read".to_owned()]
|
|
}))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_agent_from_template_request_deserialises_camelcase() {
|
|
let project_id = Uuid::from_u128(40).to_string();
|
|
let template_id = Uuid::from_u128(41).to_string();
|
|
let raw = json!({
|
|
"projectId": project_id,
|
|
"templateId": template_id,
|
|
"name": "My Agent",
|
|
"synchronized": true
|
|
});
|
|
let dto: CreateAgentFromTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert_eq!(dto.project_id, project_id);
|
|
assert_eq!(dto.template_id, template_id);
|
|
assert_eq!(dto.name.as_deref(), Some("My Agent"));
|
|
assert!(dto.synchronized);
|
|
}
|
|
|
|
#[test]
|
|
fn create_agent_from_template_request_name_defaults_to_none() {
|
|
let raw = json!({
|
|
"projectId": Uuid::nil().to_string(),
|
|
"templateId": Uuid::nil().to_string(),
|
|
"synchronized": false
|
|
});
|
|
let dto: CreateAgentFromTemplateRequestDto = serde_json::from_value(raw).unwrap();
|
|
assert!(dto.name.is_none());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parse_template_id
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn parse_template_id_accepts_uuid() {
|
|
let id = Uuid::from_u128(99);
|
|
assert_eq!(
|
|
parse_template_id(&id.to_string()).unwrap(),
|
|
TemplateId::from_uuid(id)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_template_id_rejects_garbage() {
|
|
let err = parse_template_id("INVALID").expect_err("garbage rejected");
|
|
assert_eq!(err.code, "INVALID");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_template_id_rejects_empty() {
|
|
let err = parse_template_id("").expect_err("empty string rejected");
|
|
assert_eq!(err.code, "INVALID");
|
|
}
|