feat(backend,domain,infrastructure,application,app-tauri,web-server,frontend): suppression d'un projet du registre IdeA sans toucher son dossier disque — #308 (QA verte)

cause: l'utilisateur ne pouvait pas retirer un projet du registre IdeA (projects.json) ; la seule issue laissait l'entrée fantôme dans la liste des projets, sans capacité générique de retrait.

domain: port ProjectStore::remove_project(id) -> Option<Project> avec implémentation par défaut « not supported » pour ne pas casser les stores spécialisés.

infrastructure: FsProjectStore implémente remove_project — retire l'entrée du registre persisté et la renvoie, None si id inconnu, ordre relatif du registre préservé ; test store dédié.

application: use case RemoveProjectFromRegistry (remove.rs) — NotFound explicite si id absent, retourne le projet retiré + registre rafraîchi.

backend/app-tauri/web-server: DTO RemoveProjectRequestDto/RemoveProjectResponseDto (removed, projects, restore_state, was_open), adapter partagé remove_project_for_adapter réutilisé par la commande Tauri remove_project et la route HTTP — même contrat desktop/web ; remove_project_and_deactivate désactive le projet s'il était ouvert, purge l'entrée open-projects restore correspondante et renvoie was_open/restore_state ; test adapter vérifiant le contrat de bout en bout (entrée retirée, restore purgé, root intact).

frontend: DTO + ProjectGateway.removeProject (HTTP + mock), ProjectsView expose un menu d'actions projet (ProjectActionsMenu) avec entrée Retirer, RemoveProjectDialog — confirmation explicite précisant que le dossier disque n'est pas supprimé, invalidation/rechargement de la liste des projets après succès (useProjects), tests ProjectsView/projects/adapters/httpInvoker/services.

QA verte: cargo targeted tests/check OK (domain, application project_usecases, infrastructure project_store, backend, app-tauri, web-server), npm targeted/typecheck/build OK ; suite npm test complète rouge non bloquante hors #308 (échecs préexistants listProjectSettings/web). Vérifications Git: git diff --check OK, cargo fmt --all -- --check OK.

fichiers .ideai/tickets/308/, tickets/counter.json, tickets/index.json, attachments/ et scripts/ hors périmètre et préservés en worktree (sync store traité séparément) ; aucun push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 18:27:15 +02:00
parent ca1a27a2b9
commit b9f8ed303e
25 changed files with 1009 additions and 29 deletions

View File

@ -240,6 +240,18 @@ describe("request/response gateways preserve the Tauri command contract", () =>
});
});
it("HttpProjectGateway.removeProject maps to the common command envelope", async () => {
const http = new HttpInvoker({ baseUrl: "https://h" });
const spy = vi.spyOn(http, "invoke").mockResolvedValue({} as never);
const gw = new HttpProjectGateway(http);
await gw.removeProject("project-1");
expect(spy).toHaveBeenCalledWith("remove_project", {
request: { projectId: "project-1" },
});
});
it("HttpProjectGateway maps the open-project restore command envelopes", async () => {
const http = new HttpInvoker({ baseUrl: "https://h" });
const spy = vi.spyOn(http, "invoke").mockResolvedValue({} as never);

View File

@ -39,6 +39,7 @@ import type {
Project,
ProjectFileListing,
OpenProjectRestoreState,
RemoveProjectOutput,
RestoreOpenProjectsOutput,
ProjectMcpToolPermissions,
ProjectPermissions,
@ -99,6 +100,11 @@ export class HttpProjectGateway implements ProjectGateway {
async closeProject(projectId: string): Promise<void> {
await this.http.invoke("close_project", { projectId });
}
removeProject(projectId: string): Promise<RemoveProjectOutput> {
return this.http.invoke<RemoveProjectOutput>("remove_project", {
request: { projectId },
});
}
restoreOpenProjects(): Promise<RestoreOpenProjectsOutput> {
return this.http.invoke<RestoreOpenProjectsOutput>("restore_open_projects");
}

View File

@ -72,6 +72,7 @@ import type {
ProjectFileListing,
OpenProjectRestoreFailure,
OpenProjectRestoreState,
RemoveProjectOutput,
RestoreOpenProjectsOutput,
JsonValue,
ProjectMcpToolPermissions,
@ -1314,6 +1315,28 @@ export class MockProjectGateway implements ProjectGateway {
this.restoreFailures.delete(projectId);
}
async removeProject(projectId: string): Promise<RemoveProjectOutput> {
const index = this.projects.findIndex((project) => project.id === projectId);
if (index < 0) {
const err: GatewayError = {
code: "NOT_FOUND",
message: `project ${projectId} not found`,
};
throw err;
}
const [removed] = this.projects.splice(index, 1);
const wasOpen = this.restoreProjectIds.includes(projectId);
this.restoreProjectIds = this.restoreProjectIds.filter((id) => id !== projectId);
this.restoreFailures.delete(projectId);
this.contexts.delete(projectId);
return {
removed,
projects: [...this.projects],
restoreState: await this.readOpenProjectRestoreState(),
wasOpen,
};
}
async restoreOpenProjects(): Promise<RestoreOpenProjectsOutput> {
const failures: OpenProjectRestoreFailure[] = [];
const opened: Project[] = [];

View File

@ -53,4 +53,14 @@ describe("TauriProjectGateway invoke payloads", () => {
await gateway.readOpenProjectRestoreState();
expect(invoke).toHaveBeenCalledWith("read_open_project_restore_state");
});
it("removes a project through the shared request envelope", async () => {
const gateway = new TauriProjectGateway();
await gateway.removeProject("proj-1");
expect(invoke).toHaveBeenCalledWith("remove_project", {
request: { projectId: "proj-1" },
});
});
});

View File

@ -12,6 +12,7 @@ import type {
OpenProjectRestoreState,
Project,
ProjectFileListing,
RemoveProjectOutput,
RestoreOpenProjectsOutput,
} from "@/domain";
import type { ProjectGateway } from "@/ports";
@ -33,6 +34,12 @@ export class TauriProjectGateway implements ProjectGateway {
await invoke("close_project", { projectId });
}
removeProject(projectId: string): Promise<RemoveProjectOutput> {
return invoke<RemoveProjectOutput>("remove_project", {
request: { projectId },
});
}
restoreOpenProjects(): Promise<RestoreOpenProjectsOutput> {
return invoke<RestoreOpenProjectsOutput>("restore_open_projects");
}