Files
2026-08-12 20:25:13 +01:00

125 lines
4.0 KiB
Go

package plugins
import (
"context"
"testing"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/mcp/plugintools"
"github.com/kandev/kandev/internal/plugins/manifest"
"github.com/kandev/kandev/internal/plugins/store"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func TestAgentToolCatalogIncludesActiveTools(t *testing.T) {
svc, _, _ := newTestService(t)
readOnly := true
destructive := false
svc.registry.Add(&store.Record{
Manifest: manifest.Manifest{
ID: "task-tags.v1",
DisplayName: "Task tags",
AgentTools: []manifest.AgentTool{{
Name: "add_tag",
Description: "Add a tag to a task",
Surfaces: []string{manifest.AgentToolSurfaceKanban},
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{"tag": map[string]any{"type": "string"}},
"required": []any{"tag"},
},
Annotations: manifest.AgentToolAnnotations{ReadOnlyHint: &readOnly, DestructiveHint: &destructive},
}},
},
Status: store.StatusActive,
})
svc.registry.Add(&store.Record{
Manifest: manifest.Manifest{
ID: "disabled.v1",
AgentTools: []manifest.AgentTool{{
Name: "hidden",
Description: "Hidden tool",
Surfaces: []string{manifest.AgentToolSurfaceKanban},
InputSchema: map[string]any{"type": "object"},
}},
},
Status: store.StatusDisabled,
})
snapshot, err := svc.AgentToolCatalog()
if err != nil {
t.Fatalf("AgentToolCatalog() error: %v", err)
}
if snapshot.Generation == "" || snapshot.Revision == 0 {
t.Fatalf("snapshot identity = %#v, want generation and revision", snapshot)
}
if len(snapshot.Tools) != 1 {
t.Fatalf("snapshot tools = %d, want 1", len(snapshot.Tools))
}
tool := snapshot.Tools[0]
if tool.ExposedName != plugintools.ExposedName("task-tags.v1", "add_tag") {
t.Fatalf("exposed name = %q", tool.ExposedName)
}
if !tool.ReadOnlyHint || tool.DestructiveHint {
t.Fatalf("annotations = %#v, want read-only true and destructive false", tool)
}
again, err := svc.AgentToolCatalog()
if err != nil {
t.Fatalf("second AgentToolCatalog() error: %v", err)
}
if again.Revision != snapshot.Revision {
t.Fatalf("revision changed without catalog change: %d -> %d", snapshot.Revision, again.Revision)
}
}
func TestAgentToolCatalogUsesConservativeAnnotationDefaults(t *testing.T) {
svc, _, _ := newTestService(t)
svc.registry.Add(&store.Record{
Manifest: manifest.Manifest{
ID: "defaults", AgentTools: []manifest.AgentTool{{
Name: "echo", Description: "Echo input.",
Surfaces: []string{manifest.AgentToolSurfaceKanban},
InputSchema: map[string]any{"type": "object"},
}},
},
Status: store.StatusActive,
})
snapshot, err := svc.AgentToolCatalog()
require.NoError(t, err)
require.Len(t, snapshot.Tools, 1)
tool := snapshot.Tools[0]
require.False(t, tool.ReadOnlyHint)
require.True(t, tool.DestructiveHint)
require.False(t, tool.IdempotentHint)
require.True(t, tool.OpenWorldHint)
}
func TestInvokeAgentToolLogsOnlyBoundMetadata(t *testing.T) {
core, observed := observer.New(zap.InfoLevel)
log, err := logger.NewFromZap(zap.New(core))
require.NoError(t, err)
svc := NewService(store.NewFSStore(t.TempDir()), NewRegistry(), nil, log)
_, err = svc.InvokeAgentTool(context.Background(), "missing", "echo", map[string]any{
"secret": "do-not-log",
}, AgentToolInvocationContext{
InvocationID: "invoke-1", TaskID: "task-1", SessionID: "session-1",
})
require.Error(t, err)
entries := observed.FilterMessage("plugin agent tool invocation").All()
require.Len(t, entries, 1)
fields := entries[0].ContextMap()
require.Equal(t, "invoke-1", fields["invocation_id"])
require.Equal(t, "missing", fields["plugin_id"])
require.Equal(t, "echo", fields["local_name"])
require.Equal(t, "task-1", fields["task_id"])
require.Equal(t, "session-1", fields["session_id"])
require.Equal(t, "error", fields["outcome"])
require.NotContains(t, fields, "arguments")
require.NotContains(t, fields, "result")
}