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:
@ -197,7 +197,7 @@ describe("CustomAgentChatView", () => {
|
||||
__resetProjectCliInteractionActivityForTests();
|
||||
});
|
||||
|
||||
it("settles an isolated conversation id without recovered turns as unavailable", async () => {
|
||||
it("settles an isolated conversation id without recovered turns as an actionable unavailable state (#307/#314)", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => ({ sessionId: "fresh-session" })),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
@ -209,6 +209,7 @@ describe("CustomAgentChatView", () => {
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
const onConversationId = vi.fn();
|
||||
render(
|
||||
<DIProvider
|
||||
gateways={{
|
||||
@ -226,7 +227,7 @@ describe("CustomAgentChatView", () => {
|
||||
sessionId={null}
|
||||
conversationId="orphan-conversation"
|
||||
onSessionId={vi.fn()}
|
||||
onConversationId={vi.fn()}
|
||||
onConversationId={onConversationId}
|
||||
/>
|
||||
</DIProvider>,
|
||||
);
|
||||
@ -239,13 +240,59 @@ describe("CustomAgentChatView", () => {
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await screen.findByText("Conversation liée — transcript indisponible."),
|
||||
await screen.findByText("La conversation liée n’a pas pu être restaurée."),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("Son transcript n’est plus disponible après la reprise.")).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText("Conversation liée — récupération du transcript en cours."),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole("progressbar")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Réessayer" })).toBeNull();
|
||||
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Délier la conversation" }));
|
||||
expect(onConversationId).toHaveBeenCalledWith(null);
|
||||
expect(await screen.findByText("Envoyez un message pour démarrer la conversation structurée.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("treats emptyConversation as a normal fresh conversation", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => ({
|
||||
sessionId: "fresh-session",
|
||||
transcriptUnavailableReason: "emptyConversation" as const,
|
||||
})),
|
||||
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||
sessionId,
|
||||
scrollback: [],
|
||||
transcriptUnavailableReason: "emptyConversation" as const,
|
||||
})),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
render(
|
||||
<DIProvider gateways={{ agent, system: { pickFile: vi.fn(async () => null) } } as unknown as Gateways}>
|
||||
<CustomAgentChatView projectId="project-1" agentId="agent-1" agentName="Worker" profile={profile} cwd="/repo" nodeId="node-1" sessionId={null} conversationId="conversation-1" onSessionId={vi.fn()} onConversationId={vi.fn()} />
|
||||
</DIProvider>,
|
||||
);
|
||||
expect(await screen.findByText("Envoyez un message pour démarrer la conversation structurée.")).toBeTruthy();
|
||||
expect(screen.queryByText("La conversation liée n’a pas pu être restaurée.")).toBeNull();
|
||||
});
|
||||
|
||||
it("maps NOT_FOUND to an actionable missing conversation without automatic looping", async () => {
|
||||
const agent = {
|
||||
launchAgentChat: vi.fn(async () => { throw { code: "NOT_FOUND", message: "conversation not found" }; }),
|
||||
reattachAgentChat: vi.fn(),
|
||||
sendAgentChat: vi.fn(async () => {}),
|
||||
cancelAgentChat: vi.fn(async () => {}),
|
||||
closeAgentChat: vi.fn(async () => {}),
|
||||
};
|
||||
render(
|
||||
<DIProvider gateways={{ agent, system: { pickFile: vi.fn(async () => null) } } as unknown as Gateways}>
|
||||
<CustomAgentChatView projectId="project-1" agentId="agent-1" agentName="Worker" profile={profile} cwd="/repo" nodeId="node-1" sessionId={null} conversationId="missing-conversation" onSessionId={vi.fn()} onConversationId={vi.fn()} />
|
||||
</DIProvider>,
|
||||
);
|
||||
expect(await screen.findByText("Cette conversation est introuvable dans le stockage disponible.")).toBeTruthy();
|
||||
expect(agent.launchAgentChat).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("projects self-restart lifecycle separators in the custom CLI only once", async () => {
|
||||
|
||||
Reference in New Issue
Block a user