all repos

viye @ f72d2a3

my shot at reimplementing xiki
11 files changed, 251 insertions(+), 185 deletions(-)
use olexsmir.xyz/x/is for asserts; test missing plumbings
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-30 23:38:41 +0300
Authored at: 2026-07-30 22:11:01 +0300
Change ID: kvmtqyoqyqzqxzmpxlswvlqnyrmlmnmx
Parent: aac4846
M go.mod
···
        1
        1
         module github.com/olexsmir/viye

      
        2
        2
         

      
        3
        3
         go 1.26.4

      
        
        4
        +

      
        
        5
        +require olexsmir.xyz/x v0.3.1

      
A go.sum
···
        
        1
        +olexsmir.xyz/x v0.2.0 h1:F1Wyt8J49pKawPKAdrlOk8rnm0vNYb/Ax/N2FsO2EUs=

      
        
        2
        +olexsmir.xyz/x v0.2.0/go.mod h1:MNHD3lrnOpxNKG7je8/dKgtZg5V8zPiow+U0QISz+Yc=

      
        
        3
        +olexsmir.xyz/x v0.3.1 h1:wlJjZG98duc2ZX3z1Hqx48DKUWwOFObE9dNPZqg3TK4=

      
        
        4
        +olexsmir.xyz/x v0.3.1/go.mod h1:79Z6oaW+ZCKZahm5r4Cgxbv1Ey8IQuAi1uXMhyJFJto=

      
M internal/config/config_test.go
···
        4
        4
         	"os"

      
        5
        5
         	"path/filepath"

      
        6
        6
         	"testing"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/x/is"

      
        7
        9
         )

      
        8
        10
         

      
        9
        11
         func TestLoad(t *testing.T) {

      
        10
        12
         	t.Run("creates default config when file missing", func(t *testing.T) {

      
        11
        13
         		want := Config{cfg: make(map[string]string)}

      
        12
        
        -		if err := want.read(defaultConfig); err != nil {

      
        13
        
        -			t.Fatal(err)

      
        14
        
        -		}

      
        
        14
        +		is.Err(t, want.read(defaultConfig), nil)

      
        15
        15
         

      
        16
        16
         		dir := t.TempDir()

      
        17
        17
         		t.Setenv("XDG_CONFIG_HOME", dir)

      
        18
        18
         

      
        19
        19
         		cfg, err := Load()

      
        20
        
        -		if err != nil {

      
        21
        
        -			t.Fatalf("Load() = _, %v", err)

      
        22
        
        -		}

      
        
        20
        +		is.Err(t, err, nil)

      
        23
        21
         		for key, val := range want.cfg {

      
        24
        
        -			if cfg.Get(key) != val {

      
        25
        
        -				t.Fatalf("got %s=%q, want %q", key, cfg.Get(key), val)

      
        26
        
        -			}

      
        
        22
        +			is.Equal(t, val, cfg.Get(key))

      
        27
        23
         		}

      
        28
        24
         	})

      
        29
        25
         

      ···
        32
        28
         		t.Setenv("XDG_CONFIG_HOME", dir)

      
        33
        29
         

      
        34
        30
         		cfgDir := filepath.Join(dir, "viye.conf")

      
        35
        
        -		if err := os.WriteFile(cfgDir, []byte("custom = value\n"), 0o644); err != nil {

      
        36
        
        -			t.Fatal(err)

      
        37
        
        -		}

      
        
        31
        +		is.Err(t, os.WriteFile(cfgDir, []byte("custom = value\n"), 0o644), nil)

      
        38
        32
         

      
        39
        33
         		cfg, err := Load()

      
        40
        
        -		if err != nil {

      
        41
        
        -			t.Fatalf("Load() = _, %v", err)

      
        42
        
        -		}

      
        43
        
        -		if cfg.Get("custom") != "value" {

      
        44
        
        -			t.Fatalf("got custom=%q, want %q", cfg.Get("custom"), "value")

      
        45
        
        -		}

      
        
        34
        +		is.Err(t, err, nil)

      
        
        35
        +		is.Equal(t, "value", cfg.Get("custom"))

      
        46
        36
         	})

      
        47
        37
         

      
        48
        38
         	t.Run("invalid config file returns error", func(t *testing.T) {

      ···
        50
        40
         		t.Setenv("XDG_CONFIG_HOME", dir)

      
        51
        41
         

      
        52
        42
         		cfgDir := filepath.Join(dir, "viye.conf")

      
        53
        
        -		if err := os.WriteFile(cfgDir, []byte("bad line\n"), 0o644); err != nil {

      
        54
        
        -			t.Fatal(err)

      
        55
        
        -		}

      
        
        43
        +		is.Err(t, os.WriteFile(cfgDir, []byte("bad line\n"), 0o644), nil)

      
        56
        44
         

      
        57
        45
         		_, err := Load()

      
        58
        
        -		if err != ErrInvalidConfig {

      
        59
        
        -			t.Fatalf("got %v, want ErrInvalidConfig", err)

      
        60
        
        -		}

      
        
        46
        +		is.Err(t, err, ErrInvalidConfig)

      
        61
        47
         	})

      
        62
        48
         }

      
        63
        49
         

      ···
        69
        55
         		cfg:     map[string]string{},

      
        70
        56
         	}

      
        71
        57
         

      
        72
        
        -	if err := c.Set("key", "val"); err != nil {

      
        73
        
        -		t.Fatal(err)

      
        74
        
        -	}

      
        75
        
        -	if c.cfg["key"] != "val" {

      
        76
        
        -		t.Fatalf("in-memory: got %v, want key=val", c.cfg)

      
        77
        
        -	}

      
        
        58
        +	is.Err(t, c.Set("key", "val"), nil)

      
        
        59
        +	is.Equal(t, "val", c.cfg["key"])

      
        78
        60
         

      
        79
        61
         	b, err := os.ReadFile(path)

      
        80
        
        -	if err != nil {

      
        81
        
        -		t.Fatal(err)

      
        82
        
        -	}

      
        83
        
        -	if string(b) != "key = val\n" {

      
        84
        
        -		t.Fatalf("on disk: got %q, want %q", string(b), "key = val\n")

      
        85
        
        -	}

      
        
        62
        +	is.Err(t, err, nil)

      
        
        63
        +	is.Equal(t, "key = val\n", string(b))

      
        86
        64
         }

      
        87
        65
         

      
        88
        66
         func TestRead(t *testing.T) {

      
        89
        67
         	t.Run("valid config", func(t *testing.T) {

      
        90
        68
         		c := Config{cfg: make(map[string]string)}

      
        91
        
        -		err := c.read([]byte("key1 = value1\nkey2 = value2\n"))

      
        92
        
        -		if err != nil {

      
        93
        
        -			t.Fatalf("unexpected error: %v", err)

      
        94
        
        -		}

      
        95
        
        -		if c.cfg["key1"] != "value1" || c.cfg["key2"] != "value2" {

      
        96
        
        -			t.Fatalf("got %v, want key1=value1 key2=value2", c.cfg)

      
        97
        
        -		}

      
        
        69
        +		is.Err(t, c.read([]byte("key1 = value1\nkey2 = value2\n")), nil)

      
        
        70
        +		is.Equal(t, "value1", c.cfg["key1"])

      
        
        71
        +		is.Equal(t, "value2", c.cfg["key2"])

      
        98
        72
         	})

      
        99
        73
         

      
        100
        74
         	t.Run("trailing newline", func(t *testing.T) {

      
        101
        75
         		c := Config{cfg: make(map[string]string)}

      
        102
        
        -		err := c.read([]byte("key = val\n"))

      
        103
        
        -		if err != nil {

      
        104
        
        -			t.Fatalf("unexpected error: %v", err)

      
        105
        
        -		}

      
        106
        
        -		if c.cfg["key"] != "val" {

      
        107
        
        -			t.Fatalf("got %v, want key=val", c.cfg)

      
        108
        
        -		}

      
        
        76
        +		is.Err(t, c.read([]byte("key = val\n")), nil)

      
        
        77
        +		is.Equal(t, "val", c.cfg["key"])

      
        109
        78
         	})

      
        110
        79
         

      
        111
        80
         	t.Run("empty lines", func(t *testing.T) {

      
        112
        81
         		c := Config{cfg: make(map[string]string)}

      
        113
        
        -		err := c.read([]byte("\nkey = val\n\n"))

      
        114
        
        -		if err != nil {

      
        115
        
        -			t.Fatalf("unexpected error: %v", err)

      
        116
        
        -		}

      
        117
        
        -		if c.cfg["key"] != "val" {

      
        118
        
        -			t.Fatalf("got %v, want key=val", c.cfg)

      
        119
        
        -		}

      
        
        82
        +		is.Err(t, c.read([]byte("\nkey = val\n\n")), nil)

      
        
        83
        +		is.Equal(t, "val", c.cfg["key"])

      
        120
        84
         	})

      
        121
        85
         

      
        122
        86
         	t.Run("multiple equals signs", func(t *testing.T) {

      
        123
        87
         		c := Config{cfg: make(map[string]string)}

      
        124
        
        -		err := c.read([]byte("key = val = extra"))

      
        125
        
        -		if err != ErrInvalidConfig {

      
        126
        
        -			t.Fatalf("got %v, want ErrInvalidConfig", err)

      
        127
        
        -		}

      
        
        88
        +		is.Err(t, c.read([]byte("key = val = extra")), ErrInvalidConfig)

      
        128
        89
         	})

      
        129
        90
         

      
        130
        91
         	t.Run("no separator", func(t *testing.T) {

      
        131
        92
         		c := Config{cfg: make(map[string]string)}

      
        132
        
        -		err := c.read([]byte("justtext"))

      
        133
        
        -		if err != ErrInvalidConfig {

      
        134
        
        -			t.Fatalf("got %v, want ErrInvalidConfig", err)

      
        135
        
        -		}

      
        
        93
        +		is.Err(t, c.read([]byte("justtext")), ErrInvalidConfig)

      
        136
        94
         	})

      
        137
        95
         }

      
A internal/plumbing/config/config_test.go
···
        
        1
        +package config

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"os"

      
        
        5
        +	"path/filepath"

      
        
        6
        +	"testing"

      
        
        7
        +

      
        
        8
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        9
        +

      
        
        10
        +	"olexsmir.xyz/x/is"

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +func TestMatch(t *testing.T) {

      
        
        14
        +	got := (Tool{}).Match(&viye.Context{Cmd: "config"})

      
        
        15
        +	is.Equal(t, true, got)

      
        
        16
        +}

      
        
        17
        +

      
        
        18
        +func TestExecute(t *testing.T) {

      
        
        19
        +	t.Run("no body empty config", func(t *testing.T) {

      
        
        20
        +		dir := t.TempDir()

      
        
        21
        +		t.Setenv("XDG_CONFIG_HOME", dir)

      
        
        22
        +

      
        
        23
        +		got, err := (Tool{}).Execute(&viye.Context{Path: []string{"config"}})

      
        
        24
        +		is.Err(t, err, nil)

      
        
        25
        +		is.Equal(t, "\n", got)

      
        
        26
        +	})

      
        
        27
        +

      
        
        28
        +	t.Run("no body with existing key", func(t *testing.T) {

      
        
        29
        +		dir := t.TempDir()

      
        
        30
        +		t.Setenv("XDG_CONFIG_HOME", dir)

      
        
        31
        +

      
        
        32
        +		cfgPath := filepath.Join(dir, "viye.conf")

      
        
        33
        +		os.WriteFile(cfgPath, []byte("k = v\n"), 0o644)

      
        
        34
        +

      
        
        35
        +		got, err := (Tool{}).Execute(&viye.Context{Path: []string{"config"}})

      
        
        36
        +		is.Err(t, err, nil)

      
        
        37
        +		is.Equal(t, ": k = v\n\n", got)

      
        
        38
        +	})

      
        
        39
        +

      
        
        40
        +	t.Run("set key", func(t *testing.T) {

      
        
        41
        +		dir := t.TempDir()

      
        
        42
        +		t.Setenv("XDG_CONFIG_HOME", dir)

      
        
        43
        +

      
        
        44
        +		got, err := (Tool{}).Execute(&viye.Context{

      
        
        45
        +			Path: []string{"config"},

      
        
        46
        +			Body: []string{": name = olex"},

      
        
        47
        +		})

      
        
        48
        +		is.Err(t, err, nil)

      
        
        49
        +		is.Equal(t, ": name = olex\n\n", got)

      
        
        50
        +	})

      
        
        51
        +

      
        
        52
        +	t.Run("update and delete", func(t *testing.T) {

      
        
        53
        +		dir := t.TempDir()

      
        
        54
        +		t.Setenv("XDG_CONFIG_HOME", dir)

      
        
        55
        +

      
        
        56
        +		cfgPath := filepath.Join(dir, "viye.conf")

      
        
        57
        +		os.WriteFile(cfgPath, []byte("a = 1\nb = 2\n"), 0o644)

      
        
        58
        +

      
        
        59
        +		got, err := (Tool{}).Execute(&viye.Context{

      
        
        60
        +			Path: []string{"config"},

      
        
        61
        +			Body: []string{": a = updated"},

      
        
        62
        +		})

      
        
        63
        +		is.Err(t, err, nil)

      
        
        64
        +		is.Equal(t, ": a = updated\n\n", got)

      
        
        65
        +	})

      
        
        66
        +

      
        
        67
        +	t.Run("key with spaces", func(t *testing.T) {

      
        
        68
        +		dir := t.TempDir()

      
        
        69
        +		t.Setenv("XDG_CONFIG_HOME", dir)

      
        
        70
        +

      
        
        71
        +		_, err := (Tool{}).Execute(&viye.Context{

      
        
        72
        +			Path: []string{"config"},

      
        
        73
        +			Body: []string{": my key = val"},

      
        
        74
        +		})

      
        
        75
        +		is.Err(t, err, `contains spaces`)

      
        
        76
        +	})

      
        
        77
        +}

      
M internal/plumbing/files/files_test.go
···
        7
        7
         	"testing"

      
        8
        8
         

      
        9
        9
         	"github.com/olexsmir/viye/internal/viye"

      
        
        10
        +

      
        
        11
        +	"olexsmir.xyz/x/is"

      
        10
        12
         )

      
        11
        13
         

      
        12
        14
         func TestMatch(t *testing.T) {

      
        13
        15
         	tests := []struct {

      
        14
        
        -		p string

      
        15
        
        -		m bool

      
        
        16
        +		p    string

      
        
        17
        +		want bool

      
        16
        18
         	}{

      
        17
        19
         		{"~/foo", true},

      
        18
        20
         		{"/tmp", true},

      
        19
        21
         		{"/tmp/foo", true},

      
        20
        22
         		{"./foo", true},

      
        21
        23
         		{"./foo/bar", true},

      
        22
        
        -

      
        23
        24
         		{"nope", false},

      
        24
        25
         		{"$ ls", false},

      
        25
        26
         		{"~", false},

      ···
        28
        29
         	}

      
        29
        30
         	for _, tt := range tests {

      
        30
        31
         		got := (&Tool{}).Match(&viye.Context{Path: []string{tt.p}})

      
        31
        
        -		if got != tt.m {

      
        32
        
        -			t.Errorf("Match(%q) = %v; want %v", tt.p, got, tt.m)

      
        33
        
        -		}

      
        
        32
        +		is.Equal(t, tt.want, got)

      
        34
        33
         	}

      
        35
        34
         }

      
        36
        35
         

      ···
        40
        39
         		os.WriteFile(filepath.Join(dir, "x"), nil, 0o644)

      
        41
        40
         		ctx := &viye.Context{Path: []string{dir}, Dir: "."}

      
        42
        41
         		got, err := (&Tool{}).Execute(ctx)

      
        43
        
        -		if err != nil {

      
        44
        
        -			t.Fatal(err)

      
        45
        
        -		}

      
        46
        
        -		want := "| x\n"

      
        47
        
        -		if got != want {

      
        48
        
        -			t.Errorf("Execute = %q; want %q", got, want)

      
        49
        
        -		}

      
        
        42
        +		is.Err(t, err, nil)

      
        
        43
        +		is.Equal(t, "| x\n", got)

      
        50
        44
         	})

      
        51
        45
         

      
        52
        46
         	t.Run("navigate into subdir via full path", func(t *testing.T) {

      ···
        58
        52
         		v := viye.New()

      
        59
        53
         		v.Register(&Tool{})

      
        60
        54
         		var out strings.Builder

      
        61
        
        -		if err := v.Run(&out, []string{"viye", sub}); err != nil {

      
        62
        
        -			t.Fatal(err)

      
        63
        
        -		}

      
        64
        
        -		want := "| y\n"

      
        65
        
        -		if out.String() != want {

      
        66
        
        -			t.Errorf("Run = %q; want %q", out.String(), want)

      
        67
        
        -		}

      
        
        55
        +		err := v.Run(&out, []string{"viye", sub})

      
        
        56
        +		is.Err(t, err, nil)

      
        
        57
        +		is.Equal(t, "| y\n", out.String())

      
        68
        58
         	})

      
        69
        59
         

      
        70
        60
         	t.Run("non existent path", func(t *testing.T) {

      
        71
        61
         		ctx := &viye.Context{Path: []string{"/nonexistent_foobar"}, Dir: "."}

      
        72
        
        -		if _, err := (&Tool{}).Execute(ctx); err == nil {

      
        73
        
        -			t.Error("expected error")

      
        74
        
        -		}

      
        
        62
        +		_, err := (&Tool{}).Execute(ctx)

      
        
        63
        +		is.NotEqual(t, nil, err)

      
        75
        64
         	})

      
        76
        65
         }

      
        77
        66
         

      
        78
        67
         func TestResolve(t *testing.T) {

      
        79
        68
         	home, _ := os.UserHomeDir()

      
        80
        
        -	tests := []struct {

      
        81
        
        -		p, dir string

      
        82
        
        -		want   string

      
        83
        
        -	}{

      
        
        69
        +	tests := []struct{ p, dir, want string }{

      
        84
        70
         		{"~", ".", home},

      
        85
        71
         		{"~/foo", ".", filepath.Join(home, "foo")},

      
        86
        72
         		{"/abs/path", ".", "/abs/path"},

      ···
        90
        76
         	}

      
        91
        77
         	for _, tt := range tests {

      
        92
        78
         		got := resolve(tt.p, tt.dir)

      
        93
        
        -		if got != tt.want {

      
        94
        
        -			t.Errorf("resolve(%q, %q) = %q; want %q", tt.p, tt.dir, got, tt.want)

      
        95
        
        -		}

      
        
        79
        +		is.Equal(t, tt.want, got)

      
        96
        80
         	}

      
        97
        81
         }

      
        98
        82
         

      ···
        102
        86
         	os.WriteFile(filepath.Join(dir, "a.txt"), nil, 0o644)

      
        103
        87
         

      
        104
        88
         	got, err := listDir(dir)

      
        105
        
        -	if err != nil {

      
        106
        
        -		t.Fatal(err)

      
        107
        
        -	}

      
        108
        
        -	if got != "a.txt\nsub/\n" {

      
        109
        
        -		t.Errorf("listDir = %q; want %q", got, "a.txt\\nsub/\\n")

      
        110
        
        -	}

      
        
        89
        +	is.Err(t, err, nil)

      
        
        90
        +	is.Equal(t, "a.txt\nsub/\n", got)

      
        111
        91
         }

      
M internal/plumbing/gobin/gobin_test.go
···
        4
        4
         	"testing"

      
        5
        5
         

      
        6
        6
         	"github.com/olexsmir/viye/internal/viye"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/x/is"

      
        7
        9
         )

      
        8
        10
         

      
        9
        11
         func TestMatch(t *testing.T) {

      ···
        19
        21
         	}

      
        20
        22
         	for _, tt := range tests {

      
        21
        23
         		got := (Tool{}).Match(&viye.Context{Path: tt.path})

      
        22
        
        -		if got != tt.want {

      
        23
        
        -			t.Errorf("Match(%v) = %v; want %v", tt.path, got, tt.want)

      
        24
        
        -		}

      
        
        24
        +		is.Equal(t, tt.want, got)

      
        25
        25
         	}

      
        26
        26
         }

      
        27
        27
         

      
M internal/plumbing/json/json.go
···
        13
        13
         func (Tool) Name() string                 { return "json" }

      
        14
        14
         func (Tool) Match(ctx *viye.Context) bool { return ctx.Cmd == "json" }

      
        15
        15
         func (Tool) Execute(ctx *viye.Context) (string, error) {

      
        16
        
        -	// Phase 1: no body → return editable template

      
        17
        16
         	if len(ctx.Body) == 0 {

      
        18
        
        -		return ": name: your name\n: age: 0\n", nil

      
        
        17
        +		return ": name: your name\n" +

      
        
        18
        +			": age: 0\n", nil

      
        19
        19
         	}

      
        20
        20
         

      
        21
        
        -	// Phase 2: body present → parse and produce JSON

      
        22
        21
         	m := make(map[string]string)

      
        23
        22
         	for _, line := range ctx.Body {

      
        24
        
        -		// Strip ": " prefix

      
        25
        23
         		line = strings.TrimPrefix(line, ": ")

      
        26
        24
         		col := strings.Index(line, ":")

      
        27
        25
         		if col < 0 {

      
        28
        26
         			continue

      
        29
        27
         		}

      
        
        28
        +

      
        30
        29
         		key := strings.TrimSpace(line[:col])

      
        31
        30
         		val := strings.TrimSpace(line[col+1:])

      
        32
        31
         		if key != "" {

      
A internal/plumbing/json/json_test.go
···
        
        1
        +package json

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"testing"

      
        
        5
        +

      
        
        6
        +	"github.com/olexsmir/viye/internal/viye"

      
        
        7
        +

      
        
        8
        +	"olexsmir.xyz/x/is"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +func TestMatch(t *testing.T) {

      
        
        12
        +	got := (Tool{}).Match(&viye.Context{Cmd: "json"})

      
        
        13
        +	is.Equal(t, true, got)

      
        
        14
        +}

      
        
        15
        +

      
        
        16
        +func TestExecute(t *testing.T) {

      
        
        17
        +	tests := map[string]struct {

      
        
        18
        +		body []string

      
        
        19
        +		want string

      
        
        20
        +	}{

      
        
        21
        +		"no body":          {want: ": name: your name\n: age: 0\n"},

      
        
        22
        +		"empty body slice": {want: ": name: your name\n: age: 0\n"},

      
        
        23
        +		"single key": {

      
        
        24
        +			body: []string{": name: olex"},

      
        
        25
        +			want: `| {"name":"olex"}` + "\n",

      
        
        26
        +		},

      
        
        27
        +		"multiple keys": {

      
        
        28
        +			body: []string{": name: olex", ": age: 30"},

      
        
        29
        +			want: `| {"age":"30","name":"olex"}` + "\n",

      
        
        30
        +		},

      
        
        31
        +		"values with spaces": {

      
        
        32
        +			body: []string{": title: hello world"},

      
        
        33
        +			want: `| {"title":"hello world"}` + "\n",

      
        
        34
        +		},

      
        
        35
        +		"keys with spaces": {

      
        
        36
        +			body: []string{": my key: value"},

      
        
        37
        +			want: `| {"my key":"value"}` + "\n",

      
        
        38
        +		},

      
        
        39
        +		"line without colon separator": {

      
        
        40
        +			body: []string{": justtext"},

      
        
        41
        +			want: `| {}` + "\n",

      
        
        42
        +		},

      
        
        43
        +		"line without : prefix": {

      
        
        44
        +			body: []string{"name: olex"},

      
        
        45
        +			want: `| {"name":"olex"}` + "\n",

      
        
        46
        +		},

      
        
        47
        +		"mixed valid and invalid": {

      
        
        48
        +			body: []string{": name: olex", "garbage", ": age: 30"},

      
        
        49
        +			want: `| {"age":"30","name":"olex"}` + "\n",

      
        
        50
        +		},

      
        
        51
        +		"empty key": {

      
        
        52
        +			body: []string{": : value"},

      
        
        53
        +			want: `| {}` + "\n",

      
        
        54
        +		},

      
        
        55
        +		"trailing whitespace in key": {

      
        
        56
        +			body: []string{": name  : olex"},

      
        
        57
        +			want: `| {"name":"olex"}` + "\n",

      
        
        58
        +		},

      
        
        59
        +		"trailing whitespace in value": {

      
        
        60
        +			body: []string{": name: olex  "},

      
        
        61
        +			want: `| {"name":"olex"}` + "\n",

      
        
        62
        +		},

      
        
        63
        +		"empty value": {

      
        
        64
        +			body: []string{": name: "},

      
        
        65
        +			want: `| {"name":""}` + "\n",

      
        
        66
        +		},

      
        
        67
        +	}

      
        
        68
        +	for name, tt := range tests {

      
        
        69
        +		t.Run(name, func(t *testing.T) {

      
        
        70
        +			got, err := (Tool{}).Execute(&viye.Context{Cmd: "json", Body: tt.body})

      
        
        71
        +			is.Err(t, err, nil)

      
        
        72
        +			is.Equal(t, tt.want, got)

      
        
        73
        +		})

      
        
        74
        +	}

      
        
        75
        +}

      
M internal/plumbing/shell/shell.go
···
        21
        21
         	return isCmd(ctx) || isBGCmd(ctx) || isKill(ctx) || isMkdir(ctx)

      
        22
        22
         }

      
        23
        23
         

      
        24
        
        -func (Tool) Execute(ctx *viye.Context) (string, error) {

      
        
        24
        +func (Tool) Execute(c *viye.Context) (string, error) {

      
        25
        25
         	switch {

      
        26
        
        -	case isCmd(ctx):

      
        27
        
        -		ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)

      
        
        26
        +	case isCmd(c):

      
        
        27
        +		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

      
        28
        28
         		defer cancel()

      
        29
        29
         

      
        30
        
        -		cmd := exec.CommandContext(ctxTimeout, "sh", "-c", strings.Join(ctx.Args, " "))

      
        31
        
        -		cmd.Dir = ctx.Dir

      
        
        30
        +		cmd := exec.CommandContext(ctx, "sh", "-c", strings.Join(c.Args, " "))

      
        
        31
        +		cmd.Dir = c.Dir

      
        32
        32
         		out, err := cmd.CombinedOutput()

      
        33
        33
         		if err != nil {

      
        34
        
        -			if ctxTimeout.Err() == context.DeadlineExceeded {

      
        
        34
        +			if ctx.Err() == context.DeadlineExceeded {

      
        35
        35
         				return "", fmt.Errorf("timeout: command exceeded 5s")

      
        36
        36
         			}

      
        37
        37
         		}

      
        38
        38
         		return viye.FormatOutput(string(out)), nil

      
        39
        39
         

      
        40
        
        -	case isBGCmd(ctx):

      
        41
        
        -		cmd := exec.Command("sh", "-c", strings.Join(ctx.Args, " "))

      
        
        40
        +	case isBGCmd(c):

      
        
        41
        +		cmd := exec.Command("sh", "-c", strings.Join(c.Args, " "))

      
        42
        42
         		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

      
        43
        
        -		cmd.Dir = ctx.Dir

      
        
        43
        +		cmd.Dir = c.Dir

      
        44
        44
         		if err := cmd.Start(); err != nil {

      
        45
        45
         			return "", err

      
        46
        46
         		}

      
        47
        47
         		return fmt.Sprintf("| pid: %d", cmd.Process.Pid), nil

      
        48
        48
         

      
        49
        
        -	case isKill(ctx):

      
        50
        
        -		pid, err := strconv.Atoi(ctx.Args[0])

      
        
        49
        +	case isKill(c):

      
        
        50
        +		pid, err := strconv.Atoi(c.Args[0])

      
        51
        51
         		if err != nil {

      
        52
        
        -			return "", fmt.Errorf("kill: invalid pid %q", ctx.Args[0])

      
        
        52
        +			return "", fmt.Errorf("kill: invalid pid %q", c.Args[0])

      
        53
        53
         		}

      
        54
        54
         		p, err := os.FindProcess(pid)

      
        55
        55
         		if err != nil {

      ···
        60
        60
         		}

      
        61
        61
         		return "| done\n", nil

      
        62
        62
         

      
        63
        
        -	case isMkdir(ctx):

      
        64
        
        -		path := filepath.Join(append([]string{ctx.Dir}, ctx.Args...)...)

      
        
        63
        +	case isMkdir(c):

      
        
        64
        +		path := filepath.Join(append([]string{c.Dir}, c.Args...)...)

      
        65
        65
         		if err := os.MkdirAll(path, 0o755); err != nil {

      
        66
        66
         			return "", fmt.Errorf("mkdir: %w", err)

      
        67
        67
         		}

      
M internal/plumbing/shell/shell_test.go
···
        6
        6
         	"testing"

      
        7
        7
         

      
        8
        8
         	"github.com/olexsmir/viye/internal/viye"

      
        
        9
        +

      
        
        10
        +	"olexsmir.xyz/x/is"

      
        9
        11
         )

      
        10
        12
         

      
        11
        13
         func TestMatch(t *testing.T) {

      
        12
        14
         	tests := []struct {

      
        13
        
        -		path string

      
        14
        
        -		cmd  string

      
        15
        
        -		args []string

      
        16
        
        -		want bool

      
        
        15
        +		path, cmd string

      
        
        16
        +		args      []string

      
        
        17
        +		want      bool

      
        17
        18
         	}{

      
        18
        19
         		{"$", "$", nil, true},

      
        19
        20
         		{"$ go run .", "$", []string{"go", "run", "."}, true},

      ···
        35
        36
         			Cmd:  tt.cmd,

      
        36
        37
         			Args: tt.args,

      
        37
        38
         		})

      
        38
        
        -		if got != tt.want {

      
        39
        
        -			t.Errorf("Match(%q) = %v; want %v", tt.path, got, tt.want)

      
        40
        
        -		}

      
        
        39
        +		is.Equal(t, tt.want, got)

      
        41
        40
         	}

      
        42
        41
         }

      
        43
        42
         

      ···
        49
        48
         			Args: []string{"echo", "hello"},

      
        50
        49
         			Dir:  ".",

      
        51
        50
         		})

      
        52
        
        -		if err != nil {

      
        53
        
        -			t.Fatal(err)

      
        54
        
        -		}

      
        55
        
        -		if got != "| hello\n" {

      
        56
        
        -			t.Errorf("Execute($ echo hello) = %q; want %q", got, "| hello\\n")

      
        57
        
        -		}

      
        
        51
        +		is.Err(t, err, nil)

      
        
        52
        +		is.Equal(t, "| hello\n", got)

      
        58
        53
         	})

      
        59
        54
         

      
        60
        55
         	t.Run("bg cmd", func(t *testing.T) {

      ···
        64
        59
         			Args: []string{"sleep", "0"},

      
        65
        60
         			Dir:  ".",

      
        66
        61
         		})

      
        67
        
        -		if err != nil {

      
        68
        
        -			t.Fatal(err)

      
        69
        
        -		}

      
        70
        
        -		if got != "| pid: " && len(got) < 10 {

      
        71
        
        -			t.Errorf("Execute($$ sleep 0) = %q; want pid string", got)

      
        
        62
        +		is.Err(t, err, nil)

      
        
        63
        +		if len(got) < 10 {

      
        
        64
        +			t.Fatalf("got short output %q, expected pid string", got)

      
        72
        65
         		}

      
        73
        66
         	})

      
        74
        67
         

      ···
        80
        73
         			Args: []string{"sub"},

      
        81
        74
         			Dir:  dir,

      
        82
        75
         		})

      
        83
        
        -		if err != nil {

      
        84
        
        -			t.Fatal(err)

      
        85
        
        -		}

      
        86
        
        -		if _, err := os.Stat(filepath.Join(dir, "sub")); err != nil {

      
        87
        
        -			t.Errorf("dir not created: %v", err)

      
        88
        
        -		}

      
        89
        
        -		if got != "| created "+filepath.Join(dir, "sub")+"\n" {

      
        90
        
        -			t.Errorf("output = %q; want %q", got, "| created "+filepath.Join(dir, "sub")+"\n")

      
        91
        
        -		}

      
        
        76
        +		is.Err(t, err, nil)

      
        
        77
        +

      
        
        78
        +		_, err = os.Stat(filepath.Join(dir, "sub"))

      
        
        79
        +		is.Err(t, err, nil)

      
        
        80
        +		is.Equal(t, "| created "+filepath.Join(dir, "sub")+"\n", got)

      
        92
        81
         	})

      
        93
        82
         

      
        94
        83
         	t.Run("kill", func(t *testing.T) {

      
        95
        
        -		// no reliable way to test kill without a real process,

      
        96
        
        -		// just verify it doesn't panic with valid args structure

      
        97
        
        -		if _, err := (&Tool{}).Execute(&viye.Context{

      
        
        84
        +		_, err := (&Tool{}).Execute(&viye.Context{

      
        98
        85
         			Path: []string{"kill 999999"},

      
        99
        86
         			Cmd:  "kill",

      
        100
        87
         			Args: []string{"999999"},

      
        101
        88
         			Dir:  ".",

      
        102
        
        -		}); err == nil {

      
        103
        
        -			t.Log("kill on bogus pid returned nil (expected error or success)")

      
        104
        
        -		}

      
        
        89
        +		})

      
        
        90
        +		is.NotEqual(t, nil, err)

      
        105
        91
         	})

      
        106
        92
         }

      
M internal/viye/viye_test.go
···
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"testing"

      
        
        5
        +

      
        
        6
        +	"olexsmir.xyz/x/is"

      
        5
        7
         )

      
        6
        8
         

      
        7
        9
         func TestSplitLeaf(t *testing.T) {

      ···
        25
        27
         	}

      
        26
        28
         	for _, tt := range tests {

      
        27
        29
         		cmd, args := splitLeaf(tt.leaf)

      
        28
        
        -		if cmd != tt.cmd {

      
        29
        
        -			t.Errorf("splitLeaf(%q) cmd = %q; want %q", tt.leaf, cmd, tt.cmd)

      
        30
        
        -		}

      
        31
        
        -		if len(args) != len(tt.args) {

      
        32
        
        -			t.Errorf("splitLeaf(%q) len(args) = %d; want %d", tt.leaf, len(args), len(tt.args))

      
        33
        
        -		} else {

      
        
        30
        +		is.Equal(t, tt.cmd, cmd)

      
        
        31
        +		is.Equal(t, len(tt.args), len(args))

      
        
        32
        +		if len(args) == len(tt.args) {

      
        34
        33
         			for i := range args {

      
        35
        
        -				if args[i] != tt.args[i] {

      
        36
        
        -					t.Errorf("splitLeaf(%q) args[%d] = %q; want %q", tt.leaf, i, args[i], tt.args[i])

      
        37
        
        -				}

      
        
        34
        +				is.Equal(t, tt.args[i], args[i])

      
        38
        35
         			}

      
        39
        36
         		}

      
        40
        37
         	}

      
        41
        38
         }

      
        42
        39
         

      
        43
        40
         func TestSplitArgs(t *testing.T) {

      
        44
        
        -	tests := []struct {

      
        45
        
        -		args  []string

      
        46
        
        -		want  []string

      
        47
        
        -		wantB []string

      
        48
        
        -	}{

      
        
        41
        +	tests := []struct{ args, want, wantB []string }{

      
        49
        42
         		{[]string{"/tmp/foo", "bar/baz"}, []string{"/tmp/foo", "bar/baz"}, nil},

      
        50
        43
         		{[]string{"./foo", "bar"}, []string{"./foo", "bar"}, nil},

      
        51
        44
         		{[]string{"./"}, []string{"./"}, nil},

      ···
        57
        50
         	}

      
        58
        51
         	for _, tt := range tests {

      
        59
        52
         		got, gotB := splitArgs(tt.args)

      
        60
        
        -		if len(got) != len(tt.want) {

      
        61
        
        -			t.Errorf("splitArgs(%v) path = %v; want %v", tt.args, got, tt.want)

      
        62
        
        -			continue

      
        63
        
        -		}

      
        64
        
        -		for i := range got {

      
        65
        
        -			if got[i] != tt.want[i] {

      
        66
        
        -				t.Errorf("splitArgs(%v) path = %v; want %v", tt.args, got, tt.want)

      
        67
        
        -				break

      
        
        53
        +		is.Equal(t, len(tt.want), len(got))

      
        
        54
        +		if len(got) == len(tt.want) {

      
        
        55
        +			for i := range got {

      
        
        56
        +				is.Equal(t, tt.want[i], got[i])

      
        68
        57
         			}

      
        69
        58
         		}

      
        70
        
        -		if len(gotB) != len(tt.wantB) {

      
        71
        
        -			t.Errorf("splitArgs(%v) body = %v; want %v", tt.args, gotB, tt.wantB)

      
        72
        
        -			continue

      
        73
        
        -		}

      
        74
        
        -		for i := range gotB {

      
        75
        
        -			if gotB[i] != tt.wantB[i] {

      
        76
        
        -				t.Errorf("splitArgs(%v) body = %v; want %v", tt.args, gotB, tt.wantB)

      
        77
        
        -				break

      
        
        59
        +		is.Equal(t, len(tt.wantB), len(gotB))

      
        
        60
        +		if len(gotB) == len(tt.wantB) {

      
        
        61
        +			for i := range gotB {

      
        
        62
        +				is.Equal(t, tt.wantB[i], gotB[i])

      
        78
        63
         			}

      
        79
        64
         		}

      
        80
        65
         	}