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

@ -70,6 +70,23 @@ fn running_task(id: u128) -> BackgroundTask {
.unwrap()
}
fn running_record_only_task(id: u128) -> BackgroundTask {
BackgroundTask::new(
task_id(id),
project_id(10),
agent_id(100),
BackgroundTaskKind::Command {
label: format!("task-{id}"),
},
BackgroundTaskWakePolicy::RecordOnly,
1_000,
None,
)
.unwrap()
.transition(BackgroundTaskState::Running, 1_010)
.unwrap()
}
fn success_completion(id: u128) -> BackgroundTaskCompletion {
BackgroundTaskCompletion {
task_id: task_id(id),
@ -138,6 +155,30 @@ async fn persistence_happens_before_ready_signal() {
assert_eq!(*order.lock().unwrap(), vec!["save", "ready"]);
}
#[tokio::test]
async fn record_only_completion_is_persisted_delivered_without_signal() {
let tmp = TempDir::new();
let store = Arc::new(FsBackgroundTaskStore::new(&tmp.project_path()));
let task = running_record_only_task(2);
store.create(&task).await.unwrap();
let (ready_tx, mut ready_rx) = unbounded_channel();
let sink = BackgroundCompletionSink::new(store.clone(), ready_tx);
let outcome = sink
.process_completion(success_completion(2))
.await
.unwrap();
assert_eq!(
outcome,
BackgroundCompletionSinkOutcome::PersistedRecordOnly { task_id: task.id }
);
assert!(ready_rx.try_recv().is_err());
let persisted = store.get(task.id).await.unwrap().unwrap();
assert_eq!(persisted.state, BackgroundTaskState::Completed);
assert!(persisted.completion_delivered);
}
#[tokio::test]
async fn duplicate_completion_same_task_signals_once() {
let tmp = TempDir::new();