merge(templates): permissions des templates persistantes après save/reopen — #259 (QA verte)
This commit is contained in:
@ -8,7 +8,11 @@ import { describe, it, expect, vi } from "vitest";
|
|||||||
|
|
||||||
import type { FetchLike } from "./httpInvoker";
|
import type { FetchLike } from "./httpInvoker";
|
||||||
import { HttpInvoker, defaultFetch } 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
|
* 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" });
|
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"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -80,6 +80,7 @@ import { normalizeProjectWorkState } from "../workStateNormalization";
|
|||||||
import { unsupportedOnWeb } from "./unsupported";
|
import { unsupportedOnWeb } from "./unsupported";
|
||||||
import { normalizeTurnPage } from "../conversationNormalization";
|
import { normalizeTurnPage } from "../conversationNormalization";
|
||||||
import { normalizeProfileModelCatalog } from "../profileCatalog";
|
import { normalizeProfileModelCatalog } from "../profileCatalog";
|
||||||
|
import { normalizeTemplate, normalizeTemplates } from "../templateNormalization";
|
||||||
import type { HttpInvoker } from "./httpInvoker";
|
import type { HttpInvoker } from "./httpInvoker";
|
||||||
|
|
||||||
export class HttpProjectGateway implements ProjectGateway {
|
export class HttpProjectGateway implements ProjectGateway {
|
||||||
@ -282,33 +283,33 @@ export class HttpModelServerGateway implements ModelServerGateway {
|
|||||||
|
|
||||||
export class HttpTemplateGateway implements TemplateGateway {
|
export class HttpTemplateGateway implements TemplateGateway {
|
||||||
constructor(private readonly http: HttpInvoker) {}
|
constructor(private readonly http: HttpInvoker) {}
|
||||||
listTemplates(): Promise<Template[]> {
|
async listTemplates(): Promise<Template[]> {
|
||||||
return this.http.invoke<Template[]>("list_templates");
|
return normalizeTemplates(await this.http.invoke<Template[]>("list_templates"));
|
||||||
}
|
}
|
||||||
createTemplate(input: CreateTemplateInput): Promise<Template> {
|
async createTemplate(input: CreateTemplateInput): Promise<Template> {
|
||||||
return this.http.invoke<Template>("create_template", {
|
return normalizeTemplate(await this.http.invoke<Template>("create_template", {
|
||||||
request: {
|
request: {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
defaultProfileId: input.defaultProfileId,
|
defaultProfileId: input.defaultProfileId,
|
||||||
defaultAgentName: input.defaultAgentName ?? null,
|
defaultAgentName: input.defaultAgentName ?? null,
|
||||||
systemPermissions: input.systemPermissions ?? null,
|
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
|
async updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
|
||||||
return this.http.invoke<Template>("update_template", {
|
return normalizeTemplate(await this.http.invoke<Template>("update_template", {
|
||||||
request: {
|
request: {
|
||||||
templateId,
|
templateId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
defaultProfileId: input.defaultProfileId,
|
defaultProfileId: input.defaultProfileId,
|
||||||
defaultAgentName: input.defaultAgentName ?? null,
|
defaultAgentName: input.defaultAgentName ?? null,
|
||||||
systemPermissions: input.systemPermissions ?? null,
|
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
async deleteTemplate(templateId: string): Promise<void> {
|
async deleteTemplate(templateId: string): Promise<void> {
|
||||||
await this.http.invoke("delete_template", { templateId });
|
await this.http.invoke("delete_template", { templateId });
|
||||||
|
|||||||
@ -14,37 +14,38 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
|
|
||||||
import type { Agent, AgentDrift, Template } from "@/domain";
|
import type { Agent, AgentDrift, Template } from "@/domain";
|
||||||
import type { CreateTemplateInput, TemplateGateway, UpdateTemplateInput } from "@/ports";
|
import type { CreateTemplateInput, TemplateGateway, UpdateTemplateInput } from "@/ports";
|
||||||
|
import { normalizeTemplate, normalizeTemplates } from "./templateNormalization";
|
||||||
|
|
||||||
export class TauriTemplateGateway implements TemplateGateway {
|
export class TauriTemplateGateway implements TemplateGateway {
|
||||||
listTemplates(): Promise<Template[]> {
|
async listTemplates(): Promise<Template[]> {
|
||||||
return invoke<Template[]>("list_templates");
|
return normalizeTemplates(await invoke<Template[]>("list_templates"));
|
||||||
}
|
}
|
||||||
|
|
||||||
createTemplate(input: CreateTemplateInput): Promise<Template> {
|
async createTemplate(input: CreateTemplateInput): Promise<Template> {
|
||||||
return invoke<Template>("create_template", {
|
return normalizeTemplate(await invoke<Template>("create_template", {
|
||||||
request: {
|
request: {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
defaultProfileId: input.defaultProfileId,
|
defaultProfileId: input.defaultProfileId,
|
||||||
defaultAgentName: input.defaultAgentName ?? null,
|
defaultAgentName: input.defaultAgentName ?? null,
|
||||||
systemPermissions: input.systemPermissions ?? null,
|
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
|
async updateTemplate(templateId: string, input: UpdateTemplateInput): Promise<Template> {
|
||||||
return invoke<Template>("update_template", {
|
return normalizeTemplate(await invoke<Template>("update_template", {
|
||||||
request: {
|
request: {
|
||||||
templateId,
|
templateId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
defaultProfileId: input.defaultProfileId,
|
defaultProfileId: input.defaultProfileId,
|
||||||
defaultAgentName: input.defaultAgentName ?? null,
|
defaultAgentName: input.defaultAgentName ?? null,
|
||||||
systemPermissions: input.systemPermissions ?? null,
|
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTemplate(templateId: string): Promise<void> {
|
async deleteTemplate(templateId: string): Promise<void> {
|
||||||
|
|||||||
19
frontend/src/adapters/templateNormalization.ts
Normal file
19
frontend/src/adapters/templateNormalization.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user