molecule-core/platform/internal/plugins/source_test.go
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

200 lines
5.2 KiB
Go

package plugins
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
)
// ---- ParseSource ----
func TestParseSource_BareNameBecomesLocal(t *testing.T) {
s, err := ParseSource("my-plugin")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if s.Scheme != "local" || s.Spec != "my-plugin" {
t.Errorf("got %+v", s)
}
}
func TestParseSource_ExplicitScheme(t *testing.T) {
cases := map[string]Source{
"local://foo": {Scheme: "local", Spec: "foo"},
"github://org/repo": {Scheme: "github", Spec: "org/repo"},
"github://org/repo#v1.0": {Scheme: "github", Spec: "org/repo#v1.0"},
"clawhub://name@1.2.3": {Scheme: "clawhub", Spec: "name@1.2.3"},
"https://example.com/x": {Scheme: "https", Spec: "example.com/x"},
}
for in, want := range cases {
t.Run(in, func(t *testing.T) {
got, err := ParseSource(in)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("ParseSource(%q) = %+v, want %+v", in, got, want)
}
})
}
}
func TestParseSource_EmptyRejected(t *testing.T) {
if _, err := ParseSource(""); err == nil {
t.Error("expected error on empty input")
}
if _, err := ParseSource(" "); err == nil {
t.Error("expected error on whitespace input")
}
}
func TestParseSource_StripsWhitespace(t *testing.T) {
s, err := ParseSource(" my-plugin ")
if err != nil || s.Spec != "my-plugin" {
t.Errorf("got %+v, err=%v", s, err)
}
}
func TestSource_Raw(t *testing.T) {
s := Source{Scheme: "github", Spec: "foo/bar#v1"}
if s.Raw() != "github://foo/bar#v1" {
t.Errorf("got %q", s.Raw())
}
}
// ---- Registry ----
type fakeResolver struct {
scheme string
calls int
}
func (f *fakeResolver) Scheme() string { return f.scheme }
func (f *fakeResolver) Fetch(ctx context.Context, spec, dst string) (string, error) {
f.calls++
return spec, nil
}
func TestRegistry_RegisterAndResolve(t *testing.T) {
reg := NewRegistry()
local := &fakeResolver{scheme: "local"}
gh := &fakeResolver{scheme: "github"}
reg.Register(local)
reg.Register(gh)
r, err := reg.Resolve(Source{Scheme: "github", Spec: "x/y"})
if err != nil {
t.Fatal(err)
}
if r != gh {
t.Errorf("got wrong resolver: %+v", r)
}
}
func TestRegistry_UnknownScheme(t *testing.T) {
reg := NewRegistry()
_, err := reg.Resolve(Source{Scheme: "mystery", Spec: "x"})
if err == nil {
t.Error("expected error for unknown scheme")
}
if !strings.Contains(err.Error(), "mystery") {
t.Errorf("error should name the missing scheme: %v", err)
}
}
func TestRegistry_OverwriteSameScheme(t *testing.T) {
reg := NewRegistry()
a := &fakeResolver{scheme: "local"}
b := &fakeResolver{scheme: "local"}
reg.Register(a)
reg.Register(b)
r, _ := reg.Resolve(Source{Scheme: "local", Spec: "x"})
if r != b {
t.Error("second registration should overwrite the first")
}
}
func TestRegistry_SchemesSorted(t *testing.T) {
reg := NewRegistry()
reg.Register(&fakeResolver{scheme: "local"})
reg.Register(&fakeResolver{scheme: "clawhub"})
reg.Register(&fakeResolver{scheme: "github"})
got := reg.Schemes()
want := []string{"clawhub", "github", "local"}
if !reflect.DeepEqual(got, want) {
t.Errorf("Schemes() = %v, want %v", got, want)
}
}
func TestRegistry_EmptyReturnsEmpty(t *testing.T) {
reg := NewRegistry()
if s := reg.Schemes(); len(s) != 0 {
t.Errorf("empty registry should return empty slice, got %v", s)
}
}
func TestErrPluginNotFound_IsMatchable(t *testing.T) {
// Wrap + unwrap via fmt.Errorf to prove errors.Is works through
// the fmt wrappers the resolvers use in their error returns.
err := fmt.Errorf("local resolver: plugin \"x\": %w", ErrPluginNotFound)
if !errors.Is(err, ErrPluginNotFound) {
t.Error("errors.Is did not unwrap ErrPluginNotFound")
}
}
func TestSource_StringEqualsRaw(t *testing.T) {
s := Source{Scheme: "github", Spec: "foo/bar#v1"}
if s.String() != s.Raw() {
t.Errorf("String()=%q Raw()=%q must match", s.String(), s.Raw())
}
}
func TestRegistry_ConcurrentRegisterResolve_NoRace(t *testing.T) {
// Exercises the RWMutex: interleave Register / Resolve / Schemes
// from multiple goroutines. `go test -race` fails loudly if the
// locking is wrong.
reg := NewRegistry()
reg.Register(&fakeResolver{scheme: "local"})
done := make(chan struct{})
for i := 0; i < 4; i++ {
go func(i int) {
for j := 0; j < 50; j++ {
reg.Register(&fakeResolver{scheme: fmt.Sprintf("s%d", i)})
_, _ = reg.Resolve(Source{Scheme: "local"})
_ = reg.Schemes()
}
done <- struct{}{}
}(i)
}
for i := 0; i < 4; i++ {
<-done
}
}
// ---- C1: empty spec after scheme ----
func TestParseSource_EmptySpecAfterSchemeRejected(t *testing.T) {
for _, in := range []string{"local://", "github://", "https://", "local:// "} {
t.Run(in, func(t *testing.T) {
_, err := ParseSource(in)
if err == nil {
t.Errorf("ParseSource(%q) should reject empty spec", in)
} else if !strings.Contains(err.Error(), "empty spec") {
t.Errorf("error message should mention 'empty spec': %v", err)
}
})
}
}
func TestParseSource_BareNameStillAccepted(t *testing.T) {
// The empty-spec guard must not break back-compat for bare names.
s, err := ParseSource("my-plugin")
if err != nil || s.Scheme != "local" || s.Spec != "my-plugin" {
t.Errorf("bare name broke: %+v err=%v", s, err)
}
}