improve coverage

This commit is contained in:
booleanmaybe 2026-04-03 17:27:06 -04:00
parent fe47de1d68
commit bf2c2408ec
8 changed files with 1033 additions and 0 deletions

View file

@ -359,3 +359,89 @@ func TestGetTypeRegistry(t *testing.T) {
t.Error("expected 'bug' to be valid")
}
}
func TestMaybeGetTypeRegistry_Initialized(t *testing.T) {
setupTestRegistry(t, defaultTestStatuses())
reg, ok := MaybeGetTypeRegistry()
if !ok {
t.Fatal("expected MaybeGetTypeRegistry to return true after init")
}
if reg == nil {
t.Fatal("expected non-nil registry")
}
if !reg.IsValid("story") {
t.Error("expected 'story' to be valid")
}
}
func TestMaybeGetTypeRegistry_Uninitialized(t *testing.T) {
ClearStatusRegistry()
t.Cleanup(func() {
ResetStatusRegistry(defaultTestStatuses())
})
reg, ok := MaybeGetTypeRegistry()
if ok {
t.Error("expected MaybeGetTypeRegistry to return false when uninitialized")
}
if reg != nil {
t.Error("expected nil registry when uninitialized")
}
}
func TestGetStatusRegistry_PanicsWhenUninitialized(t *testing.T) {
ClearStatusRegistry()
t.Cleanup(func() {
ResetStatusRegistry(defaultTestStatuses())
})
defer func() {
r := recover()
if r == nil {
t.Fatal("expected panic from GetStatusRegistry when uninitialized")
}
}()
GetStatusRegistry()
}
func TestGetTypeRegistry_PanicsWhenUninitialized(t *testing.T) {
ClearStatusRegistry()
t.Cleanup(func() {
ResetStatusRegistry(defaultTestStatuses())
})
defer func() {
r := recover()
if r == nil {
t.Fatal("expected panic from GetTypeRegistry when uninitialized")
}
}()
GetTypeRegistry()
}
func TestLoadStatusRegistryFromFiles_NoStatuses(t *testing.T) {
dir := t.TempDir()
f := writeTempWorkflow(t, dir, `
views:
- name: backlog
`)
reg, path, err := loadStatusRegistryFromFiles([]string{f})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if reg != nil {
t.Error("expected nil registry when no statuses defined")
}
if path != "" {
t.Errorf("expected empty path, got %q", path)
}
}
func TestLoadStatusRegistryFromFiles_ReadError(t *testing.T) {
_, _, err := loadStatusRegistryFromFiles([]string{"/nonexistent/path/workflow.yaml"})
if err == nil {
t.Fatal("expected error for nonexistent file")
}
}

View file

