* feat(cards): interactive card UI for slash commands on Feishu - Add platform-agnostic Card model (core/card.go) with builder API - Add CardSender/CardNavigable interfaces (core/interfaces.go) - Implement Feishu card renderer using column_set for mobile layout - Redesign all slash command responses to use interactive cards - Add handleCardNav router and executeCardAction for in-place updates - Convert /new /quiet /switch /stop to act: and nav: actions - /help card uses tab buttons grouped by Session/Agent/Tools/System - renderUpgradeCard: sync update check with 8s timeout - Fix: return error card instead of nil in handleCardNav /list /switch Made-with: Cursor * fix(cards): address PR #52 review feedback - Add enable_feishu_card config option (default true) so operators can fall back to plain-text replies when card permissions are unavailable; wraps Platform in interactivePlatform only when cards are enabled - Fix goroutine leak in renderUpgradeCard: use buffered channel (cap 1) so the background goroutine can always complete even after a timeout - Fix /stop concurrent logic in executeCardAction: consolidate state mutation under a single interactiveMu critical section - Fix code indentation in platform/feishu/card.go (action map and CardSelect/CardNote case blocks) - Add unit tests for Card.RenderText and Card.HasButtons (core/card_test.go) - Add Feishu platform tests for ReplyCard/SendCard and card renderer (platform/feishu/platform_test.go) - Update docs/feishu.md and README with enable_feishu_card option Made-with: Cursor
59 lines
1.6 KiB
Go
59 lines
1.6 KiB
Go
package core
|
|
|
|
import "testing"
|
|
|
|
func TestCardRenderText_IncludesAllElementTypes(t *testing.T) {
|
|
card := NewCard().
|
|
Title("Help", "blue").
|
|
Markdown("Use `/help` to see commands.").
|
|
Divider().
|
|
Buttons(PrimaryBtn("Run", "cmd:/run"), DefaultBtn("Cancel", "cmd:/cancel")).
|
|
ListItemBtn("Current session", "Switch", "primary", "act:/switch 1").
|
|
Select("Mode", []CardSelectOption{{Text: "Default", Value: "default"}, {Text: "YOLO", Value: "yolo"}}, "default").
|
|
Note("Tip: /new starts a fresh session.").
|
|
Build()
|
|
|
|
got := card.RenderText()
|
|
want := "**Help**\n\nUse `/help` to see commands.\n\n---\n\n[Run] [Cancel]\n\nCurrent session [Switch]\nMode: Default | YOLO\n\nTip: /new starts a fresh session."
|
|
if got != want {
|
|
t.Fatalf("RenderText() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCardHasButtons_DetectsInteractiveElements(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
card *Card
|
|
want bool
|
|
}{
|
|
{
|
|
name: "no interactive elements",
|
|
card: NewCard().Markdown("Plain text only").Build(),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "action row buttons",
|
|
card: NewCard().Buttons(DefaultBtn("Open", "cmd:/open")).Build(),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "list item button",
|
|
card: NewCard().ListItem("Session A", "Switch", "act:/switch 1").Build(),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "select dropdown",
|
|
card: NewCard().Select("Mode", []CardSelectOption{{Text: "Default", Value: "default"}}, "default").Build(),
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := tt.card.HasButtons(); got != tt.want {
|
|
t.Fatalf("HasButtons() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|