molecule-cli/cmd/molecule/molecule_test.go
Molecule AI SDK-Dev 1471cad4c1 feat(cli): add molecule completion [bash|zsh|fish|powershell] subcommand
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>
2026-04-22 18:06:02 +00:00

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")
}
}