Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// W3 — clipboard & export. Regression tests for the judge-2 audit
// (see .ux-review/judge2_copy_export.md).
// Clipboard & export.

// F3/P1: raw cards (help/stats) carry styled ANSI — ^Y after /help must
// raw cards (help/stats) carry styled ANSI — ^Y after /help must
// copy the last real reply, never the card.
func TestLastReplySkipsRawCards(t *testing.T) {
m := newTestModel()
Expand All @@ -30,7 +29,7 @@ func TestLastReplySkipsRawCards(t *testing.T) {
}
}

// F1/F8: exports never silently overwrite — same base gets -1, -2… suffixes.
// Exports never silently overwrite — same base gets -1, -2… suffixes.
func TestWriteExportNeverOverwrites(t *testing.T) {
dir := t.TempDir()
p1, err := writeExport(dir, "sess-abc123", "md", []byte("one"))
Expand All @@ -56,7 +55,7 @@ func TestWriteExportNeverOverwrites(t *testing.T) {
}
}

// F1/P0: /export exists in the registry with an honest format guard.
// /export exists in the registry with an honest format guard.
func TestExportCommandRegistered(t *testing.T) {
found := false
for _, c := range slashCommands() {
Expand All @@ -77,7 +76,7 @@ func TestExportCommandRegistered(t *testing.T) {
}
}

// F2/P0: remote sessions skip exec helpers — the clipboard that matters is
// remote sessions skip exec helpers — the clipboard that matters is
// on the machine running the terminal.
func TestClipboardToolRemotePrefersOSC52(t *testing.T) {
t.Setenv("SSH_TTY", "/dev/ttys004")
Expand Down
25 changes: 9 additions & 16 deletions internal/tui/cosmetics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import (
"github.com/muesli/termenv"
)

// ── C2: the first-run home signposts the core interactions ────────────────

// the first-run home signposts the core interactions ───────────────
// TestWelcomeTipSignpostsCoreInteractions: the welcome tip must name the
// core composer interactions and command discovery.
func TestWelcomeTipSignpostsCoreInteractions(t *testing.T) {
Expand All @@ -21,7 +20,7 @@ func TestWelcomeTipSignpostsCoreInteractions(t *testing.T) {
t.Errorf("welcome tip missing %q:\n%s", want, out)
}
}
// C11: the ^K microcopy reads as a verb, not jargon.
// the ^K microcopy reads as a verb, not jargon.
if !strings.Contains(out, "^K commands") {
t.Errorf("welcome tip should identify the command palette:\n%s", out)
}
Expand All @@ -30,8 +29,7 @@ func TestWelcomeTipSignpostsCoreInteractions(t *testing.T) {
}
}

// ── C4: one failure glyph ──────────────────────────────────────────────────

// one failure glyph ─────────────────────────────────────────────────
// TestFailureGlyphStandardized: ✗ is the single failure glyph; the lamp
// set must not carry the stray ✕.
func TestFailureGlyphStandardized(t *testing.T) {
Expand All @@ -40,8 +38,7 @@ func TestFailureGlyphStandardized(t *testing.T) {
}
}

// ── C5: lamp glyphs belong to the connection state alone ──────────────────

// lamp glyphs belong to the connection state alone ─────────────────
// TestLampGlyphsReserved: no other surface may reuse ● ◉ ◌ ○ — those four
// cells are the connection lamp's vocabulary.
func TestLampGlyphsReserved(t *testing.T) {
Expand All @@ -61,8 +58,7 @@ func TestLampGlyphsReserved(t *testing.T) {
}
}

// ── C3: header instruments get a first-turn decoder ───────────────────────

// header instruments get a first-turn decoder ──────────────────────
// TestCtxHintOnFirstPrompt: the first real prompt teaches what the ctx
// gauge and the connection lamp mean.
func TestCtxHintOnFirstPrompt(t *testing.T) {
Expand All @@ -88,8 +84,7 @@ func TestCtxHintOnFirstPrompt(t *testing.T) {
}
}

// ── C11: microcopy ────────────────────────────────────────────────────────

// microcopy ───────────────────────────────────────────────────────
// TestSwarmHintMicrocopy: the swarm tip speaks in plain verbs.
func TestSwarmHintMicrocopy(t *testing.T) {
if strings.Contains(hintSwarmText, "registry") || strings.Contains(hintSwarmText, "chips") {
Expand All @@ -113,8 +108,7 @@ func TestElapsedCarriesLabel(t *testing.T) {
}
}

// ── C1: the faint contract — body text never renders in faint ────────────

// the faint contract — body text never renders in faint ───────────
// TestStepArgNotFaint: step arguments are machine-voice secondary text and
// must take muted, not the chrome-only faint token.
func TestStepArgNotFaint(t *testing.T) {
Expand All @@ -127,8 +121,7 @@ func TestStepArgNotFaint(t *testing.T) {
}
}

// ── C9: the queued count lives in the footer alone ────────────────────────

// the queued count lives in the footer alone ───────────────────────
// TestApprovalHeadDropsQueuedChip: with a queue behind the head approval,
// the card head no longer repeats the count the footer already carries.
func TestApprovalHeadDropsQueuedChip(t *testing.T) {
Expand All @@ -138,7 +131,7 @@ func TestApprovalHeadDropsQueuedChip(t *testing.T) {
if strings.Contains(body, "queued") {
t.Errorf("approval card head still carries the queued count:\n%s", body)
}
// F1: the shelf chip is the single owner of the PROMPT-queue count — the
// the shelf chip is the single owner of the PROMPT-queue count — the
// footer must not repeat it (the approval head's own queue hint stays).
foot := plain(m.footer())
if strings.Contains(foot, "▸") && strings.Contains(foot, "queued") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import (
"github.com/charmbracelet/lipgloss"
)

// W1 hardening — sanitize coverage, width-discipline, and wire-field hygiene.
// Regression tests for the judge-5 robustness audit (see .ux-review/).
// Hardening — sanitize coverage, width-discipline, and wire-field hygiene.

func TestSanitizeStripsInvisibleClasses(t *testing.T) {
cases := []struct{ name, in, want string }{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import (
"github.com/charmbracelet/lipgloss"
)

// W6 — hostile terminals (contained items). Regression tests for the
// judge-5 audit E1 and the statusLine clamp.
// Hostile terminals (contained items), plus the statusLine clamp.

// E1: below the old 3-row viewport floor the View was taller than the
// below the old 3-row viewport floor the View was taller than the
// terminal — permanent scroll jitter in alt-screen. The View must fit at
// any height ≥ the minimum layout.
func TestViewFitsShortTerminals(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,7 @@ func (m *Model) relayout() {
vpH := m.height - headerHeight - footerHeight - m.inputAreaHeight() - sheet
// Degradation ladder, step 1: when the layout doesn't fit, give the
// transcript rows back by shrinking the composer first — a one-visible-
// row terminal beats a View that never fits (judge-5 E1).
// row terminal beats a View that never fits.
if vpH < 1 && m.ta.Height() > 1 {
over := 1 - vpH
m.ta.SetHeight(max(1, m.ta.Height()-over))
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/quit_confirm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestQuitGateOtherKeyDisarms(t *testing.T) {
}
// The gate eats exactly one decision key: the disarming printable rune
// falls through to the composer, so "esc/^C, keep typing" never loses a
// character (judge-3 F2).
// character.
if got := m.ta.Value(); got != "n" {
t.Errorf("disarm keypress did not reach the input: %q", got)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// Round-4 wave-1 regressions: runCtxCum reset on error, usage straggler
// Telemetry event regressions: runCtxCum reset on error, usage straggler
// must not open an orphan turn, stale wakeArmed must not mislabel.

func TestErrorResetsRunCtxCum(t *testing.T) {
Expand Down
15 changes: 5 additions & 10 deletions internal/tui/transcript_fixes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// ── F1: the queue count has a single owner — the shelf chip ────────────────

// the queue count has a single owner — the shelf chip ───────────────
// TestQueueCountSingleOwner: with prompts queued mid-turn, the shelf chip is
// the ONLY surface carrying the count — the status line and the footer must
// not repeat it.
Expand All @@ -31,8 +30,7 @@ func TestQueueCountSingleOwner(t *testing.T) {
}
}

// ── F2: the status line never hides on disconnect ──────────────────────────

// the status line never hides on disconnect ─────────────────────────
// TestStatusLineReconnectState: while disconnected the status line renders
// the reconnect state in-place instead of vanishing — and never shows the
// normal busy spinner label.
Expand Down Expand Up @@ -67,8 +65,7 @@ func TestStatusLineReconnectState(t *testing.T) {
}
}

// ── F3: one steady new-output row — no insert/remove reflow ────────────────

// one steady new-output row — no insert/remove reflow ───────────────
// TestNewOutputRowSteady: the new-output indicator lives on ONE footer row
// that never inserts or removes a line — the layout height must not change
// when the busy state toggles while scrolled up.
Expand Down Expand Up @@ -101,8 +98,7 @@ func TestNewOutputRowSteady(t *testing.T) {
}
}

// ── F4: a failed turn marks its head ───────────────────────────────────────

// a failed turn marks its head ──────────────────────────────────────
// TestFailedTurnHeadMarked: an error event on the streaming turn sets a
// sanitized failed flag that paints ✗ on the turn head and survives
// finalization within this session. (Replay does not restore it: the
Expand Down Expand Up @@ -143,8 +139,7 @@ func TestFailedTurnHeadMarked(t *testing.T) {
}
}

// ── F5: visible notices are capped to one line ─────────────────────────────

// visible notices are capped to one line ────────────────────────────
// TestNoticeCapOneLine: only the latest unexpired notice renders, folded into
// a single line; older ones collapse into a count instead of stacking.
func TestNoticeCapOneLine(t *testing.T) {
Expand Down
17 changes: 6 additions & 11 deletions internal/tui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ func (m *Model) plainView() string {
return strings.Join(parts, "\n")
}

// ── header ─────────────────────────────────────────────────────────────────

// ── header ────────────────────────────────────────────────────────────────
func (m *Model) header() string {
th := m.th
// The logo gradient is width-independent, so render it once and cache it
Expand Down Expand Up @@ -334,7 +333,7 @@ func (m *Model) statusLine() string {
return ""
}
th := m.th
// F2: a dropped socket is exactly when the reader needs this row —
// a dropped socket is exactly when the reader needs this row —
// instead of hiding, the status line owns the reconnect state in-place.
if m.disconn {
label := "◌ disconnected · ⏎ retry"
Expand Down Expand Up @@ -390,8 +389,7 @@ func (m *Model) statusLineVisible() bool {
return (m.busy || m.disconn) && m.curApproval() == nil
}

// ── transcript ───────────────────────────────────────────────────────────

// ── transcript ──────────────────────────────────────────────────────────
// streamRenderInterval is the coalescing window for high-frequency streaming
// events (tokens, thinking): instead of rebuilding the viewport — which
// re-runs glamour on the streaming tail — per event, they share one rebuild.
Expand Down Expand Up @@ -1486,8 +1484,7 @@ func (m *Model) renderNotices() string {
return line
}

// ── input / approval area ──────────────────────────────────────────────────

// ── input / approval area ─────────────────────────────────────────────────
func (m *Model) inputArea() string {
box := m.th.inputBox.Width(m.cardWidth()).Render(m.ta.View())
var above []string
Expand Down Expand Up @@ -1643,8 +1640,7 @@ func (m *Model) approvalBody() string {
return strings.Join(lines, "\n")
}

// ── footer ─────────────────────────────────────────────────────────────────

// ── footer ────────────────────────────────────────────────────────────────
func (m *Model) footer() string {
return ansi.Truncate(m.footerContent(), max(1, m.width), "")
}
Expand Down Expand Up @@ -2025,8 +2021,7 @@ func (m *Model) panelFooter(hints ...string) string {
return ansi.Truncate(out+last, max(1, m.width), "")
}

// ── small helpers ──────────────────────────────────────────────────────────

// ── small helpers ─────────────────────────────────────────────────────────
// plural renders a count with its noun inflected: plural(1, "match",
// "matches"), plural(3, "approval", "approvals"). Every "%d <noun>" surface
// goes through this so a single hit can never read as "1 matches" again.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import (
"github.com/BackendStack21/bodek/internal/client"
)

// W2 — visible state & the escape stack. Regression tests for the
// judge-3 interaction audit (see .ux-review/judge3_interaction.md).
// Visible state & the escape stack.

// F1/P0: an armed stop-agent gate must be visible when armed from the
// an armed stop-agent gate must be visible when armed from the
// composer (panel == none) — footer carries the headline and the y confirm.
func TestStopAgentGateVisibleFromComposer(t *testing.T) {
m := newTestModel()
Expand All @@ -22,7 +21,7 @@ func TestStopAgentGateVisibleFromComposer(t *testing.T) {
}
}

// F2/P1: a disarmed printable rune falls through to the composer — the gate
// a disarmed printable rune falls through to the composer — the gate
// eats exactly one decision key, never the user's first keystroke.
func TestDisarmedRuneFallsThroughToComposer(t *testing.T) {
m := newTestModel()
Expand All @@ -41,7 +40,7 @@ func TestDisarmedRuneFallsThroughToComposer(t *testing.T) {
}
}

// F4/P1: lowercase n is next (vim/less reflex), never query corruption.
// lowercase n is next (vim/less reflex), never query corruption.
func TestFindNextBinding(t *testing.T) {
m := newTestModel()
m.msgs = append(m.msgs,
Expand All @@ -65,7 +64,7 @@ func TestFindNextBinding(t *testing.T) {
}
}

// F10/P2: queue deletes are two-step — first d arms, second d deletes.
// queue deletes are two-step — first d arms, second d deletes.
func TestQueueDeleteTwoStep(t *testing.T) {
m := newTestModel()
m.queue = []string{"alpha", "beta"}
Expand Down Expand Up @@ -108,7 +107,7 @@ func TestEventsSelectionRendered(t *testing.T) {
}
}

// F3/P1: /help teaches only real keys — no phantom r, and the marquee
// /help teaches only real keys — no phantom r, and the marquee
// features (^S stop sub-agent, alt+f find, ^K palette) are listed.
func TestHelpTeachesRealKeys(t *testing.T) {
m := newTestModel()
Expand All @@ -124,7 +123,7 @@ func TestHelpTeachesRealKeys(t *testing.T) {
}
}

// F7/P2: turn jumps name their landing spot — the copy target (alt+y) is
// turn jumps name their landing spot — the copy target (alt+y) is
// verifiable on screen.
func TestJumpTurnReportsLanding(t *testing.T) {
m := newTestModel()
Expand Down
Loading