feat(cli): add read-only list and show subcommands
tapir list — table of stored summaries (date, title|id, channel, AI, fallback), recent-first. tapir show <video-id> — full summary with highlights and takeaways. DSN + user id from TAPIR_DB_DSN/TAPIR_USER_ID, never hardcoded. main.go gains a minimal os.Args[1] dispatcher kept flat so Worker F's auth/run cases union cleanly at merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
)
|
||||
|
||||
func TestFormatListColumnsAndOrdering(t *testing.T) {
|
||||
rows := []store.SummaryRow{
|
||||
{
|
||||
VideoID: "vid-newer",
|
||||
Title: "Newer Title",
|
||||
Channel: "youtube",
|
||||
AIProvider: "anthropic",
|
||||
PublishedAt: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
VideoID: "vid-older",
|
||||
Title: "Older Title",
|
||||
Channel: "youtube",
|
||||
AIProvider: "local",
|
||||
FallbackUsed: true,
|
||||
PublishedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
require.NoError(t, formatList(&b, rows))
|
||||
out := b.String()
|
||||
|
||||
require.Contains(t, out, "DATE")
|
||||
require.Contains(t, out, "TITLE")
|
||||
require.Contains(t, out, "CHANNEL")
|
||||
require.Contains(t, out, "FALLBACK")
|
||||
require.Contains(t, out, "Newer Title")
|
||||
require.Contains(t, out, "2026-02-01")
|
||||
require.Contains(t, out, "anthropic")
|
||||
|
||||
// formatList preserves caller ordering (the store query sorts recent-first).
|
||||
require.Less(t, strings.Index(out, "Newer Title"), strings.Index(out, "Older Title"))
|
||||
|
||||
// fallback flag rendered per row.
|
||||
older := lineContaining(t, out, "Older Title")
|
||||
require.Contains(t, older, "yes")
|
||||
newer := lineContaining(t, out, "Newer Title")
|
||||
require.Contains(t, newer, "no")
|
||||
}
|
||||
|
||||
func TestFormatListFallsBackToVideoIDWhenNoTitle(t *testing.T) {
|
||||
rows := []store.SummaryRow{{VideoID: "raw-video-id", CreatedAt: time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)}}
|
||||
|
||||
var b strings.Builder
|
||||
require.NoError(t, formatList(&b, rows))
|
||||
out := b.String()
|
||||
|
||||
require.Contains(t, out, "raw-video-id", "missing title falls back to video id")
|
||||
require.Contains(t, out, "2026-03-01", "missing publish date falls back to created_at")
|
||||
}
|
||||
|
||||
func TestFormatListEmpty(t *testing.T) {
|
||||
var b strings.Builder
|
||||
require.NoError(t, formatList(&b, nil))
|
||||
require.Contains(t, b.String(), "no summaries yet")
|
||||
}
|
||||
|
||||
func TestFormatShowFullSummary(t *testing.T) {
|
||||
row := store.SummaryRow{
|
||||
VideoID: "vid-1",
|
||||
Title: "Deep Dive",
|
||||
Channel: "youtube",
|
||||
URL: "https://example/watch",
|
||||
PublishedAt: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC),
|
||||
Summary: "The core argument.",
|
||||
Highlights: []string{"first point", "second point"},
|
||||
Takeaways: []string{"do this"},
|
||||
AIProvider: "local",
|
||||
AIModel: "qwen",
|
||||
FallbackUsed: true,
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
require.NoError(t, formatShow(&b, row))
|
||||
out := b.String()
|
||||
|
||||
require.Contains(t, out, "Deep Dive")
|
||||
require.Contains(t, out, "2026-05-02")
|
||||
require.Contains(t, out, "https://example/watch")
|
||||
require.Contains(t, out, "vid-1")
|
||||
require.Contains(t, out, "local/qwen")
|
||||
require.Contains(t, out, "fallback")
|
||||
require.Contains(t, out, "The core argument.")
|
||||
require.Contains(t, out, "first point")
|
||||
require.Contains(t, out, "second point")
|
||||
require.Contains(t, out, "do this")
|
||||
}
|
||||
|
||||
func TestFormatShowOmitsEmptySections(t *testing.T) {
|
||||
row := store.SummaryRow{
|
||||
VideoID: "vid-2",
|
||||
Summary: "Body only.",
|
||||
AIProvider: "local",
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
require.NoError(t, formatShow(&b, row))
|
||||
out := b.String()
|
||||
|
||||
require.Contains(t, out, "vid-2", "no title -> falls back to video id")
|
||||
require.Contains(t, out, "Body only.")
|
||||
require.NotContains(t, out, "Highlights:")
|
||||
require.NotContains(t, out, "Takeaways:")
|
||||
require.NotContains(t, out, "URL:")
|
||||
require.NotContains(t, out, "Channel:")
|
||||
}
|
||||
|
||||
func lineContaining(t *testing.T, text, needle string) string {
|
||||
t.Helper()
|
||||
for _, ln := range strings.Split(text, "\n") {
|
||||
if strings.Contains(ln, needle) {
|
||||
return ln
|
||||
}
|
||||
}
|
||||
t.Fatalf("no line containing %q in:\n%s", needle, text)
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user