molecule-core/workspace-server/internal/plugins/source_test.go
Hongming Wang d8026347e5 chore: open-source restructure — rename dirs, remove internal files, scrub secrets
Renames:
- platform/ → workspace-server/ (Go module path stays as "platform" for
  external dep compat — will update after plugin module republish)
- workspace-template/ → workspace/

Removed (moved to separate repos or deleted):
- PLAN.md — internal roadmap (move to private project board)
- HANDOFF.md, AGENTS.md — one-time internal session docs
- .claude/ — gitignored entirely (local agent config)
- infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy
- org-templates/molecule-dev/ → standalone template repo
- .mcp-eval/ → molecule-mcp-server repo
- test-results/ — ephemeral, gitignored

Security scrubbing:
- Cloudflare account/zone/KV IDs → placeholders
- Real EC2 IPs → <EC2_IP> in all docs
- CF token prefix, Neon project ID, Fly app names → redacted
- Langfuse dev credentials → parameterized
- Personal runner username/machine name → generic

Community files:
- CONTRIBUTING.md — build, test, branch conventions
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1

All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml,
README, CLAUDE.md updated for new directory names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 00:24:44 -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)
}
}