Files
IdeA/frontend/src/features/scheduled-tasks/scheduledTasks.test.tsx
Blomios 7007b2cc7e 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.
2026-09-11 10:32:01 +02:00

65 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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());
});
});