gitlink-cli/cmd/interactive/executor_test.go

297 lines
7.0 KiB
Go

package interactive
import (
"fmt"
"os"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// TestExecutor_CaptureOutput verifies that Execute captures stdout output
// even when the Run function writes directly to os.Stdout.
func TestExecutor_CaptureOutput(t *testing.T) {
e := &Executor{}
s := &common.Shortcut{
Name: "test",
Description: "test command",
Run: func(ctx *common.RuntimeContext) error {
// Do nothing — output should be empty
return nil
},
}
output, err := e.Execute(s, map[string]string{})
if err != nil {
t.Fatalf("Execute returned error: %v", err)
}
if output != "" {
t.Fatalf("expected empty output, got %q", output)
}
}
// TestExecutor_CaptureOutputPrints verifies that stdout written inside Run
// is captured and returned as a string.
func TestExecutor_CaptureOutputPrints(t *testing.T) {
e := &Executor{}
s := &common.Shortcut{
Name: "test",
Description: "test command",
Run: func(ctx *common.RuntimeContext) error {
fmt.Fprint(os.Stdout, "hello from command")
return nil
},
}
output, err := e.Execute(s, map[string]string{})
if err != nil {
t.Fatalf("Execute returned error: %v", err)
}
if output != "hello from command" {
t.Fatalf("expected %q, got %q", "hello from command", output)
}
}
// TestExecutor_RestoresStdout verifies that os.Stdout is restored after Execute.
func TestExecutor_RestoresStdout(t *testing.T) {
original := os.Stdout
e := &Executor{}
s := &common.Shortcut{
Name: "test",
Description: "test command",
Run: func(ctx *common.RuntimeContext) error {
return nil
},
}
_, _ = e.Execute(s, map[string]string{})
if os.Stdout != original {
t.Fatal("os.Stdout was not restored after Execute")
}
}
// TestParseDirectCommand uses table-driven tests for parseDirectCommand.
func TestParseDirectCommand(t *testing.T) {
tests := []struct {
name string
input string
group string
cmd string
flagStr string
ok bool
}{
{
name: "empty string",
input: "",
ok: false,
},
{
name: "single word only",
input: "issue",
ok: false,
},
{
name: "second word without plus",
input: "issue list",
ok: false,
},
{
name: "two words with plus",
input: "issue +list",
group: "issue",
cmd: "list",
flagStr: "",
ok: true,
},
{
name: "with flags",
input: "issue +list --state open",
group: "issue",
cmd: "list",
flagStr: "--state open",
ok: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
group, cmd, flagStr, ok := parseDirectCommand(tt.input)
if ok != tt.ok {
t.Errorf("ok = %v, want %v", ok, tt.ok)
}
if ok {
if group != tt.group {
t.Errorf("group = %q, want %q", group, tt.group)
}
if cmd != tt.cmd {
t.Errorf("cmd = %q, want %q", cmd, tt.cmd)
}
if flagStr != tt.flagStr {
t.Errorf("flagStr = %q, want %q", flagStr, tt.flagStr)
}
}
})
}
}
// TestParseFlags uses table-driven tests for parseFlagString.
func TestParseFlags(t *testing.T) {
tests := []struct {
name string
flagStr string
shortMap map[string]string
want map[string]string
}{
{
name: "long flags only",
flagStr: "--title hello --body world",
shortMap: map[string]string{},
want: map[string]string{
"title": "hello",
"body": "world",
},
},
{
name: "short flag expanded",
flagStr: "-n 42",
shortMap: map[string]string{"n": "number"},
want: map[string]string{
"number": "42",
},
},
{
name: "equals syntax",
flagStr: "--title=hello",
shortMap: map[string]string{},
want: map[string]string{
"title": "hello",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseFlagString(tt.flagStr, tt.shortMap)
if len(got) != len(tt.want) {
t.Fatalf("got %d flags, want %d flags: %v", len(got), len(tt.want), got)
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("flag %q = %q, want %q", k, got[k], v)
}
}
})
}
}
// TestFindShortcut covers three scenarios: found, wrong group, wrong cmd.
func TestFindShortcut(t *testing.T) {
listShortcut := &common.Shortcut{Name: "list", Description: "list issues"}
createShortcut := &common.Shortcut{Name: "create", Description: "create issue"}
all := map[string][]*common.Shortcut{
"issue": {listShortcut, createShortcut},
"pr": {{Name: "list", Description: "list PRs"}},
}
// Scenario 1: found
s, ok := findShortcut(all, "issue", "list")
if !ok {
t.Fatal("expected to find issue +list")
}
if s != listShortcut {
t.Fatal("returned wrong shortcut")
}
// Scenario 2: group not found
_, ok = findShortcut(all, "repo", "list")
if ok {
t.Fatal("expected not found for unknown group")
}
// Scenario 3: group found but cmd not found
_, ok = findShortcut(all, "issue", "close")
if ok {
t.Fatal("expected not found for unknown cmd in valid group")
}
}
// TestBuildShortMap verifies short-to-long flag mapping construction.
func TestBuildShortMap(t *testing.T) {
s := &common.Shortcut{
Name: "list",
Flags: []common.Flag{
{Name: "state", Short: "s"},
{Name: "number", Short: "n"},
{Name: "verbose"},
},
}
m := buildShortMap(s)
if len(m) != 2 {
t.Fatalf("expected 2 entries in short map, got %d", len(m))
}
if m["s"] != "state" {
t.Errorf("short 's' = %q, want 'state'", m["s"])
}
if m["n"] != "number" {
t.Errorf("short 'n' = %q, want 'number'", m["n"])
}
}
// TestMissingRequiredFlags checks that required-but-missing flags are reported.
func TestMissingRequiredFlags(t *testing.T) {
s := &common.Shortcut{
Name: "create",
Flags: []common.Flag{
{Name: "title", Required: true},
{Name: "body", Required: true},
{Name: "state", Required: false},
},
}
// Provide title but not body
args := map[string]string{"title": "hello"}
missing := missingRequiredFlags(s, args)
if len(missing) != 1 {
t.Fatalf("expected 1 missing flag, got %d: %v", len(missing), missing)
}
if missing[0] != "--body" {
t.Errorf("missing flag = %q, want '--body'", missing[0])
}
// Provide all required
args2 := map[string]string{"title": "hello", "body": "world"}
missing2 := missingRequiredFlags(s, args2)
if len(missing2) != 0 {
t.Fatalf("expected 0 missing flags, got %d: %v", len(missing2), missing2)
}
}
// TestFormatCommandDisplay verifies the display string format.
func TestFormatCommandDisplay(t *testing.T) {
s := &common.Shortcut{
Name: "create",
Description: "create an issue",
}
args := map[string]string{
"title": "hello",
"body": "world",
}
got := formatCommandDisplay("issue", s, args)
// The order of flags may vary, so check that all parts are present
if !strings.HasPrefix(got, "issue +create") {
t.Fatalf("expected prefix 'issue +create', got %q", got)
}
if !strings.Contains(got, "--title hello") {
t.Fatalf("expected '--title hello' in output, got %q", got)
}
if !strings.Contains(got, "--body world") {
t.Fatalf("expected '--body world' in output, got %q", got)
}
}