feat(backend,frontend): tâches planifiées projet — tool MCP idea_scheduled_task_* et menu utilisateur #305, fix transcript lié indisponible après reboot #314 (QA verte)
#305: nouvel agrégat durable ScheduledProjectTask dans domain (scheduled_task.rs, ids, ports ScheduledProjectTaskStore), source de vérité <app_data_dir>/scheduled-project-tasks/<projectId>.json, schedules bornés V1 interval/daily/weekly avec fuseau, cycle À approuver/Active/Suspendue avec approbation obligatoire des tâches demandées par agent; use cases application/scheduled_task (list/create/update/delete/run_now/reconcile/fire) réutilisant BackgroundTask pour les runs avec distinction planifié/manuel; store infrastructure dédié + scheduler reconcile au boot sans rattrapage en rafale, cwd confiné au projet; 7 tools MCP idea_scheduled_task_* (list/read lecture seule, create/update/delete/run_now/set_enabled write-actions soumises aux permissions natives); commandes Tauri + routes HTTP aux contrats DTO identiques (camelCase nextRunAtMs/lastRunAtMs/lastTaskId/createdBy/updatedBy); frontend: gateways Tauri/HTTP/mock + types ports/domain, surface projet 'Tâches planifiées' (liste utilisateur+agent unifiée, création/édition avec récapitulatif avant activation, approbation/refus, exécuter maintenant, suspendre/reprendre, suppression), catalogage surface web (webSurfaceCatalog/viewPlacement/ViewPanelBody/WebWorkspace); docs/scheduled-project-tasks.md et sdk docs/services.md (047c38b: pas un service plugin V1, pas d'émulation via cron système). #314: correction de 'Conversation liée — transcript indisponible' après reboot: restauration du transcript lié côté application (orchestrator service, lifecycle, session_limit, background sink, terminal registry) et côté frontend (adapters agent/http frames/stream/requestResponse, workStateNormalization, CustomAgentChatView avec état et action utile), tests dto_chat/agent_lifecycle/structured_registry_d1/background_completion_sink étendus. QA verte rapportée par QA sur feature/305-scheduled-project-tasks-mcp, sans bloquant, pour #305 et #314. Hors périmètre worktree: tickets non liés, attachments/, scripts/; aucun push.
This commit is contained in:
@ -0,0 +1,64 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockScheduledTaskGateway,
|
||||
MockSystemGateway,
|
||||
MockWorkStateGateway,
|
||||
} from "@/adapters/mock";
|
||||
import type { ScheduledProjectTask } from "@/domain";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { ScheduledTasksPanel } from "./ScheduledTasksPanel";
|
||||
|
||||
const PROJECT = "project-schedules";
|
||||
|
||||
function renderPanel(scheduledTask = new MockScheduledTaskGateway()) {
|
||||
const gateways = {
|
||||
scheduledTask,
|
||||
agent: new MockAgentGateway(),
|
||||
system: new MockSystemGateway(),
|
||||
workState: new MockWorkStateGateway(),
|
||||
} as unknown as Gateways;
|
||||
return { scheduledTask, ...render(<DIProvider gateways={gateways}><ScheduledTasksPanel projectId={PROJECT} /></DIProvider>) };
|
||||
}
|
||||
|
||||
describe("ScheduledTasksPanel", () => {
|
||||
it("shows the prescribed empty state and creates only after security review", async () => {
|
||||
const { scheduledTask } = renderPanel();
|
||||
expect(await screen.findByText("Aucune tâche planifiée pour ce projet")).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Nouvelle tâche planifiée" }));
|
||||
const dialog = screen.getByRole("dialog", { name: "Nouvelle tâche planifiée" });
|
||||
fireEvent.change(within(dialog).getByLabelText("Nom"), { target: { value: "Nettoyage" } });
|
||||
fireEvent.change(within(dialog).getByLabelText("Commande"), { target: { value: "npm" } });
|
||||
fireEvent.change(within(dialog).getByLabelText("Arguments"), { target: { value: "run\nclean" } });
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Vérifier avant activation" }));
|
||||
expect(within(dialog).getByText("Cette tâche pourra modifier ou supprimer des fichiers automatiquement.")).toBeTruthy();
|
||||
expect(within(dialog).getByText("npm run clean")).toBeTruthy();
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Créer la tâche" }));
|
||||
await waitFor(async () => expect(await scheduledTask.list(PROJECT)).toHaveLength(1));
|
||||
expect(await screen.findByText("Nettoyage")).toBeTruthy();
|
||||
expect(screen.getByText("Active")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("surfaces an agent proposal and supports refusal", async () => {
|
||||
const scheduledTask = new MockScheduledTaskGateway();
|
||||
const now = Date.now();
|
||||
const pending: ScheduledProjectTask = {
|
||||
id: "agent-request", projectId: PROJECT, name: "Purge", description: "Limiter la taille du build",
|
||||
enabled: false, approval: "pending", sandbox: "projectReadWrite",
|
||||
command: { command: "find", args: ["dist", "-delete"] },
|
||||
schedule: { kind: "daily", time: "02:00" }, timezone: "Europe/Paris",
|
||||
nextRunAtMs: null, createdBy: { kind: "agent", agentId: "agent-1" },
|
||||
updatedBy: { kind: "agent", agentId: "agent-1" }, createdAtMs: now,
|
||||
updatedAtMs: now, auditRevision: 1,
|
||||
};
|
||||
scheduledTask._seed(pending);
|
||||
renderPanel(scheduledTask);
|
||||
expect(await screen.findByText("À approuver")).toBeTruthy();
|
||||
expect(screen.getByText(/Cette tâche ne s’exécutera pas avant votre approbation/)).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Refuser" }));
|
||||
await waitFor(() => expect(screen.getByText("Suspendue")).toBeTruthy());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user