Phase 1: Replace functional options (WithWorkspaceRoot, WithSubsystem,
WithProcessService, WithWSHub) with Options{} struct. Breaking change
for consumers (agent, ide).
Phase 2: Add notification broadcasting and claude/channel capability.
- SendNotificationToAllClients: broadcasts to all MCP sessions
- ChannelSend: push named events (agent.complete, build.failed, etc.)
- ChannelSendToSession: push to a specific session
- Notifier interface for sub-packages to avoid circular imports
- claude/channel registered as experimental MCP capability
Co-Authored-By: Virgil <virgil@lethean.io>
113 lines
3 KiB
Go
113 lines
3 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// stubSubsystem is a minimal Subsystem for testing.
|
|
type stubSubsystem struct {
|
|
name string
|
|
toolsRegistered bool
|
|
}
|
|
|
|
func (s *stubSubsystem) Name() string { return s.name }
|
|
|
|
func (s *stubSubsystem) RegisterTools(server *mcp.Server) {
|
|
s.toolsRegistered = true
|
|
}
|
|
|
|
// shutdownSubsystem tracks Shutdown calls.
|
|
type shutdownSubsystem struct {
|
|
stubSubsystem
|
|
shutdownCalled bool
|
|
shutdownErr error
|
|
}
|
|
|
|
func (s *shutdownSubsystem) Shutdown(_ context.Context) error {
|
|
s.shutdownCalled = true
|
|
return s.shutdownErr
|
|
}
|
|
|
|
func TestSubsystem_Good_Registration(t *testing.T) {
|
|
sub := &stubSubsystem{name: "test-sub"}
|
|
svc, err := New(Options{Subsystems: []Subsystem{sub}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
|
|
if len(svc.Subsystems()) != 1 {
|
|
t.Fatalf("expected 1 subsystem, got %d", len(svc.Subsystems()))
|
|
}
|
|
if svc.Subsystems()[0].Name() != "test-sub" {
|
|
t.Errorf("expected name 'test-sub', got %q", svc.Subsystems()[0].Name())
|
|
}
|
|
}
|
|
|
|
func TestSubsystem_Good_ToolsRegistered(t *testing.T) {
|
|
sub := &stubSubsystem{name: "tools-sub"}
|
|
_, err := New(Options{Subsystems: []Subsystem{sub}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
if !sub.toolsRegistered {
|
|
t.Error("expected RegisterTools to have been called")
|
|
}
|
|
}
|
|
|
|
func TestSubsystem_Good_MultipleSubsystems(t *testing.T) {
|
|
sub1 := &stubSubsystem{name: "sub-1"}
|
|
sub2 := &stubSubsystem{name: "sub-2"}
|
|
svc, err := New(Options{Subsystems: []Subsystem{sub1, sub2}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
if len(svc.Subsystems()) != 2 {
|
|
t.Fatalf("expected 2 subsystems, got %d", len(svc.Subsystems()))
|
|
}
|
|
if !sub1.toolsRegistered || !sub2.toolsRegistered {
|
|
t.Error("expected all subsystems to have RegisterTools called")
|
|
}
|
|
}
|
|
|
|
func TestSubsystemShutdown_Good(t *testing.T) {
|
|
sub := &shutdownSubsystem{stubSubsystem: stubSubsystem{name: "shutdown-sub"}}
|
|
svc, err := New(Options{Subsystems: []Subsystem{sub}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
if err := svc.Shutdown(context.Background()); err != nil {
|
|
t.Fatalf("Shutdown() failed: %v", err)
|
|
}
|
|
if !sub.shutdownCalled {
|
|
t.Error("expected Shutdown to have been called")
|
|
}
|
|
}
|
|
|
|
func TestSubsystemShutdown_Bad_Error(t *testing.T) {
|
|
sub := &shutdownSubsystem{
|
|
stubSubsystem: stubSubsystem{name: "fail-sub"},
|
|
shutdownErr: context.DeadlineExceeded,
|
|
}
|
|
svc, err := New(Options{Subsystems: []Subsystem{sub}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
err = svc.Shutdown(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error from Shutdown")
|
|
}
|
|
}
|
|
|
|
func TestSubsystemShutdown_Good_NoShutdownInterface(t *testing.T) {
|
|
sub := &stubSubsystem{name: "plain-sub"}
|
|
svc, err := New(Options{Subsystems: []Subsystem{sub}})
|
|
if err != nil {
|
|
t.Fatalf("New() failed: %v", err)
|
|
}
|
|
if err := svc.Shutdown(context.Background()); err != nil {
|
|
t.Fatalf("Shutdown() should succeed for non-shutdown subsystem: %v", err)
|
|
}
|
|
}
|