Files
Loki/internal/loki/mcp_client_test.go
T
MichaelandClaude Opus 5.5 01c11be4ae Correctifs repris d'AJEAN et d'OpenFox : LSP, shell, clé web, MCP, recall, compactage, relances
- LSP (lsp.go) : un signal resté dans le tampon par une republication
  tardive faisait rendre « aucun diagnostic » à l'édition suivante, avant même
  que le serveur ait vu la nouvelle version. Le canal est vidé, une
  publication d'une version antérieure est ignorée, et check attend 350 ms de
  calme après la dernière publication (liste vide puis analyse : c'est
  l'analyse qui compte), toujours borné par lspDiagWait. Au passage :
  interblocage latent levé (writeMsg → markDead reprenait mu, déjà tenu par
  check) — dead devient atomique.
- run_shell : ErrWaitDelay (commande finie, process laissé en arrière-plan
  sur les tubes) rend la sortie au lieu de « WaitDelay expired » qui
  l'effaçait (AJEAN 7403a8a0).
- Clé de pilotage web : lue depuis un cache à chaque requête au lieu d'ouvrir
  la base sous dbMu (AJEAN #120). Lectures réussies seulement, invalidé par
  toute écriture du bucket d'état, par la date/taille du fichier (autre
  process) et au bout d'une seconde.
- MCP : l'appel suit le contexte du tour (le bouton stop l'annule), délai par
  serveur (callTimeoutSec) et allongé par l'argument timeout de l'outil,
  plafonné à 30 min ; un délai dépassé n'est plus relancé (OpenFox 2ef584b8).
- Recall : les blocs d'une compaction s'archivent en une seule transaction
  au lieu d'une par bloc (AJEAN #130).
- Compactage en cours de tour : après un essai qui n'a rien réduit, pas de
  nouvel essai avant +5 % de la fenêtre (sauf à 90 %), pour les tâches et
  sous-agents aussi ; la cause d'un résumé raté est journalisée (AJEAN
  68659e73).
- Motifs de relance : un motif vide (qui correspondait à tout) est écarté,
  une surcharge sans motif utilisable retombe sur l'embarqué, et un
  changement de retry_patterns agit sans redémarrer (OpenFox 8e9b4869,
  c52c5bf7).

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

92 lines
2.5 KiB
Go

package loki
import (
"context"
"os/exec"
"strings"
"sync"
"testing"
)
// TestMCPEnsureDeduplicatesConcurrentConnects vérifie que N appelants simultanés
// (pré-chauffage + tour de chat + panneau MCP de l'UI) partagent une seule
// tentative de connexion : sans ça, connecter en parallèle multiplierait les
// process stdio lancés pour un même serveur.
func TestMCPEnsureDeduplicatesConcurrentConnects(t *testing.T) {
testHome(t)
t.Cleanup(mcpCloseAll)
// Commande inexistante : la connexion échoue, mais l'entrée du pool est bien
// partagée — c'est ce qu'on teste.
cfg := MCPServerConfig{Command: "loki-binaire-inexistant-pour-test", Enabled: true}
const callers = 8
var wg sync.WaitGroup
got := make([]*mcpSession, callers)
for i := range got {
wg.Add(1)
go func(i int) {
defer wg.Done()
got[i] = mcpMgr.ensure("bidon", cfg)
}(i)
}
wg.Wait()
for i, s := range got {
if s != got[0] {
t.Fatalf("appelant %d a obtenu une session différente : %p vs %p", i, s, got[0])
}
select {
case <-s.ready:
default:
t.Fatalf("appelant %d a reçu une session pas encore prête", i)
}
}
if got[0].err == nil {
t.Fatal("une commande inexistante devrait produire une erreur de connexion")
}
}
// TestMCPEndToEnd connecte le serveur MCP de référence (@modelcontextprotocol/
// server-everything via npx), vérifie la découverte d'outils namespacés puis un
// appel réel (echo). Skippé si npx est absent (CI sans Node).
func TestMCPEndToEnd(t *testing.T) {
if _, err := exec.LookPath("npx"); err != nil {
t.Skip("npx indisponible — test MCP e2e ignoré")
}
testHome(t)
cfg := MCPServerConfig{
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-everything"},
Enabled: true,
}
if err := SetMCPServer("everything", cfg); err != nil {
t.Fatalf("SetMCPServer: %v", err)
}
t.Cleanup(mcpCloseAll)
tools := mcpTools()
if len(tools) == 0 {
t.Fatal("aucun outil MCP découvert (le serveur s'est-il connecté ?)")
}
echoName := mcpExposedName("everything", "echo")
found := false
for _, tl := range tools {
if tl.Function.Name == echoName {
found = true
}
if !strings.HasPrefix(tl.Function.Name, "mcp__everything__") {
t.Errorf("outil mal namespacé: %s", tl.Function.Name)
}
}
if !found {
t.Fatalf("outil %s introuvable dans %d outils", echoName, len(tools))
}
out := mcpCall(context.Background(), echoName, map[string]any{"message": "bonjour-loki"})
if !strings.Contains(out, "bonjour-loki") {
t.Fatalf("echo n'a pas renvoyé le message ; got: %q", out)
}
}