test(mcp): add subsystem tests with IPC round-trip verification
Fix cyclic type panic in contextmenu tools by using map[string]any for ContextMenuDef which contains self-referencing MenuItemDef. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a89e66c832
commit
19b949b8f3
2 changed files with 86 additions and 7 deletions
60
pkg/mcp/mcp_test.go
Normal file
60
pkg/mcp/mcp_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// pkg/mcp/mcp_test.go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/clipboard"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSubsystem_Good_Name(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
sub := New(c)
|
||||
assert.Equal(t, "display", sub.Name())
|
||||
}
|
||||
|
||||
func TestSubsystem_Good_RegisterTools(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
sub := New(c)
|
||||
// RegisterTools should not panic with a real mcp.Server
|
||||
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)
|
||||
assert.NotPanics(t, func() { sub.RegisterTools(server) })
|
||||
}
|
||||
|
||||
// Integration test: verify the IPC round-trip that MCP tool handlers use.
|
||||
|
||||
type mockClipPlatform struct {
|
||||
text string
|
||||
ok bool
|
||||
}
|
||||
|
||||
func (m *mockClipPlatform) Text() (string, bool) { return m.text, m.ok }
|
||||
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
|
||||
|
||||
func TestMCP_Good_ClipboardRoundTrip(t *testing.T) {
|
||||
c, err := core.New(
|
||||
core.WithService(clipboard.Register(&mockClipPlatform{text: "hello", ok: true})),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
// Verify the IPC path that clipboard_read tool handler uses
|
||||
result, handled, err := c.QUERY(clipboard.QueryText{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
content, _ := result.(clipboard.ClipboardContent)
|
||||
assert.Equal(t, "hello", content.Text)
|
||||
}
|
||||
|
||||
func TestMCP_Bad_NoServices(t *testing.T) {
|
||||
c, _ := core.New(core.WithServiceLock())
|
||||
// Without any services, QUERY should return handled=false
|
||||
_, handled, _ := c.QUERY(clipboard.QueryText{})
|
||||
assert.False(t, handled)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package mcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"forge.lthn.ai/core/gui/pkg/contextmenu"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
|
|
@ -10,16 +11,23 @@ import (
|
|||
|
||||
// --- contextmenu_add ---
|
||||
|
||||
// ContextMenuAddInput uses map[string]any for the menu definition because
|
||||
// contextmenu.ContextMenuDef contains self-referencing MenuItemDef (Items []MenuItemDef)
|
||||
// which the MCP SDK schema generator cannot handle (cycle detection panic).
|
||||
type ContextMenuAddInput struct {
|
||||
Name string `json:"name"`
|
||||
Menu contextmenu.ContextMenuDef `json:"menu"`
|
||||
Name string `json:"name"`
|
||||
Menu map[string]any `json:"menu"`
|
||||
}
|
||||
type ContextMenuAddOutput struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) contextMenuAdd(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuAddInput) (*mcp.CallToolResult, ContextMenuAddOutput, error) {
|
||||
_, _, err := s.core.PERFORM(contextmenu.TaskAdd{Name: input.Name, Menu: input.Menu})
|
||||
// Convert map[string]any to ContextMenuDef via JSON round-trip
|
||||
menuJSON, _ := json.Marshal(input.Menu)
|
||||
var menuDef contextmenu.ContextMenuDef
|
||||
_ = json.Unmarshal(menuJSON, &menuDef)
|
||||
_, _, err := s.core.PERFORM(contextmenu.TaskAdd{Name: input.Name, Menu: menuDef})
|
||||
if err != nil {
|
||||
return nil, ContextMenuAddOutput{}, err
|
||||
}
|
||||
|
|
@ -49,7 +57,7 @@ type ContextMenuGetInput struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
type ContextMenuGetOutput struct {
|
||||
Menu *contextmenu.ContextMenuDef `json:"menu"`
|
||||
Menu map[string]any `json:"menu"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) contextMenuGet(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuGetInput) (*mcp.CallToolResult, ContextMenuGetOutput, error) {
|
||||
|
|
@ -58,14 +66,21 @@ func (s *Subsystem) contextMenuGet(_ context.Context, _ *mcp.CallToolRequest, in
|
|||
return nil, ContextMenuGetOutput{}, err
|
||||
}
|
||||
menu, _ := result.(*contextmenu.ContextMenuDef)
|
||||
return nil, ContextMenuGetOutput{Menu: menu}, nil
|
||||
if menu == nil {
|
||||
return nil, ContextMenuGetOutput{}, nil
|
||||
}
|
||||
// Convert to map[string]any via JSON round-trip to avoid cyclic type in schema
|
||||
menuJSON, _ := json.Marshal(menu)
|
||||
var menuMap map[string]any
|
||||
_ = json.Unmarshal(menuJSON, &menuMap)
|
||||
return nil, ContextMenuGetOutput{Menu: menuMap}, nil
|
||||
}
|
||||
|
||||
// --- contextmenu_list ---
|
||||
|
||||
type ContextMenuListInput struct{}
|
||||
type ContextMenuListOutput struct {
|
||||
Menus map[string]contextmenu.ContextMenuDef `json:"menus"`
|
||||
Menus map[string]any `json:"menus"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) contextMenuList(_ context.Context, _ *mcp.CallToolRequest, _ ContextMenuListInput) (*mcp.CallToolResult, ContextMenuListOutput, error) {
|
||||
|
|
@ -74,7 +89,11 @@ func (s *Subsystem) contextMenuList(_ context.Context, _ *mcp.CallToolRequest, _
|
|||
return nil, ContextMenuListOutput{}, err
|
||||
}
|
||||
menus, _ := result.(map[string]contextmenu.ContextMenuDef)
|
||||
return nil, ContextMenuListOutput{Menus: menus}, nil
|
||||
// Convert to map[string]any via JSON round-trip to avoid cyclic type in schema
|
||||
menusJSON, _ := json.Marshal(menus)
|
||||
var menusMap map[string]any
|
||||
_ = json.Unmarshal(menusJSON, &menusMap)
|
||||
return nil, ContextMenuListOutput{Menus: menusMap}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue