molecule-core/workspace-server/internal/artifacts/client_test.go
Hongming Wang 479a027e4b 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

371 lines
11 KiB
Go

package artifacts_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/artifacts"
)
// cfEnvelope wraps a result value in the Cloudflare v4 response envelope.
func cfEnvelope(t *testing.T, result interface{}) []byte {
t.Helper()
b, err := json.Marshal(result)
if err != nil {
t.Fatalf("cfEnvelope: marshal result: %v", err)
}
env := map[string]interface{}{
"success": true,
"result": json.RawMessage(b),
"errors": []interface{}{},
}
out, err := json.Marshal(env)
if err != nil {
t.Fatalf("cfEnvelope: marshal envelope: %v", err)
}
return out
}
// cfError returns a Cloudflare v4 error envelope.
func cfError(t *testing.T, statusCode, code int, message string) ([]byte, int) {
t.Helper()
env := map[string]interface{}{
"success": false,
"result": nil,
"errors": []map[string]interface{}{
{"code": code, "message": message},
},
}
b, _ := json.Marshal(env)
return b, statusCode
}
func newTestClient(t *testing.T, mux *http.ServeMux) *artifacts.Client {
t.Helper()
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return artifacts.NewWithBaseURL("test-token", "test-ns", srv.URL)
}
// ---- CreateRepo ----------------------------------------------------------
func TestCreateRepo_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Verify auth header
if r.Header.Get("Authorization") != "Bearer test-token" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// Decode request body
var req map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if req["name"] != "my-workspace-repo" {
http.Error(w, "unexpected name", http.StatusBadRequest)
return
}
repo := artifacts.Repo{
Name: "my-workspace-repo",
ID: "repo-abc123",
RemoteURL: "https://x:tok@hash.artifacts.cloudflare.net/git/repo-abc123.git",
CreatedAt: time.Now(),
}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, repo))
})
client := newTestClient(t, mux)
repo, err := client.CreateRepo(context.Background(), artifacts.CreateRepoRequest{
Name: "my-workspace-repo",
Description: "Molecule AI workspace snapshot",
})
if err != nil {
t.Fatalf("CreateRepo: unexpected error: %v", err)
}
if repo.Name != "my-workspace-repo" {
t.Errorf("repo.Name = %q, want %q", repo.Name, "my-workspace-repo")
}
if repo.ID != "repo-abc123" {
t.Errorf("repo.ID = %q, want %q", repo.ID, "repo-abc123")
}
if repo.RemoteURL == "" {
t.Error("repo.RemoteURL is empty, want non-empty")
}
}
func TestCreateRepo_APIError(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos", func(w http.ResponseWriter, r *http.Request) {
body, status := cfError(t, http.StatusConflict, 1009, "repo already exists")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(body)
})
client := newTestClient(t, mux)
_, err := client.CreateRepo(context.Background(), artifacts.CreateRepoRequest{Name: "dup"})
if err == nil {
t.Fatal("expected error, got nil")
}
apiErr, ok := err.(*artifacts.APIError)
if !ok {
t.Fatalf("expected *APIError, got %T: %v", err, err)
}
if apiErr.StatusCode != http.StatusConflict {
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, http.StatusConflict)
}
if apiErr.Message != "repo already exists" {
t.Errorf("Message = %q, want %q", apiErr.Message, "repo already exists")
}
}
// ---- GetRepo -------------------------------------------------------------
func TestGetRepo_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos/my-repo", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
repo := artifacts.Repo{
Name: "my-repo",
ID: "repo-xyz",
RemoteURL: "https://x:tok@hash.artifacts.cloudflare.net/git/repo-xyz.git",
}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, repo))
})
client := newTestClient(t, mux)
repo, err := client.GetRepo(context.Background(), "my-repo")
if err != nil {
t.Fatalf("GetRepo: unexpected error: %v", err)
}
if repo.Name != "my-repo" {
t.Errorf("repo.Name = %q, want %q", repo.Name, "my-repo")
}
}
func TestGetRepo_NotFound(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos/missing", func(w http.ResponseWriter, r *http.Request) {
body, status := cfError(t, http.StatusNotFound, 1004, "repo not found")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(body)
})
client := newTestClient(t, mux)
_, err := client.GetRepo(context.Background(), "missing")
if err == nil {
t.Fatal("expected error, got nil")
}
apiErr, ok := err.(*artifacts.APIError)
if !ok {
t.Fatalf("expected *APIError, got %T", err)
}
if apiErr.StatusCode != http.StatusNotFound {
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, http.StatusNotFound)
}
}
// ---- ForkRepo ------------------------------------------------------------
func TestForkRepo_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos/source-repo/fork", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["name"] != "forked-repo" {
http.Error(w, "unexpected fork name", http.StatusBadRequest)
return
}
result := artifacts.ForkResult{
Repo: artifacts.Repo{
Name: "forked-repo",
ID: "repo-fork-1",
RemoteURL: "https://x:tok@hash.artifacts.cloudflare.net/git/repo-fork-1.git",
},
ObjectCount: 42,
}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, result))
})
client := newTestClient(t, mux)
result, err := client.ForkRepo(context.Background(), "source-repo", artifacts.ForkRepoRequest{
Name: "forked-repo",
})
if err != nil {
t.Fatalf("ForkRepo: unexpected error: %v", err)
}
if result.Repo.Name != "forked-repo" {
t.Errorf("Repo.Name = %q, want %q", result.Repo.Name, "forked-repo")
}
if result.ObjectCount != 42 {
t.Errorf("ObjectCount = %d, want 42", result.ObjectCount)
}
}
// ---- ImportRepo ----------------------------------------------------------
func TestImportRepo_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos/imported/import", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["url"] == "" {
http.Error(w, "url required", http.StatusBadRequest)
return
}
repo := artifacts.Repo{
Name: "imported",
ID: "repo-imp-1",
RemoteURL: "https://x:tok@hash.artifacts.cloudflare.net/git/repo-imp-1.git",
}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, repo))
})
client := newTestClient(t, mux)
repo, err := client.ImportRepo(context.Background(), "imported", artifacts.ImportRepoRequest{
URL: "https://github.com/Molecule-AI/molecule-core.git",
Branch: "main",
Depth: 1,
})
if err != nil {
t.Fatalf("ImportRepo: unexpected error: %v", err)
}
if repo.Name != "imported" {
t.Errorf("repo.Name = %q, want %q", repo.Name, "imported")
}
}
// ---- DeleteRepo ----------------------------------------------------------
func TestDeleteRepo_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos/to-delete", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
deleted := map[string]string{"id": "repo-del-1"}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
w.Write(cfEnvelope(t, deleted))
})
client := newTestClient(t, mux)
if err := client.DeleteRepo(context.Background(), "to-delete"); err != nil {
t.Fatalf("DeleteRepo: unexpected error: %v", err)
}
}
// ---- CreateToken ---------------------------------------------------------
func TestCreateToken_Success(t *testing.T) {
expiry := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/tokens", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req map[string]interface{}
json.NewDecoder(r.Body).Decode(&req)
if req["repo"] != "my-repo" {
http.Error(w, "unexpected repo", http.StatusBadRequest)
return
}
tok := artifacts.RepoToken{
ID: "tok-123",
Token: "plaintext-secret-abc",
Scope: "write",
ExpiresAt: expiry,
}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, tok))
})
client := newTestClient(t, mux)
tok, err := client.CreateToken(context.Background(), artifacts.CreateTokenRequest{
Repo: "my-repo",
Scope: "write",
TTL: 86400,
})
if err != nil {
t.Fatalf("CreateToken: unexpected error: %v", err)
}
if tok.ID != "tok-123" {
t.Errorf("ID = %q, want %q", tok.ID, "tok-123")
}
if tok.Token != "plaintext-secret-abc" {
t.Errorf("Token = %q, want %q", tok.Token, "plaintext-secret-abc")
}
if tok.Scope != "write" {
t.Errorf("Scope = %q, want %q", tok.Scope, "write")
}
}
// ---- RevokeToken ---------------------------------------------------------
func TestRevokeToken_Success(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/tokens/tok-456", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
deleted := map[string]string{"id": "tok-456"}
w.Header().Set("Content-Type", "application/json")
w.Write(cfEnvelope(t, deleted))
})
client := newTestClient(t, mux)
if err := client.RevokeToken(context.Background(), "tok-456"); err != nil {
t.Fatalf("RevokeToken: unexpected error: %v", err)
}
}
// ---- Context cancellation ------------------------------------------------
func TestCreateRepo_ContextCancelled(t *testing.T) {
// Server that never responds (simulates a hung connection)
mux := http.NewServeMux()
mux.HandleFunc("/namespaces/test-ns/repos", func(w http.ResponseWriter, r *http.Request) {
// Block until the client gives up
<-r.Context().Done()
})
client := newTestClient(t, mux)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
_, err := client.CreateRepo(ctx, artifacts.CreateRepoRequest{Name: "x"})
if err == nil {
t.Fatal("expected error from cancelled context, got nil")
}
}