package completion import ( "context" "errors" "fmt" "testing" "time" ) type fakeRuntimeClient struct { closed bool } func (f *fakeRuntimeClient) DidOpen(_ context.Context, _ string, _ string, _ int) error { return nil } func (f *fakeRuntimeClient) DidChange(_ context.Context, _ string, _ string, _ int) error { return nil } func (f *fakeRuntimeClient) Completion(_ context.Context, _ string, _ int, _ int) (Response, error) { return Response{ Items: []Item{{Label: "ok"}}, }, nil } func (f *fakeRuntimeClient) Close() error { f.closed = true return nil } // 同 language+session 的请求应复用同一个底层 client。 func TestManagerCompleteSelectsLanguageAndSession(t *testing.T) { createCount := 0 factory := func(_ context.Context, spec LanguageServerSpec, _ string) (RuntimeClient, error) { createCount++ if spec.Language != "go" { return nil, fmt.Errorf("unexpected language: %s", spec.Language) } return &fakeRuntimeClient{}, nil } m := NewManager(ManagerConfig{WorkspaceDir: "."}, []LanguageServerSpec{ {Language: "go", LanguageID: "go", Command: "gopls"}, }, factory) defer m.Close() for i := 0; i < 2; i++ { resp, err := m.Complete(context.Background(), Request{ Language: "go", SessionID: "s1", URI: "file:///main.go", Text: "package main", Line: 0, Character: 0, }) if err != nil { t.Fatalf("Complete() error = %v", err) } if len(resp.Items) != 1 || resp.Items[0].Label != "ok" { t.Fatalf("unexpected response: %+v", resp) } } if createCount != 1 { t.Fatalf("expected one client creation, got %d", createCount) } } // 未配置的语言应返回 ErrUnsupportedLanguage。 func TestManagerCompleteUnsupportedLanguage(t *testing.T) { m := NewManager(ManagerConfig{}, []LanguageServerSpec{ {Language: "go", LanguageID: "go", Command: "gopls"}, }, func(_ context.Context, _ LanguageServerSpec, _ string) (RuntimeClient, error) { return &fakeRuntimeClient{}, nil }) defer m.Close() _, err := m.Complete(context.Background(), Request{ Language: "python", URI: "file:///main.py", Text: "print('hi')", Line: 0, Character: 0, }) if !errors.Is(err, ErrUnsupportedLanguage) { t.Fatalf("expected ErrUnsupportedLanguage, got %v", err) } } // 超过空闲 TTL 的会话应被后台清理并关闭 client。 func TestManagerCleanupIdleSession(t *testing.T) { client := &fakeRuntimeClient{} m := NewManager(ManagerConfig{ WorkspaceDir: ".", SessionTTL: 30 * time.Millisecond, CleanupInterval: 10 * time.Millisecond, }, []LanguageServerSpec{ {Language: "go", LanguageID: "go", Command: "gopls"}, }, func(_ context.Context, _ LanguageServerSpec, _ string) (RuntimeClient, error) { return client, nil }) defer m.Close() _, err := m.Complete(context.Background(), Request{ Language: "go", SessionID: "s2", URI: "file:///main.go", Text: "package main", Line: 0, Character: 0, }) if err != nil { t.Fatalf("Complete() error = %v", err) } time.Sleep(90 * time.Millisecond) sessions := m.ActiveSessions() if sessions["go"] != 0 { t.Fatalf("expected session cleanup, got %+v", sessions) } if !client.closed { t.Fatal("expected client to be closed by cleanup") } }