mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
improve coverage
This commit is contained in:
parent
fe47de1d68
commit
bf2c2408ec
8 changed files with 1033 additions and 0 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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
48
ruki/ast_test.go
Normal 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
474
ruki/lower_test.go
Normal 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
167
task/status_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue