// Markdown table bounds and transcript copy selection live in the terminal
// rendering path: the daemon e2e sees only committed text, never rendered
// rows, and no harness can drive a mouse selection, so these regressions are
// invisible outside the TUI.
package tui
import (
"strings"
"testing"
"albedo/cli/internal/daemon"
"github.com/charmbracelet/x/ansi"
)
const tableTranscript = "| Tool | Logged median | Warm in-kernel benchmark |\n" +
"|:---|---:|---:|\n" +
"| `files.read` | 3 ms | 0.07 ms |\n" +
"| `files.paths` | too few isolated calls | 12.7 ms |"
// Tables render inside the transcript's width: every cell that fits survives,
// a narrow table truncates its columns rather than its rows or its syntax,
// and escaped pipes and fenced literal tables are never parsed as tables.
func TestMarkdownTablesRenderWithinTheirBounds(t *testing.T) {
for _, tc := range []struct {
name string
text string
absent string
want []string
width int
}{
{name: "keeps every column it fits", text: tableTranscript, width: 80,
want: []string{"Tool", "files.read", "files.paths", "12.7 ms"}, absent: "|:---"},
{name: "narrow truncates columns, not rows or the syntax", text: tableTranscript, width: 38,
want: []string{"Tool", "files.read", "files.path", "12.7 ms"}, absent: "|:---"},
{name: "escaped pipes and fenced rows stay literal",
text: "Intro\n\n| Name | Value |\n| --- | --- |\n| a\\|b | `x\\|y` |\n\n" +
"```text\n| literal | row |\n| --- | --- |\n```\nOutro",
width: 72,
want: []string{"Intro", "a|b", "x|y", "| literal | row |", "| --- | --- |", "Outro"}, absent: ""},
} {
got := ansi.Strip(RenderMarkdownAnsi(tc.text, tc.width))
for _, value := range tc.want {
if !strings.Contains(got, value) {
t.Fatalf("%s: width %d lost %q:\n%s", tc.name, tc.width, value, got)
}
}
if tc.absent != "" && strings.Contains(got, tc.absent) {
t.Fatalf("%s: width %d exposed %q:\n%s", tc.name, tc.width, tc.absent, got)
}
for row := range strings.SplitSeq(got, "\n") {
if w := ansi.StringWidth(row); w > tc.width {
t.Fatalf("%s: width %d produced a %d-cell row: %q", tc.name, tc.width, w, row)
}
}
}
}
func TestSelectionSkipsTheRail(t *testing.T) {
lines := []string{"│ you", "│ can you list the tools", "", "│ albedo"}
sel := Selection{Anchor: Point{Row: 0, Col: 0}, Head: Point{Row: 3, Col: 8}, Gutter: railWidth}
if got, want := SelectedText(lines, sel), "you\ncan you list the tools\n\nalbedo"; got != want {
t.Fatalf("copied %q, want %q", got, want)
}
for _, row := range HighlightSelection(lines, sel) {
if strings.HasPrefix(row, "\x1b[7m") {
t.Fatalf("highlight covers the rail: %q", row)
}
}
}
// A copy is the text: no padding out to the viewport's width or height, no
// chrome, no code block frame or quote bars, and wrapped rows joined back
// into their lines.
func TestSelectionCopiesOnlyText(t *testing.T) {
m := newTestChatModel(t, &daemon.Session{ID: "s"})
m.SetSize(60, 70)
m.appendSettledEntry(HistoryEntry{Kind: EntryUser, Text: "why does the thing break when i do the other thing, it is really annoying"})
m.appendSettledEntry(HistoryEntry{Kind: EntryTool, ToolName: "bash"})
m.appendSettledEntry(HistoryEntry{Kind: EntryAssistant, Text: "a paragraph that is long enough that it will **definitely** wrap around the width.\n" +
"hard break\n\nsee https://example.com/a/really/long/path/without/any/spaces/in/it/at/all\n\n" +
"- an item that also goes on for a while so it wraps onto a second row\n\n" +
"```go\nfmt.Println(\"a line of code that is longer than the width of the chat\")\n```\n\n" +
"> quoted text that goes on long enough to wrap around the width\n> > twice\n\n" +
"done."})
m.appendSettledEntry(HistoryEntry{Kind: EntryTurnEnd, Mood: moodDone, ElapsedMs: 5000, Tools: 1})
m.refreshViewportContent()
lines := strings.Split(m.Viewport.View(), "\n")
all := Selection{Anchor: Point{Row: 0, Col: 0}, Head: Point{Row: len(lines) - 1, Col: 60}, Gutter: railWidth}
want := "why does the thing break when i do the other thing, it is really annoying\n\n" +
"a paragraph that is long enough that it will definitely wrap around the width.\n" +
"hard break\n\nsee https://example.com/a/really/long/path/without/any/spaces/in/it/at/all\n\n" +
"• an item that also goes on for a while so it wraps onto a second row\n\n" +
"fmt.Println(\"a line of code that is longer than the width of the chat\")\n\n" +
"quoted text that goes on long enough to wrap around the width\n\ntwice\n\n" +
"done."
if got := SelectedText(lines, all); got != want {
t.Fatalf("copied %q, want %q", got, want)
}
// chrome copies when it is all you selected
for i, line := range lines {
if strings.Contains(ansi.Strip(line), "bash") {
row := Selection{Anchor: Point{Row: i, Col: 0}, Head: Point{Row: i, Col: 60}, Gutter: railWidth}
if got := SelectedText(lines, row); got != "ran bash" {
t.Fatalf("copied the tool row as %q", got)
}
}
}
}