From dbd47462ab580fdbd96dd2f8614ef5bbb1c81837 Mon Sep 17 00:00:00 2001 From: hive-quality Date: Tue, 1 Sep 2026 08:26:21 -0400 Subject: [PATCH] =?UTF-8?q?[quality]=20=F0=9F=A7=AA=20test(tui):=20cover?= =?UTF-8?q?=20the=20zero-covered=20attach=20error=20types,=20KickResult.Qu?= =?UTF-8?q?eued,=20and=20bare=20stub.View?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-function coverage showed four 0%% seams in pkg/tui that no open PR claims: - attach.go: tmuxNotFoundError.Error/Unwrap and tmuxSessionMissingError.Unwrap — the typed-error contract the attach preflight footer and errors.Is chains depend on. New attach_errors_test.go pins the exact messages (with/without tmux detail) and that both Unwrap methods expose the underlying cause. - client/actions.go: KickResult.Queued — the #5325 async-kick seam distinguishing 'queued' from 'in-flight'. New kick_result_test.go pins both wire values, the empty case, and case-sensitivity. - panes/pane.go: stub.View — every shipped pane overrides View, so the shared fallback is reachable only through the next pane that embeds stub; stub_view_test.go pins it directly (box fill + degenerate size). app.Run stays uncovered: it wraps tea.NewProgram().Run() on the live terminal and needs a seam, not a test-only workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: hive-quality --- src/pkg/tui/attach_errors_test.go | 61 ++++++++++++++++++++++++++ src/pkg/tui/client/kick_result_test.go | 26 +++++++++++ src/pkg/tui/panes/stub_view_test.go | 32 ++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/pkg/tui/attach_errors_test.go create mode 100644 src/pkg/tui/client/kick_result_test.go create mode 100644 src/pkg/tui/panes/stub_view_test.go diff --git a/src/pkg/tui/attach_errors_test.go b/src/pkg/tui/attach_errors_test.go new file mode 100644 index 000000000..9356316e8 --- /dev/null +++ b/src/pkg/tui/attach_errors_test.go @@ -0,0 +1,61 @@ +package tui + +import ( + "errors" + "testing" +) + +// The attach preflight communicates failures through two typed errors so the +// app (and these tests) can branch on cause without matching prose. Their +// Error/Unwrap contracts were previously uncovered: a reworded message or a +// dropped Unwrap would have shipped silently, breaking errors.Is chains and +// the operator-facing footer text. + +func TestTmuxNotFoundErrorMessageAndUnwrap(t *testing.T) { + cause := errors.New("exec: \"tmux\": executable file not found in $PATH") + err := &tmuxNotFoundError{err: cause} + + want := "tmux is not installed or is not available in PATH" + if got := err.Error(); got != want { + t.Errorf("Error() = %q, want %q", got, want) + } + if !errors.Is(err, cause) { + t.Error("errors.Is(err, cause) = false, want true — Unwrap must expose the LookPath error") + } + + var typed *tmuxNotFoundError + if !errors.As(error(err), &typed) { + t.Error("errors.As failed to recover *tmuxNotFoundError") + } +} + +func TestTmuxSessionMissingErrorMessageVariants(t *testing.T) { + cause := errors.New("exit status 1") + + t.Run("without detail", func(t *testing.T) { + err := &tmuxSessionMissingError{session: "hive-scout", err: cause} + want := `tmux session "hive-scout" is unavailable` + if got := err.Error(); got != want { + t.Errorf("Error() = %q, want %q", got, want) + } + }) + + t.Run("with detail", func(t *testing.T) { + err := &tmuxSessionMissingError{ + session: "hive-scout", + detail: "no server running on /tmp/tmux-1001/default", + err: cause, + } + want := `tmux session "hive-scout" is unavailable: no server running on /tmp/tmux-1001/default` + if got := err.Error(); got != want { + t.Errorf("Error() = %q, want %q", got, want) + } + }) + + t.Run("unwrap exposes tmux exit error", func(t *testing.T) { + err := &tmuxSessionMissingError{session: "hive-scout", err: cause} + if !errors.Is(err, cause) { + t.Error("errors.Is(err, cause) = false, want true — Unwrap must expose the has-session error") + } + }) +} diff --git a/src/pkg/tui/client/kick_result_test.go b/src/pkg/tui/client/kick_result_test.go new file mode 100644 index 000000000..9b1c2b969 --- /dev/null +++ b/src/pkg/tui/client/kick_result_test.go @@ -0,0 +1,26 @@ +package client + +import "testing" + +// KickResult.Queued is the seam callers use to distinguish "this call queued +// the prompt" from "an in-flight delivery absorbed it" (#5325 async kick). +// It was previously uncovered, so a typo against the wrong wire constant +// ("in-flight", or the lifecycle "resumed" mistake documented on Paused) +// would not have failed any test. +func TestKickResultQueued(t *testing.T) { + cases := []struct { + status string + want bool + }{ + {"queued", true}, + {"in-flight", false}, // deduplicated into an existing delivery + {"", false}, + {"Queued", false}, // wire values are lowercase; case must not be folded + } + for _, tc := range cases { + r := KickResult{Status: tc.status, Agent: "scout"} + if got := r.Queued(); got != tc.want { + t.Errorf("KickResult{Status: %q}.Queued() = %v, want %v", tc.status, got, tc.want) + } + } +} diff --git a/src/pkg/tui/panes/stub_view_test.go b/src/pkg/tui/panes/stub_view_test.go new file mode 100644 index 000000000..83505e906 --- /dev/null +++ b/src/pkg/tui/panes/stub_view_test.go @@ -0,0 +1,32 @@ +package panes + +import ( + "strings" + "testing" +) + +// TestBareStubViewRendersPlaceholder pins stub.View directly. Every shipped +// pane now overrides View, so this method is reachable only through the NEXT +// pane that embeds stub before its real content lands — exactly the moment +// nothing else would catch a regression in the shared fallback. +func TestBareStubViewRendersPlaceholder(t *testing.T) { + s := stub{title: "PENDING"} + + const w, h = 30, 6 + view := s.View(w, h) + for _, want := range []string{"PENDING", placeholder} { + if !strings.Contains(view, want) { + t.Fatalf("stub.View() missing %q:\n%s", want, view) + } + } + if lines := strings.Count(view, "\n") + 1; lines != h { + t.Fatalf("stub.View() renders %d lines, want exactly %d", lines, h) + } + if vw := visibleWidth(view); vw != w { + t.Fatalf("stub.View() widest line is %d cells, want exactly %d", vw, w) + } + + if got := s.View(0, 0); got != "" { + t.Fatalf("stub.View(0,0) = %q, want empty", got) + } +}