Files
nib-v1/internal/parse/grammar_test.go
T
lerko 51cbf86d77 feat(parse): add capture grammar parser
Extracts glyph prefix, @time anchors, #tags, and ^card suffixes from
raw input. Table-driven tests cover all grammar forms including edge
cases. 24 tests passing.
2026-05-14 11:09:27 -04:00

126 lines
3.9 KiB
Go

package parse
import (
"strings"
"testing"
)
func sp(s string) *string { return &s }
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
wantBody string
wantGlyph string
wantTime *string
wantTags []string
wantCard *string
wantErrSub string
}{
// Glyph detection
{"plain note", "hello world", "hello world", "note", nil, nil, nil, ""},
{"dash todo", "- deploy nginx", "deploy nginx", "todo", nil, nil, nil, ""},
{"unicode todo", "▸ deploy nginx", "deploy nginx", "todo", nil, nil, nil, ""},
{"star event", "* dentist", "dentist", "event", nil, nil, nil, ""},
{"unicode event", "◇ dentist", "dentist", "event", nil, nil, nil, ""},
// Time anchor
{"with time", "meeting @14:00", "meeting", "note", sp("14:00"), nil, nil, ""},
{"time at start", "@9:30 standup", "standup", "note", sp("9:30"), nil, nil, ""},
{"invalid hours", "meeting @25:00", "", "", nil, nil, nil, "invalid time"},
{"invalid minutes", "meeting @14:60", "", "", nil, nil, nil, "invalid time"},
// Tags
{"single tag", "deploy #ops", "deploy", "note", nil, []string{"ops"}, nil, ""},
{"multiple tags", "deploy #ops #infra", "deploy", "note", nil, []string{"ops", "infra"}, nil, ""},
{"duplicate tags", "deploy #ops #ops", "deploy", "note", nil, []string{"ops"}, nil, ""},
{"tag with hyphen", "task #dev-ops", "task", "note", nil, []string{"dev-ops"}, nil, ""},
// Card suffix
{"caret card", "trick #nginx ^card", "trick", "note", nil, []string{"nginx"}, sp("snippet"), ""},
{"caret c", "trick ^c", "trick", "note", nil, nil, sp("snippet"), ""},
{"caret template", "deploy ${host} ^template", "deploy ${host}", "note", nil, nil, sp("template"), ""},
{"caret snippet explicit", "trick ^snippet", "trick", "note", nil, nil, sp("snippet"), ""},
{"invalid card type", "thing ^bogus", "", "", nil, nil, nil, "invalid card type"},
// Combined
{"full input", "- deploy nginx to staging @15:00 #ops", "deploy nginx to staging", "todo", sp("15:00"), []string{"ops"}, nil, ""},
{"full with card", "figured out the proxy_pass trick #nginx ^card", "figured out the proxy_pass trick", "note", nil, []string{"nginx"}, sp("snippet"), ""},
// Edge cases
{"empty input", "", "", "", nil, nil, nil, "empty"},
{"only glyph", "-", "", "", nil, nil, nil, "empty body"},
{"only modifiers", "#ops @14:00", "", "", nil, nil, nil, "empty body"},
{"whitespace only", " ", "", "", nil, nil, nil, "empty"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.input)
if tt.wantErrSub != "" {
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErrSub)
}
if !strings.Contains(err.Error(), tt.wantErrSub) {
t.Fatalf("expected error containing %q, got %q", tt.wantErrSub, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Body != tt.wantBody {
t.Errorf("body: got %q, want %q", got.Body, tt.wantBody)
}
if got.Glyph != tt.wantGlyph {
t.Errorf("glyph: got %q, want %q", got.Glyph, tt.wantGlyph)
}
if !ptrEq(got.TimeAnchor, tt.wantTime) {
t.Errorf("time_anchor: got %v, want %v", strPtr(got.TimeAnchor), strPtr(tt.wantTime))
}
if !tagsEq(got.Tags, tt.wantTags) {
t.Errorf("tags: got %v, want %v", got.Tags, tt.wantTags)
}
if !ptrEq(got.CardSuffix, tt.wantCard) {
t.Errorf("card_suffix: got %v, want %v", strPtr(got.CardSuffix), strPtr(tt.wantCard))
}
})
}
}
func ptrEq(a, b *string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
func strPtr(p *string) string {
if p == nil {
return "<nil>"
}
return *p
}
func tagsEq(a, b []string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}