go/pkg/cli/tracker_test.go
Claude 228170d610
feat(cli): TUI components, Frame AppShell, command builders (#11-15)
Issues #11-13: WithAppName for variant binaries, NewPassthrough builder
for flag.FlagSet commands, RegisterCommands test coverage with resetGlobals
helper. Fix pre-existing daemon_test.go break.

Issue #14: Rich Table with box-drawing borders (Normal, Rounded, Heavy,
Double), per-column CellStyleFn, WithMaxWidth responsive truncation.
Tree renderer with box-drawing connectors and styled nodes. Parallel
TaskTracker with braille spinners, thread-safe concurrent updates, and
non-TTY fallback. Streaming text renderer with word-wrap and channel
pattern support.

Issue #15: Frame live compositional AppShell using HLCRF regions with
Model interface, Navigate/Back content swapping, alt-screen live mode,
graceful non-TTY fallback. Built-in region components: StatusLine,
KeyHints, Breadcrumb. Zero new dependencies — pure ANSI + x/term.

68 tests, all passing with -race.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 19:45:23 +00:00

188 lines
3.9 KiB
Go

package cli
import (
"bytes"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTaskTracker_Good(t *testing.T) {
t.Run("add and complete tasks", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{} // non-TTY
t1 := tr.Add("repo-a")
t2 := tr.Add("repo-b")
t1.Update("pulling...")
t2.Update("pulling...")
t1.Done("up to date")
t2.Done("3 commits behind")
out := tr.String()
assert.Contains(t, out, "repo-a")
assert.Contains(t, out, "repo-b")
assert.Contains(t, out, "up to date")
assert.Contains(t, out, "3 commits behind")
})
t.Run("task states", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
task := tr.Add("test")
// Pending
_, _, state := task.snapshot()
assert.Equal(t, taskPending, state)
// Running
task.Update("working")
_, status, state := task.snapshot()
assert.Equal(t, taskRunning, state)
assert.Equal(t, "working", status)
// Done
task.Done("finished")
_, status, state = task.snapshot()
assert.Equal(t, taskDone, state)
assert.Equal(t, "finished", status)
})
t.Run("task fail", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
task := tr.Add("bad-repo")
task.Fail("connection refused")
_, status, state := task.snapshot()
assert.Equal(t, taskFailed, state)
assert.Equal(t, "connection refused", status)
})
t.Run("concurrent updates", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
var wg sync.WaitGroup
for i := range 10 {
task := tr.Add("task-" + string(rune('a'+i)))
wg.Add(1)
go func(t *TrackedTask) {
defer wg.Done()
t.Update("running")
time.Sleep(5 * time.Millisecond)
t.Done("ok")
}(task)
}
wg.Wait()
assert.True(t, tr.allDone())
})
t.Run("summary all passed", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
tr.Add("a").Done("ok")
tr.Add("b").Done("ok")
tr.Add("c").Done("ok")
assert.Equal(t, "3/3 passed", tr.Summary())
})
t.Run("summary with failures", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
tr.Add("a").Done("ok")
tr.Add("b").Fail("error")
tr.Add("c").Done("ok")
assert.Equal(t, "2/3 passed, 1 failed", tr.Summary())
})
t.Run("wait completes for non-TTY", func(t *testing.T) {
var buf bytes.Buffer
tr := NewTaskTracker()
tr.out = &buf
task := tr.Add("quick")
go func() {
time.Sleep(10 * time.Millisecond)
task.Done("done")
}()
tr.Wait()
assert.Contains(t, buf.String(), "quick")
assert.Contains(t, buf.String(), "done")
})
t.Run("name width alignment", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
tr.Add("short")
tr.Add("very-long-repo-name")
w := tr.nameWidth()
assert.Equal(t, 19, w)
})
t.Run("String output format", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
tr.Add("repo-a").Done("clean")
tr.Add("repo-b").Fail("dirty")
tr.Add("repo-c").Update("pulling")
out := tr.String()
assert.Contains(t, out, "✓")
assert.Contains(t, out, "✗")
assert.Contains(t, out, "⠋")
})
}
func TestTaskTracker_Bad(t *testing.T) {
t.Run("allDone with no tasks", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
assert.True(t, tr.allDone())
})
t.Run("allDone incomplete", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
tr.Add("pending")
assert.False(t, tr.allDone())
})
}
func TestTrackedTask_Good(t *testing.T) {
t.Run("thread safety", func(t *testing.T) {
tr := NewTaskTracker()
tr.out = &bytes.Buffer{}
task := tr.Add("concurrent")
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
task.Update("running")
}()
}
wg.Wait()
_, status, state := task.snapshot()
require.Equal(t, taskRunning, state)
require.Equal(t, "running", status)
})
}