Files
goclaw/internal/agent/image_gen_gate_test.go
nguyenha935andGoClaw Operator 4d3c6bcb05 feat: add Telegram manager channel permissions (#1339)
Co-authored-by: GoClaw Operator <operator@goclaw>
2026-07-04 00:25:00 +07:00

238 lines
7.4 KiB
Go

package agent
// Tests for the two-tier image_generation gate in buildFilteredTools.
//
// Gate conditions (ALL must be true to inject the native tool):
// (1) provider implements CapabilitiesAware and Capabilities().ImageGeneration == true
// (2) Loop.allowImageGeneration == true (agent config, defaults true; admin-only control)
//
// Additionally: final-iteration stripping takes priority — all tools removed.
import (
"slices"
"testing"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/tools"
)
// imageCapableProvider is a stub provider that also implements CapabilitiesAware
// and can toggle ImageGeneration on/off.
type imageCapableProvider struct {
stubProvider
imageGen bool
}
func (p *imageCapableProvider) Capabilities() providers.ProviderCapabilities {
return providers.ProviderCapabilities{
Streaming: true,
ToolCalling: true,
ImageGeneration: p.imageGen,
}
}
// buildImageGenLoop constructs a minimal Loop for gate testing.
// Uses the stubExecutor already defined in loop_pipeline_tool_callbacks_test.go.
func buildImageGenLoop(allowImageGen bool, prov providers.Provider) *Loop {
return &Loop{
provider: prov,
allowImageGeneration: allowImageGen,
tools: &stubExecutor{},
}
}
// hasImageGenTool returns true if the slice contains the image_generation sentinel.
func hasImageGenTool(defs []providers.ToolDefinition) bool {
for _, d := range defs {
if d.Type == "image_generation" {
return true
}
}
return false
}
// ─── Gate: all conditions true → tool present ─────────────────────────────
func TestImageGenGate_AllTrue_ToolPresent(t *testing.T) {
prov := &imageCapableProvider{imageGen: true}
l := buildImageGenLoop(true, prov)
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 1, 10, nil, nil)
if !hasImageGenTool(defs) {
t.Error("expected image_generation tool when all gate conditions are true")
}
}
// ─── Gate: provider capability false → tool absent ────────────────────────
func TestImageGenGate_ProviderNoCapability_ToolAbsent(t *testing.T) {
prov := &imageCapableProvider{imageGen: false}
l := buildImageGenLoop(true, prov)
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 1, 10, nil, nil)
if hasImageGenTool(defs) {
t.Error("image_generation must NOT be in tools when provider does not advertise ImageGeneration")
}
}
// ─── Gate: provider not CapabilitiesAware → tool absent ──────────────────
func TestImageGenGate_ProviderNotCapabilitiesAware_ToolAbsent(t *testing.T) {
// stubProvider (from intent_classify_test.go) does NOT implement CapabilitiesAware.
prov := &stubProvider{}
l := buildImageGenLoop(true, prov)
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 1, 10, nil, nil)
if hasImageGenTool(defs) {
t.Error("image_generation must NOT be in tools when provider is not CapabilitiesAware")
}
}
// ─── Gate: agent config disables → tool absent ───────────────────────────
func TestImageGenGate_AgentConfigDisabled_ToolAbsent(t *testing.T) {
prov := &imageCapableProvider{imageGen: true}
l := buildImageGenLoop(false, prov) // allowImageGeneration = false
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 1, 10, nil, nil)
if hasImageGenTool(defs) {
t.Error("image_generation must NOT be in tools when agent config disables it")
}
}
// ─── Final iteration strips all tools including image_generation ──────────
func TestImageGenGate_FinalIteration_AllToolsStripped(t *testing.T) {
prov := &imageCapableProvider{imageGen: true}
l := buildImageGenLoop(true, prov)
// iteration == maxIter → final stripping path; gate never reached
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 5, 5, nil, nil)
if len(defs) != 0 {
t.Errorf("final iteration must strip all tools; got %d: %v", len(defs), defs)
}
}
type filteringExecutor struct {
stubExecutor
defs []providers.ToolDefinition
}
func (e *filteringExecutor) ProviderDefs() []providers.ToolDefinition {
return e.defs
}
func (e *filteringExecutor) Get(name string) (tools.Tool, bool) {
return nil, false
}
func TestImageGenGate_FilteringNoPanic(t *testing.T) {
prov := &imageCapableProvider{imageGen: true}
exec := &filteringExecutor{
defs: []providers.ToolDefinition{
{
Type: "function",
Function: &providers.ToolFunctionSchema{
Name: "read_file",
},
},
{
Type: "image_generation",
Function: nil,
},
},
}
l := &Loop{
provider: prov,
allowImageGeneration: true,
tools: exec,
orchMode: "spawn", // Triggers orchModeDenyTools
disabledTools: map[string]bool{"read_file": true}, // Triggers disabled tools filter
agentType: "open", // Triggers bootstrap filter
skillEvolve: false, // Triggers skill evolve filter
}
req := &RunRequest{
ChannelType: "telegram", // Triggers channel filtering
}
// This should run successfully without panic.
defs, allowed, _ := l.buildFilteredTools(req, true, 1, 10, nil, nil)
if allowed != nil {
if _, exists := allowed[""]; exists {
t.Error("allowedTools map should not contain empty key for native tools")
}
}
_ = defs
}
func TestChannelAwareToolsHiddenWithoutChannelType(t *testing.T) {
reg := tools.NewRegistry()
reg.Register(tools.NewTelegramManagerTool())
l := &Loop{
provider: &stubProvider{},
allowImageGeneration: false,
tools: reg,
}
defs, _, _ := l.buildFilteredTools(&RunRequest{}, false, 1, 10, nil, nil)
if hasFunctionTool(defs, "telegram_manager") {
t.Fatal("telegram_manager must be hidden when no channel type is present")
}
}
func TestTelegramManagerHiddenUntilChannelPermissionEnabled(t *testing.T) {
reg := tools.NewRegistry()
reg.Register(tools.NewTelegramManagerTool())
l := &Loop{
provider: &stubProvider{},
allowImageGeneration: false,
tools: reg,
}
defs, _, _ := l.buildFilteredTools(&RunRequest{ChannelType: "telegram"}, false, 1, 10, nil, nil)
if hasFunctionTool(defs, "telegram_manager") {
t.Fatal("telegram_manager must be hidden for Telegram runs until the channel grants permissions")
}
defs, _, _ = l.buildFilteredTools(&RunRequest{
ChannelType: "telegram",
TelegramManagerPermissions: []string{"topic"},
}, false, 1, 10, nil, nil)
if !hasFunctionTool(defs, "telegram_manager") {
t.Fatal("telegram_manager must be visible for Telegram runs with channel permissions")
}
}
func TestTelegramManagerHiddenFromPromptUntilChannelPermissionEnabled(t *testing.T) {
reg := tools.NewRegistry()
reg.Register(tools.NewTelegramManagerTool())
l := &Loop{tools: reg}
if slices.Contains(l.filteredToolNamesForChannel("telegram", nil), "telegram_manager") {
t.Fatal("telegram_manager must be hidden from prompt until channel permissions are granted")
}
if !slices.Contains(l.filteredToolNamesForChannel("telegram", []string{"topic"}), "telegram_manager") {
t.Fatal("telegram_manager must appear in prompt for Telegram runs with channel permissions")
}
}
func hasFunctionTool(defs []providers.ToolDefinition, name string) bool {
for _, d := range defs {
if d.Function != nil && d.Function.Name == name {
return true
}
}
return false
}