Wires shell completion for all 4 shells via Cobra's built-in generators. Covers the remaining item from the implementation status checklist. Adds: - internal/cmd/completion.go: cobra.GenXxxCompletion wrappers with usage examples for each shell - 5 new integration tests in cmd/molecule/molecule_test.go: - TestCLI_Completion_Help (4 shells × help flag) - TestCLI_Completion_GeneratesScript (4 shells × script output) - TestCLI_Completion_InvalidShell (exit code 2 on bad shell) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
864 lines
24 KiB
Go
864 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mockServer returns an httptest.Server that handles molecule-cli API calls.
|
|
// It serves responses under basePath so tests can hit <server.URL + basePath>.
|
|
func mockServer(t *testing.T, basePath string) *httptest.Server {
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
|
|
// --- Workspaces ---
|
|
workspaces := []map[string]interface{}{
|
|
{
|
|
"id": "ws-001",
|
|
"name": "test-workspace",
|
|
"status": "online",
|
|
"role": "researcher",
|
|
"runtime": "claude-code",
|
|
"created_at": "2026-04-01T12:00:00Z",
|
|
"tier": 2,
|
|
},
|
|
{
|
|
"id": "ws-002",
|
|
"name": "prod-workspace",
|
|
"status": "online",
|
|
"role": "pm",
|
|
"tier": 3,
|
|
},
|
|
}
|
|
|
|
mux.HandleFunc(basePath+"/workspaces", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(workspaces)
|
|
case http.MethodPost:
|
|
var req map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
resp := map[string]interface{}{
|
|
"id": "ws-new",
|
|
"name": req["name"],
|
|
"status": "creating",
|
|
"created_at": "2026-04-21T00:00:00Z",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(resp)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(workspaces[0])
|
|
case http.MethodDelete:
|
|
// CLI may send ?confirm=true query param
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-missing", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
http.Error(w, `{"error":"workspace not found"}`, http.StatusNotFound)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001/restart", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001/delete", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001/agents", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
agents := []map[string]interface{}{
|
|
{"id": "ag-001", "name": "researcher-agent", "workspace_id": "ws-001", "status": "online", "model": "claude-opus-4"},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(agents)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001/delegate", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
resp := map[string]interface{}{
|
|
"delegation_id": "del-001",
|
|
"status": "queued",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
json.NewEncoder(w).Encode(resp)
|
|
})
|
|
|
|
// --- Agents ---
|
|
agents := []map[string]interface{}{
|
|
{"id": "ag-001", "name": "researcher-agent", "workspace_id": "ws-001", "status": "online", "model": "claude-opus-4"},
|
|
{"id": "ag-002", "name": "pm-agent", "workspace_id": "ws-002", "status": "online", "model": "claude-sonnet-4"},
|
|
}
|
|
|
|
mux.HandleFunc(basePath+"/agents", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(agents)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/agents/ag-001", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(agents[0])
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/agents/ag-missing", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, `{"error":"agent not found"}`, http.StatusNotFound)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/registry/ws-001/peers", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
peers := []map[string]interface{}{
|
|
{"id": "ws-002", "name": "prod-workspace", "workspace_id": "ws-002", "status": "online", "model": "claude-sonnet-4"},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(peers)
|
|
})
|
|
|
|
mux.HandleFunc(basePath+"/workspaces/ws-001/a2a", 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]string
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
resp := map[string]interface{}{
|
|
"result": "Message delivered to researcher-agent: " + req["message"],
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
})
|
|
|
|
// --- Health ---
|
|
mux.HandleFunc(basePath+"/health", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
resp := map[string]interface{}{
|
|
"status": "ok",
|
|
"version": "1.2.3",
|
|
"uptime": "42h",
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
})
|
|
|
|
return server
|
|
}
|
|
|
|
// repoRoot returns the repo root directory.
|
|
func repoRoot() string {
|
|
// test file is at cmd/molecule/molecule_test.go
|
|
// ../.. from molecule/ goes to cmd, ../../.. goes to clone-cli
|
|
return filepath.Dir(filepath.Dir(filepath.Dir("/workspace/repos/clone-cli/cmd/molecule/")))
|
|
}
|
|
|
|
// mol returns the path to the CLI binary, building it if needed.
|
|
func mol(t *testing.T) string {
|
|
root := repoRoot()
|
|
exe := filepath.Join(t.TempDir(), "molecule")
|
|
goBin := runtime.GOEXE // e.g. "/usr/bin/go" — respects PATH
|
|
cmd := exec.Command(goBin, "build", "-o", exe, "./cmd/molecule")
|
|
cmd.Dir = root
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go build ./cmd/molecule: %v\n%s", err, out)
|
|
}
|
|
return exe
|
|
}
|
|
|
|
// TestMain exists so we can skip tests when go build fails outside of normal circumstances.
|
|
// The real test logic is in the functions below.
|
|
func TestMain(m *testing.M) {
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestCLI_Help(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
stderr bool // expect no stderr output on success
|
|
}{
|
|
{"root help", []string{"--help"}, false},
|
|
{"workspace help", []string{"workspace", "--help"}, false},
|
|
{"agent help", []string{"agent", "--help"}, false},
|
|
{"platform help", []string{"platform", "--help"}, false},
|
|
{"config help", []string{"config", "--help"}, false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, tc.args...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule %v: %v\nstdout: %s\nstderr: %s", strings.Join(tc.args, " "), err, stdout.String(), stderr.String())
|
|
}
|
|
if stderr.Len() > 0 && tc.stderr {
|
|
t.Errorf("unexpected stderr:\n%s", stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if out == "" {
|
|
t.Errorf("empty stdout for mol %v", strings.Join(tc.args, " "))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCLI_Version(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--version")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule --version: %v", err)
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "molecule") {
|
|
t.Errorf("expected 'molecule' in version output, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceList(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "list")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace list: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "test-workspace") {
|
|
t.Errorf("expected 'test-workspace' in output, got:\n%s", out)
|
|
}
|
|
if !strings.Contains(out, "prod-workspace") {
|
|
t.Errorf("expected 'prod-workspace' in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceList_JSON(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "--output", "json", "workspace", "list")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace list --output json: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
var out []map[string]interface{}
|
|
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
|
|
t.Fatalf("non-JSON output: %s\nstderr: %s", stdout.String(), stderr.String())
|
|
}
|
|
if len(out) != 2 {
|
|
t.Errorf("expected 2 workspaces, got %d", len(out))
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceInspect(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "inspect", "ws-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace inspect ws-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{"ws-001", "test-workspace", "online", "researcher"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("expected %q in output, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceInspect_NotFound(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "inspect", "ws-missing")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing workspace, got none")
|
|
}
|
|
// Should exit with non-zero code
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code for missing workspace, got 0")
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceCreate(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "create", "--name", "my-workspace")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace create: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "my-workspace") {
|
|
t.Errorf("expected 'my-workspace' in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceCreate_MissingName(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "create")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
// Missing required flag should exit with non-zero code
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing --name, got none")
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code for missing required flag, got 0")
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceDelete(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "delete", "ws-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace delete ws-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "deleted") {
|
|
t.Errorf("expected 'deleted' in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceRestart(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "restart", "ws-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace restart ws-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "Restart") {
|
|
t.Errorf("expected 'Restart' in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceAudit(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "audit")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace audit: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{"test-workspace", "prod-workspace", "researcher-agent"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("expected %q in output, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLI_WorkspaceDelegate(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "workspace", "delegate", "ws-001", "ws-002", "do the thing")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule workspace delegate: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "Delegation") && !strings.Contains(out, "ws-002") {
|
|
t.Errorf("expected delegation confirmation in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentList(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "list")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule agent list: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{"researcher-agent", "pm-agent"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("expected %q in output, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentList_WorkspaceFiltered(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "list", "ws-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule agent list ws-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "researcher-agent") {
|
|
t.Errorf("expected 'researcher-agent' in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentInspect(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "inspect", "ag-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule agent inspect ag-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{"ag-001", "researcher-agent", "online"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("expected %q in output, got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentInspect_NotFound(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "inspect", "ag-missing")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing agent, got none")
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentSend(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "send", "ag-001", "hello world")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule agent send: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "hello world") && !strings.Contains(out, "delivered") {
|
|
t.Errorf("expected delivery confirmation in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_AgentPeers(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "peers", "ws-001")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule agent peers ws-001: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "prod-workspace") {
|
|
t.Errorf("expected 'prod-workspace' in peers output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_PlatformHealth(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "platform", "health")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule platform health: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "ok") && !strings.Contains(out, "1.2.3") {
|
|
t.Errorf("expected health info in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_PlatformAudit(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "platform", "audit")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule platform audit: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "test-workspace") || !strings.Contains(out, "prod-workspace") {
|
|
t.Errorf("expected workspaces in platform audit output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_UnknownSubcommand(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "agen", "inspect")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error for unknown subcommand, got none")
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code for unknown subcommand, got 0")
|
|
}
|
|
}
|
|
|
|
func TestCLI_MissingRequiredArg(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "agent", "inspect")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing required arg, got none")
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code for missing required arg, got 0")
|
|
}
|
|
}
|
|
|
|
func TestCLI_ConfigInit(t *testing.T) {
|
|
exe := mol(t)
|
|
dir := t.TempDir()
|
|
cmd := exec.Command(exe, "config", "init")
|
|
cmd.Dir = dir
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule config init: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
f := filepath.Join(dir, "molecule.yaml")
|
|
if _, err := os.Stat(f); err != nil {
|
|
t.Errorf("molecule.yaml not scaffolded at %s", f)
|
|
}
|
|
}
|
|
|
|
func TestCLI_ConfigList(t *testing.T) {
|
|
server := mockServer(t, "")
|
|
defer server.Close()
|
|
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "--api-url", server.URL, "config", "list")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule config list: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
// Should at least show something without crashing
|
|
out := stdout.String()
|
|
if out == "" {
|
|
t.Errorf("empty stdout for molecule config list")
|
|
}
|
|
}
|
|
|
|
func TestCLI_Init(t *testing.T) {
|
|
exe := mol(t)
|
|
dir := t.TempDir()
|
|
cmd := exec.Command(exe, "init")
|
|
cmd.Dir = dir
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule init: %v\nstderr: %s", err, stderr.String())
|
|
}
|
|
f := filepath.Join(dir, "molecule.yaml")
|
|
if _, err := os.Stat(f); err != nil {
|
|
t.Errorf("molecule.yaml not scaffolded at %s", f)
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "Scaffolded") && !strings.Contains(out, "molecule.yaml") {
|
|
t.Errorf("expected scaffolded confirmation in output, got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCLI_Init_AlreadyExists(t *testing.T) {
|
|
exe := mol(t)
|
|
dir := t.TempDir()
|
|
// pre-create molecule.yaml
|
|
os.WriteFile(filepath.Join(dir, "molecule.yaml"), []byte("x"), 0o644)
|
|
cmd := exec.Command(exe, "init")
|
|
cmd.Dir = dir
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error when molecule.yaml exists, got none")
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code when molecule.yaml exists, got 0")
|
|
}
|
|
}
|
|
|
|
func TestCLI_Completion_Help(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
|
|
t.Run(shell+"-help", func(t *testing.T) {
|
|
cmd := exec.Command(exe, "completion", shell, "--help")
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule completion %s --help: %v\nstderr: %s", shell, err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if out == "" {
|
|
t.Errorf("empty stdout for molecule completion %s --help", shell)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCLI_Completion_GeneratesScript(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
for _, shell := range []string{"bash", "zsh", "fish", "powershell"} {
|
|
t.Run(shell, func(t *testing.T) {
|
|
cmd := exec.Command(exe, "completion", shell)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
t.Fatalf("molecule completion %s: %v\nstderr: %s", shell, err, stderr.String())
|
|
}
|
|
out := stdout.String()
|
|
if out == "" {
|
|
t.Errorf("empty completion script for %s", shell)
|
|
}
|
|
// The script should mention molecule or contain a directive/completion call
|
|
if !strings.Contains(out, "molecule") && !strings.Contains(out, "_molecule") {
|
|
t.Errorf("completion script for %s does not mention molecule:\n%s", shell, out)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCLI_Completion_InvalidShell(t *testing.T) {
|
|
exe := mol(t)
|
|
root := repoRoot()
|
|
cmd := exec.Command(exe, "completion", "unsupported-shell")
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
cmd.Dir = root
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
t.Fatalf("expected error for unsupported shell, got none")
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatalf("expected *exec.ExitError, got %T", err)
|
|
}
|
|
if exitErr.ExitCode() == 0 {
|
|
t.Errorf("expected non-zero exit code for unsupported shell, got 0")
|
|
}
|
|
}
|