Files
Loki/internal/loki/code_subagent_test.go
T
Claude ded1c97c17 Lot 47 : outil subagent du mode Code exécuté par la façade
Le builder délègue à un rôle en lecture seule (explorer, code-reviewer,
planner) sans passer par /tool/exec : src/chat/subagent.rs porte
toolSubagent. Mêmes refus avant tout appel au moteur (rôle inconnu,
verifier compris, tâche vide, récursion), contexte isolé servi par le Go
(POST /internal/v1/turn/subagent : prompt du rôle + briefing machine,
outils, capacités ; subagentView et subagentFraming partagés), tour
imbriqué run_turn avec l'endpoint, le raisonnement, l'écho et l'arrêt du
tour qui délègue, température 0,6, travail annexe (second slot, effacé à
la fin), télémétrie à part. Seul le texte écrit après le dernier outil
revient, rogné, préfixé du rôle, tronqué à 6000 runes.

Le tour du rôle est porté par SubHost, qui délègue tout à son GoTurnHost
sauf un nouvel appel à subagent ; les outils de la façade passent dans
GoTurnHost::exec_tool_base, que seul le tour principal complète de
subagent (le type du tour imbriqué ne se contient pas lui-même).

Parité : journal tiré du Go contre un faux moteur scripté
(parity_subagent_test.go → tests/parity_subagent/subagent.json : refus,
réponse directe, rapport après deux outils, texte vide relancé, rapport
tronqué, cadrage du planner, moteur qui refuse ; requêtes du rôle),
rejoué par un tour complet de la façade (tests/lot6.rs) ;
TestSubagentVueInterne côté Go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
2026-10-16 07:12:29 +00:00

86 lines
3.1 KiB
Go

package loki
import (
"context"
"encoding/json"
"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")
}
}
}
// Lot 47 : la façade demande au Go le contexte isolé d'un rôle délégué —
// le même prompt système, les mêmes outils et capacités que toolSubagent.
func TestSubagentVueInterne(t *testing.T) {
testHome(t)
t.Setenv(internalTokenEnv, jetonTest)
w := appelOAIInterne(t, "POST", "/internal/v1/turn/subagent", `{"role":"planner"}`)
if w.Code != 200 {
t.Fatalf("vue : %d %s", w.Code, w.Body.String())
}
var v struct {
System string `json:"system"`
Tools []Tool `json:"tools"`
Caps Caps `json:"caps"`
}
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
t.Fatal(err)
}
caps, system := subagentView("planner")
if v.System != system || v.Caps != caps || len(v.Tools) != len(EnabledTools(caps)) {
t.Fatalf("vue : %+v", v.Caps)
}
for _, body := range []string{`{"role":"verifier"}`, `{"role":""}`, `{`} {
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/subagent", body); w.Code != 400 {
t.Fatalf("%s : %d", body, w.Code)
}
}
}