package tui import ( "context" "strings" "testing" tea "charm.land/bubbletea/v2" "gitgud.io/mike/mpv-manager/pkg/installer" ) func TestOperationStreamDrainsTrailingOutputBeforeTerminalResult(t *testing.T) { sandboxConfigHome(t) m := NewModel() m.state = StateInstalling m.installing = true m.installOperationType = OpTypeConfig output := make(chan string, 1) result := make(chan error, 1) result <- nil updated, cmd := m.Update(saveChannelsMsg{outputChan: output, installDone: result}) m = updated.(*Model) first := cmd() updated, cmd = m.Update(first) m = updated.(*Model) if !m.operationResultReceived { t.Fatal("ready terminal result was not received") } if m.installComplete { t.Fatal("terminal result completed before output closed and drained") } output <- "trailing output" close(output) for step := 0; step < 8 && !m.installComplete; step++ { if cmd == nil { t.Fatal("stream stopped before terminal delivery") } updated, cmd = m.Update(cmd()) m = updated.(*Model) } if !m.installComplete { t.Fatal("stream never delivered its terminal result") } if !strings.Contains(strings.Join(m.installOutput, "\n"), "trailing output") { t.Fatalf("trailing output was lost: %v", m.installOutput) } } func TestCtrlCCancelsAndJoinsWorkerBeforeQuit(t *testing.T) { sandboxConfigHome(t) m := NewModel() m.state = StateInstalling m.installing = true m.installOperationType = OpTypeConfig ctx, cancel := context.WithCancel(context.Background()) output := make(chan string) result := make(chan error, 1) workerStopped := make(chan struct{}) go func() { defer close(workerStopped) <-ctx.Done() output <- "worker shutdown complete" result <- ctx.Err() close(result) close(output) }() updated, streamCmd := m.Update(saveChannelsMsg{outputChan: output, installDone: result, cancel: cancel}) m = updated.(*Model) updated, quitCmd := m.Update(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl}) m = updated.(*Model) if quitCmd != nil { t.Fatal("Ctrl+C quit before the worker acknowledged cancellation") } if !m.operationCancelRequested || !m.operationQuitAfter { t.Fatal("Ctrl+C did not enter owned cancellation state") } cmd := streamCmd for step := 0; step < 12; step++ { if cmd == nil { t.Fatal("stream stopped before worker acknowledgement") } msg := cmd() if _, ok := msg.(tea.QuitMsg); ok { select { case <-workerStopped: default: t.Fatal("TUI emitted Quit before worker shutdown") } return } updated, cmd = m.Update(msg) m = updated.(*Model) } t.Fatal("Ctrl+C cancellation never emitted Quit after worker shutdown") } func TestEscapeCancelsAndReturnsOnlyAfterWorkerStops(t *testing.T) { sandboxConfigHome(t) m := NewModel() m.state = StateInstalling m.installing = true m.installOperationType = OpTypeConfig ctx, cancel := context.WithCancel(context.Background()) output := make(chan string) result := make(chan error, 1) workerStopped := make(chan struct{}) go func() { defer close(workerStopped) <-ctx.Done() result <- ctx.Err() close(result) close(output) }() updated, cmd := m.Update(saveChannelsMsg{outputChan: output, installDone: result, cancel: cancel}) m = updated.(*Model) updated, keyCmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) m = updated.(*Model) if keyCmd != nil || m.state != StateInstalling { t.Fatal("Escape released the installing state before worker shutdown") } for step := 0; step < 12 && m.state != StateMainMenu; step++ { if cmd == nil { t.Fatal("stream stopped before returning to the menu") } updated, cmd = m.Update(cmd()) m = updated.(*Model) } if m.state != StateMainMenu { t.Fatal("Escape did not return after worker shutdown") } select { case <-workerStopped: default: t.Fatal("menu was restored before the worker stopped") } } func TestTUICancellationWaitsForCommittedWorkerWithoutCancellingIt(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() output := make(chan string, 8) result := make(chan error, 1) committed := make(chan struct{}) finish := make(chan struct{}) guard := launchOperationWorker(ctx, output, result, func(_ context.Context, cr *installer.CommandRunner) error { if err := cr.BeginCommit(); err != nil { return err } close(committed) <-finish return ctx.Err() }) <-committed model := Model{operationCancel: cancel, operationCommitGuard: guard} model.requestOperationCancellation(false, true) if model.operationCancelRequested || !model.operationQuitAfter || ctx.Err() != nil { t.Fatal("committed worker was cancelled or quit intent was lost") } close(finish) if err := <-result; err != nil { t.Fatalf("commit result changed to cancellation: %v", err) } for range output { } }