fix(templates): mcpToolPermissions persisté via create/update_template — désérialisation par alias préférés (mcpToolPermissions > defaultMcpToolPermissions > defaultMcpToolPolicy) sur les DTO Create/UpdateTemplateRequestDto — #244 (QA verte)
This commit is contained in:
@ -255,6 +255,24 @@ fn create_template_request_deserialises_template_defaults() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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();
|
||||
@ -291,6 +309,22 @@ fn update_template_request_deserialises_metadata_and_nullable_clears() {
|
||||
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();
|
||||
|
||||
@ -4281,8 +4281,7 @@ impl From<UpdateTemplateOutput> for TemplateDto {
|
||||
}
|
||||
|
||||
/// Request DTO for `create_template`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CreateTemplateRequestDto {
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
@ -4291,20 +4290,54 @@ pub struct CreateTemplateRequestDto {
|
||||
/// Default runtime profile id for agents created from this template.
|
||||
pub default_profile_id: String,
|
||||
/// Optional default agent name for agents created from this template.
|
||||
#[serde(default)]
|
||||
pub default_agent_name: Option<String>,
|
||||
/// Optional agent-level system permission override for created agents.
|
||||
#[serde(default)]
|
||||
pub default_system_permissions: Option<SystemPermissionSet>,
|
||||
/// Optional agent-level MCP tool policy override for created agents.
|
||||
#[serde(
|
||||
default,
|
||||
rename = "defaultMcpToolPermissions",
|
||||
alias = "defaultMcpToolPolicy"
|
||||
)]
|
||||
pub default_mcp_tool_policy: Option<McpToolPolicy>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CreateTemplateRequestDto {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut value = Value::deserialize(deserializer)?;
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| serde::de::Error::custom("expected create_template object"))?;
|
||||
let name = take_required_string(object, "name").map_err(serde::de::Error::custom)?;
|
||||
let content = take_required_string(object, "content").map_err(serde::de::Error::custom)?;
|
||||
let default_profile_id =
|
||||
take_required_string(object, "defaultProfileId").map_err(serde::de::Error::custom)?;
|
||||
let default_agent_name = take_optional_value::<String>(object, "defaultAgentName")
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
let default_system_permissions =
|
||||
take_optional_value::<SystemPermissionSet>(object, "defaultSystemPermissions")
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
let default_mcp_tool_policy = take_optional_value_preferred_aliases::<McpToolPolicy>(
|
||||
object,
|
||||
&[
|
||||
"mcpToolPermissions",
|
||||
"defaultMcpToolPermissions",
|
||||
"defaultMcpToolPolicy",
|
||||
],
|
||||
)
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
Ok(Self {
|
||||
name,
|
||||
content,
|
||||
default_profile_id,
|
||||
default_agent_name,
|
||||
default_system_permissions,
|
||||
default_mcp_tool_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateTemplateRequestDto {
|
||||
/// Converts to the use-case input, parsing the profile id.
|
||||
///
|
||||
@ -4366,10 +4399,13 @@ impl<'de> Deserialize<'de> for UpdateTemplateRequestDto {
|
||||
let default_system_permissions =
|
||||
take_optional_value::<SystemPermissionSet>(object, "defaultSystemPermissions")
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let default_mcp_tool_policy = take_optional_value_alias::<McpToolPolicy>(
|
||||
let default_mcp_tool_policy = take_optional_value_preferred_aliases::<McpToolPolicy>(
|
||||
object,
|
||||
"defaultMcpToolPermissions",
|
||||
"defaultMcpToolPolicy",
|
||||
&[
|
||||
"mcpToolPermissions",
|
||||
"defaultMcpToolPermissions",
|
||||
"defaultMcpToolPolicy",
|
||||
],
|
||||
)
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(Self {
|
||||
@ -4434,23 +4470,25 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn take_optional_value_alias<T>(
|
||||
fn take_optional_value_preferred_aliases<T>(
|
||||
object: &mut serde_json::Map<String, Value>,
|
||||
primary_key: &str,
|
||||
legacy_key: &str,
|
||||
keys: &[&str],
|
||||
) -> Result<Option<Option<T>>, String>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let primary = take_optional_value(object, primary_key)?;
|
||||
let legacy = take_optional_value(object, legacy_key)?;
|
||||
match (primary, legacy) {
|
||||
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
|
||||
(None, None) => Ok(None),
|
||||
(Some(_), Some(_)) => Err(format!(
|
||||
"{primary_key} and {legacy_key} cannot both be provided"
|
||||
)),
|
||||
for key in keys {
|
||||
let parsed = take_optional_value(object, key)?;
|
||||
if parsed.is_some() {
|
||||
for ignored_key in keys {
|
||||
if ignored_key != key {
|
||||
object.remove(*ignored_key);
|
||||
}
|
||||
}
|
||||
return Ok(parsed);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Parses a template-id string (UUID) coming from the frontend.
|
||||
|
||||
@ -239,6 +239,46 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => {
|
||||
expect(templates[0].contentMd).toBe("# Updated content");
|
||||
});
|
||||
|
||||
it("reopening an edited template preserves saved MCP tool permissions in the editor", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const tmpl = new MockTemplateGateway(agent);
|
||||
await tmpl.createTemplate({
|
||||
name: "Permissioned",
|
||||
content: "# Initial",
|
||||
defaultProfileId: "",
|
||||
mcpToolPermissions: null,
|
||||
});
|
||||
renderTemplatesPanel({ agent, template: tmpl });
|
||||
|
||||
await screen.findByText("Permissioned");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "edit Permissioned" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("template MCP tool idea_ticket_read")).toBeTruthy();
|
||||
});
|
||||
|
||||
const checkbox = screen.getByLabelText(
|
||||
"template MCP tool idea_ticket_read",
|
||||
) as HTMLInputElement;
|
||||
expect(checkbox.checked).toBe(false);
|
||||
fireEvent.click(checkbox);
|
||||
expect(checkbox.checked).toBe(true);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save template" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByLabelText("template editor")).toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "edit Permissioned" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("template MCP tool idea_ticket_read")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(
|
||||
(screen.getByLabelText("template MCP tool idea_ticket_read") as HTMLInputElement).checked,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("deleting a template removes it from the list", async () => {
|
||||
renderTemplatesPanel();
|
||||
await createTemplate("ToDelete");
|
||||
|
||||
Reference in New Issue
Block a user