mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 14:15:00 +00:00
238 lines
7.4 KiB
Go
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
|
|
}
|