@ -298,6 +298,118 @@ func TestTableFormatterEmptyString(t *testing.T) {
}
}
func TestTableFormatterAllFieldsDefault(t *testing.T) {
// bare select with nil fields resolves to all canonical fields
proj := &ruki.TaskProjection{
Fields: nil,
Tasks: []*task.Task{{ID: "TIKI-A00001", Title: "Test", Status: "ready", Priority: 3}},
}
var buf bytes.Buffer
if err := NewTableFormatter().Format(&buf, proj); err != nil {
t.Fatal(err)
}
out := buf.String()
// should include all canonical fields in the header
if !strings.Contains(out, "id") || !strings.Contains(out, "title") || !strings.Contains(out, "priority") {
t.Errorf("all-fields table should contain canonical fields:\n%s", out)
}
}
func TestRenderIntEdgeCases(t *testing.T) {
// renderInt with non-int value
if got := renderInt("not-an-int"); got != "" {
t.Errorf("renderInt(string) = %q, want empty", got)
}
// renderInt with 0
if got := renderInt(0); got != "0" {
t.Errorf("renderInt(0) = %q, want %q", got, "0")
}
// renderInt with valid int
if got := renderInt(42); got != "42" {
t.Errorf("renderInt(42) = %q, want %q", got, "42")
}
}
func TestRenderValueNil(t *testing.T) {
if got := renderValue(nil, 0); got != "" {
t.Errorf("renderValue(nil) = %q, want empty", got)
}
}
func TestRenderDateEdgeCases(t *testing.T) {
// renderDate with non-time value
if got := renderDate("not-a-time"); got != "" {
t.Errorf("renderDate(string) = %q, want empty", got)
}
}
func TestRenderTimestampEdgeCases(t *testing.T) {
// renderTimestamp with non-time value
if got := renderTimestamp("not-a-time"); got != "" {
t.Errorf("renderTimestamp(string) = %q, want empty", got)
}
}
func TestRenderListEdgeCases(t *testing.T) {
// renderList with non-slice value
if got := renderList(42); got != "" {
t.Errorf("renderList(int) = %q, want empty", got)
}
// renderList with nil slice
if got := renderList([]string(nil)); got != "" {
t.Errorf("renderList(nil) = %q, want empty", got)
}
}
func TestExtractFieldValueUnknown(t *testing.T) {
tk := &task.Task{ID: "TIKI-A00001"}
if got := extractFieldValue(tk, "nonexistent"); got != nil {
t.Errorf("extractFieldValue(unknown) = %v, want nil", got)
}
}
func TestTableFormatterIntField(t *testing.T) {
proj := &ruki.TaskProjection{
Fields: []string{"priority", "points"},
Tasks: []*task.Task{{Priority: 3, Points: 8}},
}
var buf bytes.Buffer
if err := NewTableFormatter().Format(&buf, proj); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "3") {
t.Errorf("missing priority value:\n%s", out)
}
if !strings.Contains(out, "8") {
t.Errorf("missing points value:\n%s", out)
}
}
func TestTableFormatterRecurrenceField(t *testing.T) {
proj := &ruki.TaskProjection{
Fields: []string{"recurrence"},
Tasks: []*task.Task{{Recurrence: "0 0 * * MON"}},
}
var buf bytes.Buffer
if err := NewTableFormatter().Format(&buf, proj); err != nil {
t.Fatal(err)
}
out := buf.String()
if !strings.Contains(out, "0 0 * * MON") {
t.Errorf("missing recurrence value:\n%s", out)
}
}
func nonEmptyLines(s string) []string {
var result []string
for _, line := range strings.Split(s, "\n") {

View file

@ -139,3 +139,31 @@ func TestRunSelectQueryUserFunction(t *testing.T) {
t.Errorf("user() should resolve to memory-user:\n%s", out)
}
}
func TestRunSelectQueryWhitespaceOnly(t *testing.T) {
s := setupRunnerTest(t)
var buf bytes.Buffer
err := RunSelectQuery(s, " ", &buf)
if err == nil {
t.Fatal("expected error for whitespace-only query")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("error should mention empty: %v", err)
}
}
func TestRunSelectQueryWithOrderBy(t *testing.T) {
s := setupRunnerTest(t)
var buf bytes.Buffer
err := RunSelectQuery(s, `select id, title order by priority`, &buf)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
if !strings.Contains(out, "TIKI-AAA001") || !strings.Contains(out, "TIKI-BBB002") {
t.Errorf("order by query should return all tasks:\n%s", out)
}
}

48
ruki/ast_test.go Normal file
View file

@ -0,0 +1,48 @@
package ruki
import "testing"
// TestConditionNodeInterface verifies all Condition implementors satisfy the interface.
func TestConditionNodeInterface(t *testing.T) {
conditions := []Condition{
&BinaryCondition{Op: "and"},
&NotCondition{},
&CompareExpr{Op: "="},
&IsEmptyExpr{},
&InExpr{},
&QuantifierExpr{Kind: "any"},
}
for _, c := range conditions {
c.conditionNode() // exercise marker method
}
if len(conditions) != 6 {
t.Errorf("expected 6 condition types, got %d", len(conditions))
}
}
// TestExprNodeInterface verifies all Expr implementors satisfy the interface.
func TestExprNodeInterface(t *testing.T) {
exprs := []Expr{
&FieldRef{Name: "status"},
&QualifiedRef{Qualifier: "old", Name: "status"},
&StringLiteral{Value: "hello"},
&IntLiteral{Value: 42},
&DateLiteral{},
&DurationLiteral{Value: 1, Unit: "day"},
&ListLiteral{},
&EmptyLiteral{},
&FunctionCall{Name: "now"},
&BinaryExpr{Op: "+"},
&SubQuery{},
}
for _, e := range exprs {
e.exprNode() // exercise marker method
}
if len(exprs) != 11 {
t.Errorf("expected 11 expr types, got %d", len(exprs))
}
}

474
ruki/lower_test.go Normal file
View file

@ -0,0 +1,474 @@
package ruki
import (
"testing"
)
// TestLower_DurationUnits exercises all duration unit branches in parseDurationLiteral
// through the parser (which calls lowerUnary → parseDurationLiteral).
func TestLower_DurationUnits(t *testing.T) {
p := newTestParser()
units := []struct {
literal string
}{
{"1day"},
{"2days"},
{"1week"},
{"3weeks"},
{"1month"},
{"2months"},
{"1year"},
{"1hour"},
{"1hours"},
{"30min"},
{"30mins"},
{"10sec"},
{"10secs"},
}
for _, tt := range units {
t.Run(tt.literal, func(t *testing.T) {
input := `select where due > 2026-01-01 + ` + tt.literal
_, err := p.ParseStatement(input)
if err != nil {
t.Fatalf("unexpected error for duration %s: %v", tt.literal, err)
}
})
}
}
// TestLower_DateLiteralInCondition exercises the date literal branch in lowerUnary.
func TestLower_DateLiteralInCondition(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where due = 2026-06-15`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Select == nil {
t.Fatal("expected select statement")
}
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", stmt.Select.Where)
}
dl, ok := cmp.Right.(*DateLiteral)
if !ok {
t.Fatalf("expected DateLiteral, got %T", cmp.Right)
}
if dl.Value.Year() != 2026 || dl.Value.Month() != 6 || dl.Value.Day() != 15 {
t.Errorf("unexpected date: %v", dl.Value)
}
}
// TestLower_IntLiteral exercises the int literal branch.
func TestLower_IntLiteral(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where priority = 3`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", stmt.Select.Where)
}
il, ok := cmp.Right.(*IntLiteral)
if !ok {
t.Fatalf("expected IntLiteral, got %T", cmp.Right)
}
if il.Value != 3 {
t.Errorf("expected 3, got %d", il.Value)
}
}
// TestLower_EmptyLiteral exercises the empty literal branch.
func TestLower_EmptyLiteral(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where assignee is empty`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ie, ok := stmt.Select.Where.(*IsEmptyExpr)
if !ok {
t.Fatalf("expected IsEmptyExpr, got %T", stmt.Select.Where)
}
if ie.Negated {
t.Error("expected non-negated is empty")
}
}
// TestLower_IsNotEmpty exercises the is not empty branch.
func TestLower_IsNotEmpty(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where assignee is not empty`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ie, ok := stmt.Select.Where.(*IsEmptyExpr)
if !ok {
t.Fatalf("expected IsEmptyExpr, got %T", stmt.Select.Where)
}
if !ie.Negated {
t.Error("expected negated is not empty")
}
}
// TestLower_NotIn exercises the not in branch.
func TestLower_NotIn(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where status not in ["done"]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
in, ok := stmt.Select.Where.(*InExpr)
if !ok {
t.Fatalf("expected InExpr, got %T", stmt.Select.Where)
}
if !in.Negated {
t.Error("expected negated not in")
}
}
// TestLower_In exercises the in branch.
func TestLower_In(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where status in ["done", "ready"]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
in, ok := stmt.Select.Where.(*InExpr)
if !ok {
t.Fatalf("expected InExpr, got %T", stmt.Select.Where)
}
if in.Negated {
t.Error("expected non-negated in")
}
}
// TestLower_QuantifierAll exercises the all quantifier branch.
func TestLower_QuantifierAll(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where dependsOn all status = "done"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
q, ok := stmt.Select.Where.(*QuantifierExpr)
if !ok {
t.Fatalf("expected QuantifierExpr, got %T", stmt.Select.Where)
}
if q.Kind != "all" {
t.Errorf("expected 'all', got %q", q.Kind)
}
}
// TestLower_ParenCondition exercises the parenthesized condition branch.
func TestLower_ParenCondition(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where (status = "done" or status = "ready")`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
bc, ok := stmt.Select.Where.(*BinaryCondition)
if !ok {
t.Fatalf("expected BinaryCondition, got %T", stmt.Select.Where)
}
if bc.Op != "or" {
t.Errorf("expected 'or', got %q", bc.Op)
}
}
// TestLower_ParenExpr exercises the parenthesized expression branch.
func TestLower_ParenExpr(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="x" priority=(1 + 2)`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
}
// TestLower_NotCondition exercises the not condition branch.
func TestLower_NotCondition(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where not status = "done"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
nc, ok := stmt.Select.Where.(*NotCondition)
if !ok {
t.Fatalf("expected NotCondition, got %T", stmt.Select.Where)
}
if nc.Inner == nil {
t.Fatal("expected non-nil inner condition")
}
}
// TestLower_SelectStar exercises the select * branch.
func TestLower_SelectStar(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select *`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Select.Fields != nil {
t.Errorf("expected nil fields for select *, got %v", stmt.Select.Fields)
}
}
// TestLower_SelectBare exercises bare select (no fields, no star).
func TestLower_SelectBare(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Select.Fields != nil {
t.Errorf("expected nil fields for bare select, got %v", stmt.Select.Fields)
}
}
// TestLower_SelectFields exercises the specific fields branch.
func TestLower_SelectFields(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select id, title, status`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Select.Fields == nil {
t.Fatal("expected non-nil fields")
}
expected := []string{"id", "title", "status"}
if len(stmt.Select.Fields) != len(expected) {
t.Fatalf("expected %d fields, got %d", len(expected), len(stmt.Select.Fields))
}
for i, f := range stmt.Select.Fields {
if f != expected[i] {
t.Errorf("fields[%d] = %q, want %q", i, f, expected[i])
}
}
}
// TestLower_OrderByAscDesc exercises the order by lowering with explicit asc/desc.
func TestLower_OrderByAscDesc(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select order by priority desc, title asc`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(stmt.Select.OrderBy) != 2 {
t.Fatalf("expected 2 order by clauses, got %d", len(stmt.Select.OrderBy))
}
if !stmt.Select.OrderBy[0].Desc {
t.Error("expected first clause to be desc")
}
if stmt.Select.OrderBy[1].Desc {
t.Error("expected second clause to be asc (not desc)")
}
}
// TestLower_FuncCallArgs exercises function call lowering with multiple args.
func TestLower_FuncCallArgs(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where contains(title, "bug") = contains(title, "fix")`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", stmt.Select.Where)
}
fc, ok := cmp.Left.(*FunctionCall)
if !ok {
t.Fatalf("expected FunctionCall, got %T", cmp.Left)
}
if fc.Name != "contains" {
t.Errorf("expected 'contains', got %q", fc.Name)
}
if len(fc.Args) != 2 {
t.Errorf("expected 2 args, got %d", len(fc.Args))
}
}
// TestLower_ListLiteral exercises list literal lowering.
func TestLower_ListLiteral(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="x" tags=["a", "b", "c"]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
// find the tags assignment
for _, a := range stmt.Create.Assignments {
if a.Field == "tags" {
ll, ok := a.Value.(*ListLiteral)
if !ok {
t.Fatalf("expected ListLiteral, got %T", a.Value)
}
if len(ll.Elements) != 3 {
t.Errorf("expected 3 elements, got %d", len(ll.Elements))
}
return
}
}
t.Fatal("tags assignment not found")
}
// TestLower_BinaryExpr exercises binary expression lowering.
func TestLower_BinaryExpr(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`create title="hello" + " world"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if stmt.Create == nil {
t.Fatal("expected create statement")
}
for _, a := range stmt.Create.Assignments {
if a.Field == "title" {
be, ok := a.Value.(*BinaryExpr)
if !ok {
t.Fatalf("expected BinaryExpr, got %T", a.Value)
}
if be.Op != "+" {
t.Errorf("expected '+', got %q", be.Op)
}
return
}
}
t.Fatal("title assignment not found")
}
// TestLower_SubQuery exercises subquery lowering inside count().
func TestLower_SubQuery(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where count(select where status = "done") >= 1`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", stmt.Select.Where)
}
fc, ok := cmp.Left.(*FunctionCall)
if !ok {
t.Fatalf("expected FunctionCall, got %T", cmp.Left)
}
if fc.Name != "count" {
t.Errorf("expected 'count', got %q", fc.Name)
}
sq, ok := fc.Args[0].(*SubQuery)
if !ok {
t.Fatalf("expected SubQuery arg, got %T", fc.Args[0])
}
if sq.Where == nil {
t.Fatal("expected non-nil where in subquery")
}
}
// TestLower_SubQueryBare exercises subquery without where inside count().
func TestLower_SubQueryBare(t *testing.T) {
p := newTestParser()
stmt, err := p.ParseStatement(`select where count(select) >= 0`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmp, ok := stmt.Select.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", stmt.Select.Where)
}
fc, ok := cmp.Left.(*FunctionCall)
if !ok {
t.Fatalf("expected FunctionCall, got %T", cmp.Left)
}
sq, ok := fc.Args[0].(*SubQuery)
if !ok {
t.Fatalf("expected SubQuery, got %T", fc.Args[0])
}
if sq.Where != nil {
t.Error("expected nil where in bare subquery")
}
}
// TestLower_QualifiedRef exercises qualified ref lowering in triggers.
func TestLower_QualifiedRef(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`before update where old.status = "in progress" deny "no"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cmp, ok := trig.Where.(*CompareExpr)
if !ok {
t.Fatalf("expected CompareExpr, got %T", trig.Where)
}
qr, ok := cmp.Left.(*QualifiedRef)
if !ok {
t.Fatalf("expected QualifiedRef, got %T", cmp.Left)
}
if qr.Qualifier != "old" || qr.Name != "status" {
t.Errorf("expected old.status, got %s.%s", qr.Qualifier, qr.Name)
}
}
// TestLower_TriggerDeny exercises the deny lowering path.
func TestLower_TriggerDeny(t *testing.T) {
p := newTestParser()
trig, err := p.ParseTrigger(`before delete deny "cannot delete"`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if trig.Deny == nil {
t.Fatal("expected non-nil deny")
}
if *trig.Deny != "cannot delete" {
t.Errorf("expected 'cannot delete', got %q", *trig.Deny)
}
}
// TestUnquoteString exercises the unquoteString helper directly.
func TestUnquoteString(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"simple quoted", `"hello"`, "hello"},
{"escaped quote", `"say \"hi\""`, `say "hi"`},
{"no quotes", "bare", "bare"},
{"empty quotes", `""`, ""},
{"single char", `"x"`, "x"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := unquoteString(tt.input)
if got != tt.want {
t.Errorf("unquoteString(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

167
task/status_test.go Normal file
View file

@ -0,0 +1,167 @@
package task
import (
"testing"
"github.com/boolean-maybe/tiki/config"
"github.com/boolean-maybe/tiki/workflow"
)
func setupStatusTestRegistry(t *testing.T) {
t.Helper()
config.ResetStatusRegistry([]workflow.StatusDef{
{Key: "backlog", Label: "Backlog", Emoji: "📥", Default: true},
{Key: "ready", Label: "Ready", Emoji: "📋", Active: true},
{Key: "in_progress", Label: "In Progress", Emoji: "⚙️", Active: true},
{Key: "review", Label: "Review", Emoji: "👀", Active: true},
{Key: "done", Label: "Done", Emoji: "✅", Done: true},
})
t.Cleanup(func() { config.ClearStatusRegistry() })
}
func TestParseStatus(t *testing.T) {
setupStatusTestRegistry(t)
tests := []struct {
name string
input string
wantStatus Status
wantOK bool
}{
{"valid status", "done", "done", true},
{"empty input returns default", "", "backlog", true},
{"normalized input", "In-Progress", "in_progress", true},
{"unknown status", "nonexistent", "backlog", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ParseStatus(tt.input)
if ok != tt.wantOK {
t.Errorf("ParseStatus(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
if got != tt.wantStatus {
t.Errorf("ParseStatus(%q) = %q, want %q", tt.input, got, tt.wantStatus)
}
})
}
}
func TestNormalizeStatus(t *testing.T) {
setupStatusTestRegistry(t)
if got := NormalizeStatus("DONE"); got != "done" {
t.Errorf("NormalizeStatus(%q) = %q, want %q", "DONE", got, "done")
}
if got := NormalizeStatus("unknown"); got != "backlog" {
t.Errorf("NormalizeStatus(%q) = %q, want %q (default)", "unknown", got, "backlog")
}
}
func TestMapStatus(t *testing.T) {
setupStatusTestRegistry(t)
if got := MapStatus("ready"); got != "ready" {
t.Errorf("MapStatus(%q) = %q, want %q", "ready", got, "ready")
}
}
func TestStatusToString(t *testing.T) {
setupStatusTestRegistry(t)
if got := StatusToString("done"); got != "done" {
t.Errorf("StatusToString(%q) = %q, want %q", "done", got, "done")
}
if got := StatusToString("nonexistent"); got != "backlog" {
t.Errorf("StatusToString(%q) = %q, want default", "nonexistent", got)
}
}
func TestStatusEmoji(t *testing.T) {
setupStatusTestRegistry(t)
if got := StatusEmoji("done"); got != "✅" {
t.Errorf("StatusEmoji(%q) = %q, want %q", "done", got, "✅")
}
if got := StatusEmoji("nonexistent"); got != "" {
t.Errorf("StatusEmoji(%q) = %q, want empty", "nonexistent", got)
}
}
func TestStatusLabel(t *testing.T) {
setupStatusTestRegistry(t)
if got := StatusLabel("in_progress"); got != "In Progress" {
t.Errorf("StatusLabel(%q) = %q, want %q", "in_progress", got, "In Progress")
}
if got := StatusLabel("nonexistent"); got != "nonexistent" {
t.Errorf("StatusLabel(%q) = %q, want raw key", "nonexistent", got)
}
}
func TestStatusDisplay(t *testing.T) {
setupStatusTestRegistry(t)
if got := StatusDisplay("done"); got != "Done ✅" {
t.Errorf("StatusDisplay(%q) = %q, want %q", "done", got, "Done ✅")
}
}
func TestStatusDisplay_NoEmoji(t *testing.T) {
config.ResetStatusRegistry([]workflow.StatusDef{
{Key: "plain", Label: "Plain", Default: true},
})
t.Cleanup(func() { config.ClearStatusRegistry() })
if got := StatusDisplay("plain"); got != "Plain" {
t.Errorf("StatusDisplay(%q) = %q, want %q (no emoji)", "plain", got, "Plain")
}
}
func TestDefaultStatus(t *testing.T) {
setupStatusTestRegistry(t)
if got := DefaultStatus(); got != "backlog" {
t.Errorf("DefaultStatus() = %q, want %q", got, "backlog")
}
}
func TestDoneStatus(t *testing.T) {
setupStatusTestRegistry(t)
if got := DoneStatus(); got != "done" {
t.Errorf("DoneStatus() = %q, want %q", got, "done")
}
}
func TestAllStatuses(t *testing.T) {
setupStatusTestRegistry(t)
all := AllStatuses()
expected := []Status{"backlog", "ready", "in_progress", "review", "done"}
if len(all) != len(expected) {
t.Fatalf("AllStatuses() returned %d, want %d", len(all), len(expected))
}
for i, s := range all {
if s != expected[i] {
t.Errorf("AllStatuses()[%d] = %q, want %q", i, s, expected[i])
}
}
}
func TestIsActiveStatus(t *testing.T) {
setupStatusTestRegistry(t)
if IsActiveStatus("backlog") {
t.Error("expected backlog to not be active")
}
if !IsActiveStatus("ready") {
t.Error("expected ready to be active")
}
if !IsActiveStatus("in_progress") {
t.Error("expected in_progress to be active")
}
if IsActiveStatus("done") {
t.Error("expected done to not be active")
}
}

View file

@ -157,6 +157,76 @@ func TestTypeHelpers_FallbackWithoutConfig(t *testing.T) {
})
}
func TestParseDisplay(t *testing.T) {
tests := []struct {
name string
input string
wantType Type
wantOK bool
}{
{"story display", "Story 🌀", TypeStory, true},
{"bug display", "Bug 💥", TypeBug, true},
{"spike display", "Spike 🔍", TypeSpike, true},
{"epic display", "Epic 🗂️", TypeEpic, true},
{"unknown display", "Unknown 🤷", TypeStory, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ParseDisplay(tt.input)
if ok != tt.wantOK {
t.Errorf("ParseDisplay(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
if got != tt.wantType {
t.Errorf("ParseDisplay(%q) = %q, want %q", tt.input, got, tt.wantType)
}
})
}
}
func TestAllTypes(t *testing.T) {
types := AllTypes()
if len(types) == 0 {
t.Fatal("AllTypes() returned empty list")
}
// verify well-known types are present
found := make(map[Type]bool)
for _, tp := range types {
found[tp] = true
}
for _, want := range []Type{TypeStory, TypeBug, TypeSpike, TypeEpic} {
if !found[want] {
t.Errorf("AllTypes() missing %q", want)
}
}
}
func TestParseType(t *testing.T) {
tests := []struct {
name string
input string
wantType Type
wantOK bool
}{
{"valid story", "story", TypeStory, true},
{"valid bug", "bug", TypeBug, true},
{"alias feature", "feature", TypeStory, true},
{"unknown", "nonsense", TypeStory, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := ParseType(tt.input)
if ok != tt.wantOK {
t.Errorf("ParseType(%q) ok = %v, want %v", tt.input, ok, tt.wantOK)
}
if got != tt.wantType {
t.Errorf("ParseType(%q) = %q, want %q", tt.input, got, tt.wantType)
}
})
}
}
// testStatusDefs returns the standard test status definitions.
func testStatusDefs() []config.StatusDef {
return []config.StatusDef{

View file

@ -244,6 +244,54 @@ func TestStatusRegistry_AllReturnsCopy(t *testing.T) {
}
}
func TestNewStatusRegistry_MultipleDoneWarns(t *testing.T) {
defs := []StatusDef{
{Key: "alpha", Label: "Alpha", Default: true, Done: true},
{Key: "beta", Label: "Beta", Done: true},
}
reg, err := NewStatusRegistry(defs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// first done wins
if reg.DoneKey() != "alpha" {
t.Errorf("expected done key 'alpha', got %q", reg.DoneKey())
}
}
func TestNewStatusRegistry_MultipleDefaultWarns(t *testing.T) {
defs := []StatusDef{
{Key: "alpha", Label: "Alpha", Default: true},
{Key: "beta", Label: "Beta", Default: true},
}
reg, err := NewStatusRegistry(defs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// first default wins
if reg.DefaultKey() != "alpha" {
t.Errorf("expected default key 'alpha', got %q", reg.DefaultKey())
}
}
func TestNewStatusRegistry_NoDoneKey(t *testing.T) {
defs := []StatusDef{
{Key: "alpha", Label: "Alpha", Default: true},
{Key: "beta", Label: "Beta"},
}
reg, err := NewStatusRegistry(defs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if reg.DoneKey() != "" {
t.Errorf("expected empty done key, got %q", reg.DoneKey())
}
// IsDone should return false for all statuses
if reg.IsDone("alpha") {
t.Error("expected alpha to not be done")
}
}
func TestStatusKeyConstants(t *testing.T) {
if StatusBacklog != "backlog" {
t.Errorf("StatusBacklog = %q", StatusBacklog)