Files
pad/internal/cli/format_markdown_table_test.go
David Barkhausen b9381bf5f1 feat(cli): markdown output on the remaining list surfaces; broaden ANSI stripping (#1080)
Completes #898 and fixes #1076.

Markdown on the seven surfaces left out of #1070, so `--format markdown` is now
honestly global and the flag help collapses to "table, json, markdown":

- `item comments`, `item deps`, `project activity`, `attachment list`,
  `library list`, `role list`, `workspace members`.

Two of those are not tabular, and markdown follows the terminal shape rather
than forcing a table onto them:

- `item comments` keeps the attribution-line-then-body form, and the body is
  emitted VERBATIM. A comment body is authored as markdown; escaping it would
  turn its lists and code fences into literal text. Only the attribution line,
  which we construct, is sanitized.
- `item deps` keeps its two sections as `## Blocks` / `## Blocked by` lists.
  Colour carried the direction in the terminal (yellow out, red in); headings
  carry it here.

New shared spine: `cli.RenderMarkdownTable(w, headers, rows)`. Every cell is
escaped, and ragged rows are padded or truncated to the header width so a short
or long row can't shift the column count and break the table. Wiring a surface
is now naming columns and mapping rows.

#1076 — ANSI stripping covered only SGR (`ESC[…m`), so non-SGR CSI sequences,
OSC-8 hyperlinks, and stray C0 controls survived, both in the table width maths
and in markdown output whose doc comment promised escape-free text. Replaced
`sgrPattern` with `ansiPattern` + `stripANSI` covering OSC, CSI, two-character
Fe escapes, and stray C0/DEL, with TAB/LF/CR deliberately preserved for callers
that normalize them. `displayWidth` now uses it too: a control sequence is
zero-width, so counting it was a column-alignment bug of the same family.

Tests: 12 stripping cases, 4 table-helper cases (including ragged rows), 4
renderer cases for the two non-tabular surfaces, and the routing test extended
to 8 subtests — one per surface, driven through cobra against an httptest
server. Also covers the two gaps named in #1076: `item starred` and the scoped
`item list <collection>` path. Each new guard was proven by mutating the source
and watching it fail, not just by passing.

Gates: go build ./... PASS; go vet PASS; gofmt clean; golangci-lint 0 issues.
Both touched packages show the same 6+2 pre-existing Windows failures as clean
main under an identical sandboxed run.
2026-08-14 22:35:23 -04:00

76 lines
2.0 KiB
Go

package cli
import (
"bytes"
"strings"
"testing"
)
func TestRenderMarkdownTable_Shape(t *testing.T) {
var buf bytes.Buffer
RenderMarkdownTable(&buf, []string{"A", "B"}, [][]string{{"1", "2"}, {"3", "4"}})
want := strings.Join([]string{
"| A | B |",
"| --- | --- |",
"| 1 | 2 |",
"| 3 | 4 |",
"",
}, "\n")
if got := buf.String(); got != want {
t.Errorf("table =\n%q\nwant\n%q", got, want)
}
}
func TestRenderMarkdownTable_EscapesCells(t *testing.T) {
var buf bytes.Buffer
RenderMarkdownTable(&buf, []string{"A"}, [][]string{
{"pipe | here"},
{"line\nbreak"},
{"ansi \x1b[2K here"},
})
out := buf.String()
if !strings.Contains(out, `pipe \| here`) {
t.Errorf("pipe not escaped:\n%s", out)
}
if strings.Contains(out, "line\nbreak") {
t.Errorf("newline not collapsed:\n%q", out)
}
if strings.ContainsRune(out, 0x1b) {
t.Errorf("escape byte survived:\n%q", out)
}
// Header + separator + three rows, and nothing extra from the newline cell.
if n := len(strings.Split(strings.TrimRight(out, "\n"), "\n")); n != 5 {
t.Errorf("got %d lines, want 5:\n%s", n, out)
}
}
// A row with the wrong number of cells must not change the column count, or the
// whole table stops parsing as a table.
func TestRenderMarkdownTable_NormalizesRaggedRows(t *testing.T) {
var buf bytes.Buffer
RenderMarkdownTable(&buf, []string{"A", "B", "C"}, [][]string{
{"only-one"},
{"one", "two", "three", "four-is-too-many"},
})
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
for i, line := range lines {
if n := strings.Count(line, "|"); n != 4 {
t.Errorf("line %d has %d pipes, want 4 (3 cells): %q", i, n, line)
}
}
if strings.Contains(buf.String(), "four-is-too-many") {
t.Errorf("overflow cell was not dropped:\n%s", buf.String())
}
}
func TestRenderMarkdownTable_NoHeadersWritesNothing(t *testing.T) {
var buf bytes.Buffer
RenderMarkdownTable(&buf, nil, [][]string{{"x"}})
if buf.Len() != 0 {
t.Errorf("expected no output without headers, got %q", buf.String())
}
}