Files
kandev/apps/backend/pkg/pluginsdk/serve_test.go
T
João Salavisa 1ba81a7cd0 refactor(plugins): remove the unused tools feature (#1776)
Plugin `tools[]` overlapped with MCP, kandev's established mechanism for
exposing tools to agents (`internal/mcp/`), and the `InvokeTool` path was
never wired into agent tool sets — the `listTools` handler was
listing-only. Rather than maintain a half-built parallel surface, remove
it. If plugins ever contribute agent tools, they should feed through MCP,
not a separate `InvokeTool` RPC.

Removes:
- proto `InvokeTool` RPC + `ToolRequest`/`ToolResponse`/`ToolContext`
  messages (regenerated `*.pb.go` / `*_grpc.pb.go`)
- pkg/pluginsdk `InvokeTool` from the `Plugin` interface and
  `UnimplementedPlugin`, the Tool* Go-native types, and the
  `RemotePlugin`/`GRPCServer` dispatch
- internal/plugins `Service.InvokeTool`, the `listTools` handler +
  `GET /api/plugins/tools` route, `PluginToolDTO`, and the manifest
  `Tools` field + tool-name uniqueness validation
- frontend `PluginTool`/`PluginToolDTO` types, `listPluginTools`, and the
  manifest-card tools list
- fixture/e2e `InvokeTool` echo impl and the `tools:` manifest block

Test doubles that used the "crash" tool to exercise crash-restart now use
a "crash" webhook key instead. Internal specs (GRPC-CONTRACT, spec) drop
the tool RPC/feature; docs/public was already updated in #1774.

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 17:03:13 +01:00

98 lines
3.5 KiB
Go

package pluginsdk
import (
"context"
"testing"
"time"
hcplugin "github.com/hashicorp/go-plugin"
"github.com/stretchr/testify/require"
)
// fakeAuthorPlugin is a minimal author-facing Plugin used to exercise the
// real go-plugin gRPC + broker wiring end to end (no subprocess spawn —
// hcplugin.TestPluginGRPCConn wires a real unix-socket grpc.Server/Client
// pair with a real GRPCBroker, matching production transport behavior).
type fakeAuthorPlugin struct {
UnimplementedPlugin
events []*Event
}
func (p *fakeAuthorPlugin) OnEvent(_ context.Context, e *Event) error {
p.events = append(p.events, e)
return nil
}
func (p *fakeAuthorPlugin) HandleWebhook(_ context.Context, req *WebhookRequest) (*WebhookResponse, error) {
return &WebhookResponse{Status: 200, Body: append([]byte("got:"), req.Body...)}, nil
}
// TestServe_EndToEnd exercises the same GRPCPlugin wiring Serve() (plugin
// side) and kandev's runtime manager (host side, via plugin.NewClient)
// use, over a real go-plugin gRPC connection + broker
// (hcplugin.TestPluginGRPCConn). It covers both Plugin RPCs plus the
// Host broker round trip (§3/§4 of docs/plans/plugins/GRPC-CONTRACT.md).
func TestServe_EndToEnd(t *testing.T) {
author := &fakeAuthorPlugin{}
hostImpl := &recordingHost{
getStateFn: func(_ context.Context, scope, scopeID, key string) (map[string]any, bool, error) {
if scope == "task" && scopeID == "t1" && key == "k1" {
return map[string]any{"greeting": "hi"}, true, nil
}
return nil, false, nil
},
}
// hcplugin.TestPluginGRPCConn dispenses/serves the SAME Plugin value on
// both ends of the harness (there's no real process boundary), so one
// GRPCPlugin with both Impl and Host set plays both roles: GRPCServer
// runs as the "plugin subprocess" would, GRPCClient runs as "kandev"
// would. In production these are two separate processes each setting
// only one of Impl/Host.
gp := &GRPCPlugin{Impl: author, Host: hostImpl, HostDialTimeout: 5 * time.Second}
client, server := hcplugin.TestPluginGRPCConn(t, false, map[string]hcplugin.Plugin{
PluginMapKey: gp,
})
defer func() { _ = client.Close() }()
defer server.Stop()
raw, err := client.Dispense(PluginMapKey)
require.NoError(t, err)
remote, ok := raw.(*RemotePlugin)
require.True(t, ok, "Dispense(%q) should return *RemotePlugin, got %T", PluginMapKey, raw)
t.Run("DeliverEvent", func(t *testing.T) {
err := remote.DeliverEvent(context.Background(), &Event{
EventID: "e1",
EventType: "task.created",
Payload: map[string]any{"a": float64(1)},
})
require.NoError(t, err)
require.Len(t, author.events, 1)
require.Equal(t, "e1", author.events[0].EventID)
require.Equal(t, map[string]any{"a": float64(1)}, author.events[0].Payload)
})
t.Run("HandleWebhook", func(t *testing.T) {
resp, err := remote.HandleWebhook(context.Background(), &WebhookRequest{Body: []byte("hi")})
require.NoError(t, err)
require.Equal(t, int32(200), resp.Status)
require.Equal(t, []byte("got:hi"), resp.Body)
})
t.Run("HostBrokerRoundTrip", func(t *testing.T) {
require.Eventually(t, func() bool {
return author.Host() != nil
}, 5*time.Second, 10*time.Millisecond, "Serve-equivalent GRPCServer should inject Host via the broker")
value, found, err := author.Host().GetState(context.Background(), "task", "t1", "k1")
require.NoError(t, err)
require.True(t, found)
require.Equal(t, map[string]any{"greeting": "hi"}, value)
_, found, err = author.Host().GetState(context.Background(), "task", "t1", "missing")
require.NoError(t, err)
require.False(t, found)
})
}