domain: variante ProjectIconSet::Custom, métadonnées CustomProjectIconAsset (id opaque, media type png/jpeg/webp, byte_size, dimensions décodées, sha256, nom de fichier original sans chemin, created_at), CustomProjectIconContent et port CustomProjectIconAssetStore (list/read/exists/create/delete) avec CustomProjectIconStoreError dédiée. application: use case project_icon_assets — import validé png/jpeg/webp avec rejet du contenu SVG, dimensionnement et digest sha256, bibliothèque partagée à l'instance indépendante de tout projet, suppression refusée tant que l'asset est référencé par un projet. infrastructure: store fichier store/project_icon_asset.rs — index canonique + octets, création atomique sous extension validée. backend/web-server: contrat HTTP /api/project-icon-assets (GET liste, POST import, GET lecture, GET /usage, DELETE) avec prévol OPTIONS et tests contrat incluant le rejet SVG. app-tauri: commandes import/list/read/get_usage/delete_custom_project_icon_asset exposées au frontend. frontend: port ProjectIconAssetGateway (list/import/delete/getUrl sans exposer de chemin filesystem), gateways http+mock avec tests, DI, section Icônes personnalisées du ProjectIconPicker (import fichier accept png/jpeg/webp, sélection role=option, suppression avec retrait de sélection, erreurs lisibles i18n FR/EN), tests projectIcons étendus. QA verte: contrats HTTP et Tauri identiques, suites cargo et npm ciblées vertes.
150 lines
4.5 KiB
TypeScript
150 lines
4.5 KiB
TypeScript
/**
|
|
* L1 — each mock gateway satisfies its port interface (typecheck via the typed
|
|
* `Gateways` binding below) and behaves sensibly offline.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import type { DomainEvent } from "@/domain";
|
|
import type { Gateways } from "@/ports";
|
|
import { createMockGateways, MockAgentGateway, MockSystemGateway } from "./index";
|
|
|
|
// Typechecking this assignment proves every mock implements its port.
|
|
const gateways: Gateways = createMockGateways();
|
|
|
|
describe("createMockGateways", () => {
|
|
it("exposes all gateways", () => {
|
|
expect(Object.keys(gateways).sort()).toEqual([
|
|
"agent",
|
|
"conversation",
|
|
"desktopServer",
|
|
"device",
|
|
"embedder",
|
|
"focusedProject",
|
|
"git",
|
|
"input",
|
|
"layout",
|
|
"memory",
|
|
"modelServer",
|
|
"permission",
|
|
"plugin",
|
|
"pluginConfig",
|
|
"pluginEvents",
|
|
"pluginStorage",
|
|
"pluginTask",
|
|
"pluginToolchain",
|
|
"pluginWorkspace",
|
|
"profile",
|
|
"project",
|
|
"projectIconAsset",
|
|
"remote",
|
|
"scheduledTask",
|
|
"skill",
|
|
"system",
|
|
"template",
|
|
"terminal",
|
|
"ticket",
|
|
"uiPreferences",
|
|
"userSettings",
|
|
"window",
|
|
"workState",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("MockSystemGateway", () => {
|
|
it("health returns an alive report echoing the note", async () => {
|
|
const sys = new MockSystemGateway();
|
|
const report = await sys.health("ping");
|
|
expect(report.alive).toBe(true);
|
|
expect(report.note).toBe("ping");
|
|
expect(typeof report.version).toBe("string");
|
|
expect(typeof report.timeMillis).toBe("number");
|
|
expect(typeof report.correlationId).toBe("string");
|
|
});
|
|
|
|
it("health note defaults to null when omitted", async () => {
|
|
const report = await new MockSystemGateway().health();
|
|
expect(report.note).toBeNull();
|
|
});
|
|
|
|
it("delivers emitted domain events to subscribers and stops after unsubscribe", async () => {
|
|
const sys = new MockSystemGateway();
|
|
const received: DomainEvent[] = [];
|
|
const unsub = await sys.onDomainEvent((e) => received.push(e));
|
|
|
|
sys.emit({ type: "projectCreated", projectId: "p1" });
|
|
expect(received).toHaveLength(1);
|
|
expect(received[0]).toEqual({ type: "projectCreated", projectId: "p1" });
|
|
|
|
unsub();
|
|
sys.emit({ type: "projectCreated", projectId: "p2" });
|
|
expect(received).toHaveLength(1);
|
|
});
|
|
|
|
it("health emits a smoke domain event to subscribers", async () => {
|
|
const sys = new MockSystemGateway();
|
|
const received: DomainEvent[] = [];
|
|
await sys.onDomainEvent((e) => received.push(e));
|
|
await sys.health();
|
|
expect(received.some((e) => e.type === "projectCreated")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("other mock gateways", () => {
|
|
it("schedules a restart only for the agent runtime bound to the exact cell", async () => {
|
|
const agentGateway = new MockAgentGateway();
|
|
const agent = await agentGateway.createAgent("p1", {
|
|
name: "Worker",
|
|
profileId: "profile-1",
|
|
});
|
|
await agentGateway.launchAgent(
|
|
"p1",
|
|
agent.id,
|
|
{ cwd: "/p1", rows: 24, cols: 80, nodeId: "node-1" },
|
|
() => {},
|
|
);
|
|
|
|
await expect(
|
|
agentGateway.restartCellAgentRuntime({
|
|
projectId: "p1",
|
|
nodeId: "node-1",
|
|
agentId: agent.id,
|
|
presentation: "tui",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
status: "scheduled",
|
|
agentId: agent.id,
|
|
nodeId: "node-1",
|
|
presentation: "tui",
|
|
});
|
|
await expect(
|
|
agentGateway.restartCellAgentRuntime({
|
|
projectId: "p1",
|
|
nodeId: "stale-node",
|
|
agentId: agent.id,
|
|
presentation: "tui",
|
|
}),
|
|
).rejects.toMatchObject({ code: "AGENT_NOT_BOUND_TO_CELL" });
|
|
});
|
|
|
|
it("project.createProject returns a project and listProjects reflects it", async () => {
|
|
expect(await gateways.project.listProjects()).toEqual([]);
|
|
const created = await gateways.project.createProject("n", "/root");
|
|
expect(created.name).toBe("n");
|
|
expect(created.root).toBe("/root");
|
|
expect(typeof created.id).toBe("string");
|
|
});
|
|
|
|
it("terminal.openTerminal returns handles with incrementing session ids", async () => {
|
|
const a = await gateways.terminal.openTerminal({ cwd: "/cwd", rows: 24, cols: 80 }, () => {});
|
|
const b = await gateways.terminal.openTerminal({ cwd: "/cwd", rows: 24, cols: 80 }, () => {});
|
|
expect(a.sessionId).not.toEqual(b.sessionId);
|
|
});
|
|
|
|
it("git.branches returns main as current branch", async () => {
|
|
const b = await gateways.git.branches("p");
|
|
expect(b.current).toBe("main");
|
|
expect(b.branches).toContain("main");
|
|
});
|
|
});
|