Files
Loki/internal/loki/tasks_once_test.go
MichaelandClaude Opus 5.5 2bbf0ba76d Tâches : « rappelle-moi dans 20 minutes » sans cron, à l'heure du navigateur
Repris d'AJEAN 0.17.5.

- task_create accepte in_minutes (dans N minutes) ou at (« HH:MM », prochaine
  occurrence, ou « AAAA-MM-JJ HH:MM ») pour une tâche unique, calculée côté
  serveur : le modèle n'a plus à écrire un cron ni à jongler avec les
  fuseaux pour un simple rappel. Elle s'écrit « @once AAAA-MM-JJ HH:MM » et
  se désactive après son passage ; manquée pendant un arrêt, elle part au
  redémarrage.
- Le navigateur envoie son fuseau (Europe/Paris…) avec chaque message ; il
  est retenu et sert aux tâches que l'IA planifie. Le conteneur tourne en
  UTC : « à 17h » tombait deux heures à côté. La réponse de task_create
  nomme le fuseau utilisé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:25:04 +02:00

69 lines
2.3 KiB
Go

package loki
import (
"strings"
"testing"
"time"
)
// « @once » : une date future est acceptée, une date passée refusée.
func TestScheduleOnce(t *testing.T) {
from := time.Date(2026, 10, 2, 12, 0, 0, 0, time.UTC)
at, err := nextAfter("@once 2026-10-02 12:30", "UTC", from)
if err != nil || !at.Equal(from.Add(30*time.Minute)) {
t.Fatalf("@once futur : %v, %v", at, err)
}
if _, err := nextAfter("@once 2026-10-02 11:00", "UTC", from); err == nil {
t.Fatal("@once passé accepté")
}
if !isOnce(" @ONCE 2026-10-02 12:30") || isOnce("@every 2h") {
t.Fatal("isOnce se trompe")
}
}
// L'IA crée un rappel « dans 20 minutes » ou « à HH:MM » sans écrire de cron,
// dans le fuseau du navigateur ; une fois passée, la tâche se désactive.
func TestTacheUniqueParLIA(t *testing.T) {
testHome(t)
rememberUserTZ("Europe/Paris")
if got := defaultTaskTZ(); got != "Europe/Paris" {
t.Fatalf("fuseau retenu = %q", got)
}
rememberUserTZ("Pas/Un_Fuseau") // invalide : ignoré
if got := defaultTaskTZ(); got != "Europe/Paris" {
t.Fatalf("fuseau invalide retenu : %q", got)
}
out := toolTaskCreate(map[string]any{"name": "rappel", "prompt": "dis bonjour", "in_minutes": float64(20)})
if !strings.HasPrefix(out, "[ok]") || !strings.Contains(out, "Europe/Paris") {
t.Fatalf("création « dans 20 min » : %s", out)
}
out = toolTaskCreate(map[string]any{"name": "apéro", "prompt": "rappelle l'apéro", "at": "18:30"})
if !strings.HasPrefix(out, "[ok]") {
t.Fatalf("création « à 18:30 » : %s", out)
}
if out := toolTaskCreate(map[string]any{"name": "flou", "prompt": "x", "at": "demain"}); !strings.HasPrefix(out, "[erreur]") {
t.Fatalf("'at' invalide accepté : %s", out)
}
var once Task
for _, tk := range listTasks() {
if tk.Name == "rappel" {
once = tk
}
}
if !isOnce(once.Schedule) || once.TZ != "Europe/Paris" || once.NextRun == 0 {
t.Fatalf("tâche unique mal posée : %+v", once)
}
want := time.Now().Add(20 * time.Minute)
if d := time.UnixMilli(once.NextRun).Sub(want); d > time.Minute || d < -time.Minute {
t.Fatalf("prochaine exécution à %v, attendu ≈ %v", time.UnixMilli(once.NextRun), want)
}
recordTaskEnd(once.ID, time.Now(), "bonjour", nil)
after, _ := getTask(once.ID)
if after.Enabled || after.NextRun != 0 {
t.Fatalf("tâche unique encore active après son passage : %+v", after)
}
}