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:
2026-09-11 10:32:01 +02:00
parent 72792bb02b
commit 7007b2cc7e
63 changed files with 3887 additions and 100 deletions

View File

@ -8,9 +8,10 @@
use app_tauri_lib::dto::{
CellKind, ChatAttachmentDto, ChatAttachmentInputDto, ChatDraftScopeRequestDto,
ChatPromptAttachmentDto, ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto,
ReadChatDraftResponseDto, ReattachChatDto, ReplyChunk, ReplyProgressDto, ReplyProgressKindDto,
ReplyProgressSourceDto, ReplyProgressStageDto, SaveChatDraftRequestDto, TerminalSessionDto,
ChatPromptAttachmentDto, ConversationTranscriptUnavailableReasonDto,
ImportChatAttachmentsRequestDto, ImportChatAttachmentsResponseDto, ReadChatDraftResponseDto,
ReattachChatDto, ReplyChunk, ReplyProgressDto, ReplyProgressKindDto, ReplyProgressSourceDto,
ReplyProgressStageDto, SaveChatDraftRequestDto, TerminalSessionDto,
};
use application::{LaunchAgentOutput, StructuredSessionDescriptor};
use domain::project::ProjectPath;
@ -407,10 +408,10 @@ fn import_chat_attachments_request_response_use_camel_case() {
#[test]
fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
let dto = ReattachChatDto {
session_id: "sess-1".into(),
conversation_id: Some("conv-1".into()),
scrollback: vec![
let dto = ReattachChatDto::from_scrollback(
"sess-1".into(),
Some("conv-1".into()),
vec![
ReplyChunk::UserPrompt {
at_ms: None,
text: "Hi".into(),
@ -426,7 +427,8 @@ fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
content: "Hi".into(),
},
],
};
true,
);
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(
v,
@ -445,14 +447,22 @@ fn reattach_chat_dto_serialises_camel_case_with_typed_scrollback() {
#[test]
fn reattach_chat_dto_empty_scrollback_is_empty_array() {
let dto = ReattachChatDto {
session_id: "s".into(),
conversation_id: None,
scrollback: vec![],
};
let dto = ReattachChatDto::from_scrollback("s".into(), None, vec![], true);
let v = serde_json::to_value(&dto).unwrap();
assert_eq!(v["scrollback"], json!([]));
assert!(v.get("conversationId").is_none());
assert_eq!(v["transcriptUnavailableReason"], "missingTranscript");
assert_eq!(
dto.transcript_unavailable_reason,
Some(ConversationTranscriptUnavailableReasonDto::MissingTranscript)
);
}
#[test]
fn reattach_chat_dto_distinguishes_fresh_empty_conversation() {
let dto = ReattachChatDto::from_scrollback("s".into(), None, vec![], false);
let v = serde_json::to_value(dto).unwrap();
assert_eq!(v["transcriptUnavailableReason"], "emptyConversation");
}
// ---------------------------------------------------------------------------