Files
Loki/internal/loki/code_subagent_test.go
Claude c4e399e101 Mode code : sous-agents (explorer, code-reviewer, planner)
Reprise de l'idée des sous-agents d'OpenFox, sur la mécanique déjà en place
pour la passe de vérification : un runChat isolé, non persisté.

L'outil `subagent` délègue une question bornée à un rôle qui travaille dans
SON propre contexte et ne rend que sa réponse. Sur un modèle local, c'est la
fenêtre de contexte qu'on sauve : « trouve où est géré le cache » coûte dix
lectures de fichiers qui restaient ensuite dans l'historique jusqu'à la
compaction, alors que seule la réponse comptait.

Les rôles explorer et code-reviewer, jusqu'ici définis mais jamais appelés,
deviennent utilisables. Tous les rôles délégués sont en LECTURE SEULE : pas
de write/edit (ce qui modifie le dépôt reste dans le fil principal, sous les
yeux de l'utilisateur), pas de subagent (aucune récursion), pas de mémoire ni
de web. Seul le planner pose des critères — son prompt le lui demande — et
marquer un critère « passé » reste le privilège de la passe de vérification.

Le rôle verifier n'est PAS délégable : le builder se décernerait son propre
satisfecit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
2026-09-22 14:08:01 +00:00

57 lines
2.1 KiB
Go

package loki
import (
"context"
"strings"
"testing"
)
// Les garde-fous de l'outil subagent doivent répondre SANS appeler le modèle :
// un rôle inconnu, une tâche vide ou un appel depuis un sous-agent sont refusés
// à l'entrée (sinon chaque faute de frappe coûte une génération complète).
func TestSubagentRefusesBeforeAnyInference(t *testing.T) {
cases := []struct {
name string
args map[string]any
parent Caps
want string
}{
{"rôle inconnu", map[string]any{"role": "architecte", "task": "x"}, Caps{Code: true}, "rôle inconnu"},
{"rôle verifier refusé", map[string]any{"role": "verifier", "task": "x"}, Caps{Code: true}, "rôle inconnu"},
{"tâche vide", map[string]any{"role": "explorer", "task": " "}, Caps{Code: true}, "tâche vide"},
{"pas de récursion", map[string]any{"role": "explorer", "task": "x"}, Caps{Code: true, Role: "explorer"}, "ne peut pas en appeler un autre"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := toolSubagent(context.Background(), tc.args, tc.parent)
if !strings.HasPrefix(got, "[erreur]") || !strings.Contains(got, tc.want) {
t.Fatalf("attendu un refus contenant %q, obtenu %q", tc.want, got)
}
})
}
}
// Un rôle délégué travaille en LECTURE SEULE : ni write/edit (les modifications
// restent dans le fil principal), ni subagent (pas de récursion), et seul le
// planner pose des critères.
func TestSubagentRolesAreReadOnly(t *testing.T) {
forbidden := map[string]bool{"write": true, "edit": true, "subagent": true, "mem_add": true, "mem_edit": true, "web_search": true}
for _, role := range subagentRoles {
names := map[string]bool{}
for _, tool := range EnabledTools(Caps{Agent: true, Code: true, Role: role}) {
names[tool.Function.Name] = true
}
for f := range forbidden {
if names[f] {
t.Fatalf("le rôle %s ne devrait pas avoir l'outil %s", role, f)
}
}
if !names["read"] || !names["grep"] {
t.Fatalf("le rôle %s devrait pouvoir lire le code", role)
}
if got := names["criteria"]; got != (role == "planner") {
t.Fatalf("critères pour %s : %v (attendu %v)", role, got, role == "planner")
}
}
}