molecule-core/workspace-server/internal/plugins/github_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

326 lines
9.9 KiB
Go

package plugins
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestGithubResolver_Scheme(t *testing.T) {
if NewGithubResolver().Scheme() != "github" {
t.Error("scheme must be 'github'")
}
}
// Stub git runner that writes a synthetic repo tree into the clone
// target dir, so tests don't need a real git binary or network.
func stubGit(repoContents map[string]string) func(ctx context.Context, dir string, args ...string) error {
return func(ctx context.Context, dir string, args ...string) error {
if err := ctx.Err(); err != nil {
return err
}
if len(args) < 2 || args[0] != "clone" {
return errors.New("unexpected git args")
}
target := args[len(args)-1]
if err := os.MkdirAll(target, 0o755); err != nil {
return err
}
// Synthesize a .git dir so we can prove the resolver strips it.
if err := os.MkdirAll(filepath.Join(target, ".git"), 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(target, ".git", "HEAD"), []byte("ref: refs/heads/main"), 0o644); err != nil {
return err
}
for path, content := range repoContents {
full := filepath.Join(target, path)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return err
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
return err
}
}
return nil
}
}
func TestGithubResolver_ClonesAndStripsGitDir(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
r := &GithubResolver{
GitRunner: stubGit(map[string]string{
"plugin.yaml": "name: demo\n",
"skills/h/SKILL.md": "---\nname: h\ndescription: d\n---\n",
"adapters/claude_code.py": "from plugins_registry.builtins import AgentskillsAdaptor as Adaptor\n",
}),
BaseURL: "file:///dev/null",
}
dst := t.TempDir()
name, err := r.Fetch(context.Background(), "org/repo", dst)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if name != "repo" {
t.Errorf("got name %q, want 'repo'", name)
}
// Contents copied.
for _, want := range []string{"plugin.yaml", "skills/h/SKILL.md", "adapters/claude_code.py"} {
if _, err := os.Stat(filepath.Join(dst, want)); err != nil {
t.Errorf("missing %q: %v", want, err)
}
}
// .git was stripped from the clone target before copy, so dst has no .git.
if _, err := os.Stat(filepath.Join(dst, ".git")); !os.IsNotExist(err) {
t.Error(".git dir must not survive into dst")
}
}
func TestGithubResolver_PassesRefAsBranch(t *testing.T) {
var seenArgs []string
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
seenArgs = args
target := args[len(args)-1]
_ = os.MkdirAll(target, 0o755)
return nil
},
}
if _, err := r.Fetch(context.Background(), "org/repo#v1.2.0", t.TempDir()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !containsArg(seenArgs, "--branch") || !containsArg(seenArgs, "v1.2.0") {
t.Errorf("args should include --branch v1.2.0, got %v", seenArgs)
}
}
func TestGithubResolver_OmitsBranchFlagWhenNoRef(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
var seenArgs []string
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
seenArgs = args
target := args[len(args)-1]
_ = os.MkdirAll(target, 0o755)
return nil
},
}
if _, err := r.Fetch(context.Background(), "org/repo", t.TempDir()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if containsArg(seenArgs, "--branch") {
t.Errorf("no ref → no --branch flag, got %v", seenArgs)
}
}
func TestGithubResolver_RejectsInvalidSpec(t *testing.T) {
r := NewGithubResolver()
for _, spec := range []string{
"",
"single-segment",
"too/many/segments",
"/leading-slash",
"trailing/",
"bad char/repo",
"org/repo#bad ref",
} {
t.Run(spec, func(t *testing.T) {
_, err := r.Fetch(context.Background(), spec, t.TempDir())
if err == nil {
t.Errorf("should have rejected %q", spec)
}
})
}
}
func TestGithubResolver_BubblesUpGitError(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
return errors.New("simulated auth failure")
},
}
_, err := r.Fetch(context.Background(), "org/repo", t.TempDir())
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "simulated") {
t.Errorf("error should bubble git failure: %v", err)
}
}
func TestGithubResolver_UsesDefaultsWhenNilFields(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
// A zero-value GithubResolver should still have defaults filled in
// at Fetch time. Verified indirectly: we pass a stub that records
// the URL passed to `git clone`.
var seenArgs []string
r := &GithubResolver{}
r.GitRunner = func(ctx context.Context, dir string, args ...string) error {
seenArgs = args
target := args[len(args)-1]
return os.MkdirAll(target, 0o755)
}
if _, err := r.Fetch(context.Background(), "org/repo", t.TempDir()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
url := ""
for _, a := range seenArgs {
if strings.HasPrefix(a, "http") {
url = a
break
}
}
if !strings.HasPrefix(url, "https://github.com/org/repo") {
t.Errorf("default BaseURL not applied, got %q", url)
}
}
func containsArg(args []string, target string) bool {
for _, a := range args {
if a == target {
return true
}
}
return false
}
// ---- defaultGitRunner ----
func TestDefaultGitRunner_PropagatesFailureFromMissingGit(t *testing.T) {
t.Setenv("PATH", "/nonexistent")
err := defaultGitRunner(context.Background(), t.TempDir(), "status")
if err == nil {
t.Error("expected error when git is unavailable on PATH")
}
}
func TestDefaultGitRunner_UsesWorkingDirHomeFallback(t *testing.T) {
// Force HOME empty so the resolver adds the fallback.
t.Setenv("HOME", "")
// Still need real git or a bogus arg. Use `--version` which succeeds
// on any system that has git, then skip if not.
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed on this system")
}
if err := defaultGitRunner(context.Background(), t.TempDir(), "--version"); err != nil {
t.Errorf("git --version should succeed: %v", err)
}
}
func TestGithubResolver_NilGitRunnerUsesDefault(t *testing.T) {
// Passing nil GitRunner should fall back to defaultGitRunner. With no
// git on PATH, that fallback errors — we don't need real git here.
t.Setenv("PATH", "/nonexistent")
r := &GithubResolver{GitRunner: nil, BaseURL: "https://example.com"}
_, err := r.Fetch(context.Background(), "org/repo", t.TempDir())
if err == nil {
t.Error("expected default git runner to error without a git binary")
}
}
func TestGithubResolver_CopyToDstFailure(t *testing.T) {
r := &GithubResolver{
GitRunner: stubGit(map[string]string{"plugin.yaml": "name: x\n"}),
}
// Make dst read-only so copyTree fails after the successful clone.
dst := t.TempDir()
if err := os.Chmod(dst, 0o555); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chmod(dst, 0o755) })
_, err := r.Fetch(context.Background(), "org/repo", dst)
if err == nil {
t.Error("expected copy failure when dst is read-only")
}
}
func TestGithubResolver_AlwaysPassesDepth1(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
var seenArgs []string
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
seenArgs = args
target := args[len(args)-1]
return os.MkdirAll(target, 0o755)
},
}
if _, err := r.Fetch(context.Background(), "org/repo", t.TempDir()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !containsArg(seenArgs, "--depth=1") {
t.Errorf("expected --depth=1 in git args, got %v", seenArgs)
}
}
func TestGithubResolver_PassesDoubleDashBeforeURL(t *testing.T) {
// When a ref is specified, we pass `--` after --branch <ref> as
// defense-in-depth against ref-as-flag injection.
var seenArgs []string
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
seenArgs = args
target := args[len(args)-1]
return os.MkdirAll(target, 0o755)
},
}
if _, err := r.Fetch(context.Background(), "org/repo#main", t.TempDir()); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !containsArg(seenArgs, "--") {
t.Errorf("expected `--` separator in git args, got %v", seenArgs)
}
}
func TestGithubResolver_RejectsRefStartingWithHyphen(t *testing.T) {
r := NewGithubResolver()
_, err := r.Fetch(context.Background(), "org/repo#-exec=/evil", t.TempDir())
if err == nil {
t.Error("ref starting with '-' must be rejected")
}
}
func TestGithubResolver_MapsRepositoryNotFoundToSentinel(t *testing.T) {
t.Setenv("PLUGIN_ALLOW_UNPINNED", "true")
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
return errors.New("remote: Repository not found.\nfatal: repository 'https://github.com/x/y.git' not found")
},
}
_, err := r.Fetch(context.Background(), "org/repo", t.TempDir())
if !errors.Is(err, ErrPluginNotFound) {
t.Errorf("expected ErrPluginNotFound, got %v", err)
}
}
func TestGithubResolver_MapsMissingBranchToSentinel(t *testing.T) {
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
return errors.New("fatal: Remote branch bogus not found in upstream origin")
},
}
_, err := r.Fetch(context.Background(), "org/repo#bogus", t.TempDir())
if !errors.Is(err, ErrPluginNotFound) {
t.Errorf("expected ErrPluginNotFound for missing ref, got %v", err)
}
}
func TestGithubResolver_AuthFailureIsNotErrPluginNotFound(t *testing.T) {
r := &GithubResolver{
GitRunner: func(ctx context.Context, dir string, args ...string) error {
return errors.New("fatal: Authentication failed for 'https://github.com/private/repo.git/'")
},
}
_, err := r.Fetch(context.Background(), "private/repo", t.TempDir())
if err == nil {
t.Fatal("expected error")
}
if errors.Is(err, ErrPluginNotFound) {
t.Errorf("auth failure must not surface as ErrPluginNotFound: %v", err)
}
}