feat(veans): add canonical status to bucket-title mapping
This commit is contained in:
parent
36fb0f0ace
commit
6b48a37710
|
|
@ -0,0 +1,123 @@
|
|||
// Package status maps the five canonical veans statuses to Vikunja bucket
|
||||
// IDs and the `done` flag. The mapping is canonical and reflected verbatim
|
||||
// in the agent prompt (see internal/commands/prompt.tmpl).
|
||||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/veans/internal/config"
|
||||
)
|
||||
|
||||
// Status is the agent-facing state name.
|
||||
type Status string
|
||||
|
||||
const (
|
||||
Todo Status = "todo"
|
||||
InProgress Status = "in-progress"
|
||||
InReview Status = "in-review"
|
||||
Completed Status = "completed"
|
||||
Scrapped Status = "scrapped"
|
||||
)
|
||||
|
||||
// All returns the canonical statuses in display order.
|
||||
func All() []Status {
|
||||
return []Status{Todo, InProgress, InReview, Completed, Scrapped}
|
||||
}
|
||||
|
||||
// CanonicalBucketTitles is the strict-with-override list seeded by `init`.
|
||||
var CanonicalBucketTitles = []string{
|
||||
"Todo",
|
||||
"In Progress",
|
||||
"In Review",
|
||||
"Done",
|
||||
"Scrapped",
|
||||
}
|
||||
|
||||
// BucketTitle returns the bucket name that backs each status.
|
||||
func (s Status) BucketTitle() string {
|
||||
switch s {
|
||||
case Todo:
|
||||
return "Todo"
|
||||
case InProgress:
|
||||
return "In Progress"
|
||||
case InReview:
|
||||
return "In Review"
|
||||
case Completed:
|
||||
return "Done"
|
||||
case Scrapped:
|
||||
return "Scrapped"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Done reports whether tasks in this status should have done=true.
|
||||
func (s Status) Done() bool {
|
||||
return s == Completed || s == Scrapped
|
||||
}
|
||||
|
||||
// Parse normalizes user input. Accepts the canonical hyphenated form, plus
|
||||
// underscored/snake variants and a couple of natural-language synonyms.
|
||||
func Parse(raw string) (Status, error) {
|
||||
n := strings.TrimSpace(strings.ToLower(raw))
|
||||
n = strings.ReplaceAll(n, "_", "-")
|
||||
n = strings.ReplaceAll(n, " ", "-")
|
||||
switch n {
|
||||
case "todo":
|
||||
return Todo, nil
|
||||
case "in-progress", "wip", "doing":
|
||||
return InProgress, nil
|
||||
case "in-review", "review":
|
||||
return InReview, nil
|
||||
case "completed", "done":
|
||||
return Completed, nil
|
||||
case "scrapped", "cancelled", "canceled":
|
||||
return Scrapped, nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown status %q (expected one of: %s)",
|
||||
raw, strings.Join(allStrings(), ", "))
|
||||
}
|
||||
|
||||
// BucketID resolves a status to the bucket ID stored in .veans.yml.
|
||||
func BucketID(s Status, b config.Buckets) (int64, error) {
|
||||
switch s {
|
||||
case Todo:
|
||||
return b.Todo, nil
|
||||
case InProgress:
|
||||
return b.InProgress, nil
|
||||
case InReview:
|
||||
return b.InReview, nil
|
||||
case Completed:
|
||||
return b.Done, nil
|
||||
case Scrapped:
|
||||
return b.Scrapped, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unknown status %q", s)
|
||||
}
|
||||
|
||||
// FromBucketID is the inverse of BucketID — used by `list` to render the
|
||||
// status of a task fetched from the API.
|
||||
func FromBucketID(id int64, b config.Buckets) Status {
|
||||
switch id {
|
||||
case b.Todo:
|
||||
return Todo
|
||||
case b.InProgress:
|
||||
return InProgress
|
||||
case b.InReview:
|
||||
return InReview
|
||||
case b.Done:
|
||||
return Completed
|
||||
case b.Scrapped:
|
||||
return Scrapped
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func allStrings() []string {
|
||||
out := make([]string, 0, 5)
|
||||
for _, s := range All() {
|
||||
out = append(out, string(s))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/veans/internal/config"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
cases := map[string]Status{
|
||||
"todo": Todo,
|
||||
"TODO": Todo,
|
||||
"in-progress": InProgress,
|
||||
"in_progress": InProgress,
|
||||
"in progress": InProgress,
|
||||
"WIP": InProgress,
|
||||
"doing": InProgress,
|
||||
"in-review": InReview,
|
||||
"review": InReview,
|
||||
"completed": Completed,
|
||||
"done": Completed,
|
||||
"scrapped": Scrapped,
|
||||
"cancelled": Scrapped,
|
||||
"canceled": Scrapped,
|
||||
}
|
||||
for in, want := range cases {
|
||||
got, err := Parse(in)
|
||||
if err != nil {
|
||||
t.Errorf("Parse(%q): %v", in, err)
|
||||
continue
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("Parse(%q): got %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
if _, err := Parse("nope"); err == nil {
|
||||
t.Errorf("Parse(\"nope\"): expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoneFlag(t *testing.T) {
|
||||
if !Completed.Done() || !Scrapped.Done() {
|
||||
t.Fatal("Completed/Scrapped should be done")
|
||||
}
|
||||
if Todo.Done() || InProgress.Done() || InReview.Done() {
|
||||
t.Fatal("Todo/InProgress/InReview should not be done")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucketIDRoundTrip(t *testing.T) {
|
||||
b := config.Buckets{Todo: 11, InProgress: 12, InReview: 13, Done: 14, Scrapped: 15}
|
||||
for _, s := range All() {
|
||||
id, err := BucketID(s, b)
|
||||
if err != nil {
|
||||
t.Fatalf("BucketID(%q): %v", s, err)
|
||||
}
|
||||
if got := FromBucketID(id, b); got != s {
|
||||
t.Errorf("FromBucketID(%d) = %q, want %q", id, got, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue