fix(templates): les permissions des templates survivent à la sauvegarde/réouverture — create/update_template envoient defaultSystemPermissions (alias DTO backend) et les réponses list/create/update sont normalisées (defaultSystemPermissions/defaultMcpToolPermissions → clés frontend) via templateNormalization — #259 (QA verte)

This commit is contained in:
2026-09-03 14:31:57 +02:00
parent 580977faf0
commit 4d34adeab5
4 changed files with 118 additions and 21 deletions

View File

@ -8,7 +8,11 @@ import { describe, it, expect, vi } from "vitest";
import type { FetchLike } from "./httpInvoker";
import { HttpInvoker, defaultFetch } from "./httpInvoker";
import { HttpProjectGateway, HttpGitGateway } from "./requestResponseGateways";
import {
HttpProjectGateway,
HttpGitGateway,
HttpTemplateGateway,
} from "./requestResponseGateways";
/**
* Installs a global `fetch` that records its receiver (`this`), runs `body`, then
@ -251,4 +255,76 @@ describe("request/response gateways preserve the Tauri command contract", () =>
expect(spy).toHaveBeenCalledWith("git_status", { projectId: "p1" });
});
it("HttpTemplateGateway normalizes saved default MCP tool permissions from backend DTOs", async () => {
const http = new HttpInvoker({ baseUrl: "https://h" });
const template = {
id: "t1",
name: "Permissioned",
contentMd: "# Body",
version: 1,
defaultProfileId: "p1",
defaultSystemPermissions: { network: "allow" },
defaultMcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
};
const spy = vi
.spyOn(http, "invoke")
.mockResolvedValueOnce([template] as never)
.mockResolvedValueOnce(template as never)
.mockResolvedValueOnce(template as never);
const gw = new HttpTemplateGateway(http);
await expect(gw.listTemplates()).resolves.toMatchObject([
{
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
},
]);
await expect(
gw.createTemplate({
name: "Permissioned",
content: "# Body",
defaultProfileId: "p1",
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
}),
).resolves.toMatchObject({
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
});
await expect(
gw.updateTemplate("t1", {
name: "Permissioned",
content: "# Body",
defaultProfileId: "p1",
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
}),
).resolves.toMatchObject({
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
});
expect(spy).toHaveBeenNthCalledWith(2, "create_template", {
request: {
name: "Permissioned",
content: "# Body",
defaultProfileId: "p1",
defaultAgentName: null,
defaultSystemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
},
});
expect(spy).toHaveBeenNthCalledWith(3, "update_template", {
request: {
templateId: "t1",
name: "Permissioned",
content: "# Body",
defaultProfileId: "p1",
defaultAgentName: null,
defaultSystemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
},
});
});
});

View File

@ -80,6 +80,7 @@ import { normalizeProjectWorkState } from "../workStateNormalization";
import { unsupportedOnWeb } from "./unsupported";
import { normalizeTurnPage } from "../conversationNormalization";
import { normalizeProfileModelCatalog } from "../profileCatalog";
import { normalizeTemplate, normalizeTemplates } from "../templateNormalization";
import type { HttpInvoker } from "./httpInvoker";
export class HttpProjectGateway implements ProjectGateway {
@ -282,33 +283,33 @@ export class HttpModelServerGateway implements ModelServerGateway {
export class HttpTemplateGateway implements TemplateGateway {
constructor(private readonly http: HttpInvoker) {}
listTemplates(): Promise<Template[]> {
return this.http.invoke<Template[]>("list_templates");
async listTemplates(): Promise<Template[]> {
return normalizeTemplates(await this.http.invoke<Template[]>("list_templates"));
}
createTemplate(input: CreateTemplateInput): Promise<Template> {
return this.http.invoke<Template>("create_template", {
async createTemplate(input: CreateTemplateInput): Promise<Template> {
return normalizeTemplate(await this.http.invoke<Template>("create_template", {
request: {
name: input.name,
content: input.content,
defaultProfileId: input.defaultProfileId,
defaultAgentName: input.defaultAgentName ?? null,
systemPermissions: input.systemPermissions ?? null,
defaultSystemPermissions: input.systemPermissions ?? null,
mcpToolPermissions: input.mcpToolPermissions ?? null,
},
});
}));
}
updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
return this.http.invoke<Template>("update_template", {
async updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
return normalizeTemplate(await this.http.invoke<Template>("update_template", {
request: {
templateId,
name: input.name,
content: input.content,
defaultProfileId: input.defaultProfileId,
defaultAgentName: input.defaultAgentName ?? null,
systemPermissions: input.systemPermissions ?? null,
defaultSystemPermissions: input.systemPermissions ?? null,
mcpToolPermissions: input.mcpToolPermissions ?? null,
},
});
}));
}
async deleteTemplate(templateId: string): Promise<void> {
await this.http.invoke("delete_template", { templateId });

View File

@ -14,37 +14,38 @@ import { invoke } from "@tauri-apps/api/core";
import type { Agent, AgentDrift, Template } from "@/domain";
import type { CreateTemplateInput, TemplateGateway, UpdateTemplateInput } from "@/ports";
import { normalizeTemplate, normalizeTemplates } from "./templateNormalization";
export class TauriTemplateGateway implements TemplateGateway {
listTemplates(): Promise<Template[]> {
return invoke<Template[]>("list_templates");
async listTemplates(): Promise<Template[]> {
return normalizeTemplates(await invoke<Template[]>("list_templates"));
}
createTemplate(input: CreateTemplateInput): Promise<Template> {
return invoke<Template>("create_template", {
async createTemplate(input: CreateTemplateInput): Promise<Template> {
return normalizeTemplate(await invoke<Template>("create_template", {
request: {
name: input.name,
content: input.content,
defaultProfileId: input.defaultProfileId,
defaultAgentName: input.defaultAgentName ?? null,
systemPermissions: input.systemPermissions ?? null,
defaultSystemPermissions: input.systemPermissions ?? null,
mcpToolPermissions: input.mcpToolPermissions ?? null,
},
});
}));
}
updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
return invoke<Template>("update_template", {
async updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
return normalizeTemplate(await invoke<Template>("update_template", {
request: {
templateId,
name: input.name,
content: input.content,
defaultProfileId: input.defaultProfileId,
defaultAgentName: input.defaultAgentName ?? null,
systemPermissions: input.systemPermissions ?? null,
defaultSystemPermissions: input.systemPermissions ?? null,
mcpToolPermissions: input.mcpToolPermissions ?? null,
},
});
}));
}
async deleteTemplate(templateId: string): Promise<void> {

View File

@ -0,0 +1,19 @@
import type { McpToolPolicy, SystemPermissionSet, Template } from "@/domain";
type TemplateDto = Template & {
defaultSystemPermissions?: SystemPermissionSet | null;
defaultMcpToolPermissions?: McpToolPolicy | null;
};
export function normalizeTemplate(template: TemplateDto): Template {
const { defaultSystemPermissions, defaultMcpToolPermissions, ...rest } = template;
return {
...rest,
systemPermissions: rest.systemPermissions ?? defaultSystemPermissions ?? null,
mcpToolPermissions: rest.mcpToolPermissions ?? defaultMcpToolPermissions ?? null,
};
}
export function normalizeTemplates(templates: TemplateDto[]): Template[] {
return templates.map(normalizeTemplate);
}