package queue import ( "os" "path/filepath" "strings" "testing" "time" ) func TestAppend_CreatesFileWithHeader(t *testing.T) { dir := t.TempDir() item := Item{ Kind: "gc-review", Title: "stale ref", Project: "demo", Detail: "fact `foo` references missing path", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC), } if err := Append(dir, item); err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(dir, Filename)) if err != nil { t.Fatal(err) } got := string(b) if !strings.HasPrefix(got, "# eeco queue\n") { t.Errorf("missing header:\n%s", got) } want := "- [ ] **gc-review** — stale ref _(demo, 2026-05-19)_\n fact `foo` references missing path\n" if !strings.Contains(got, want) { t.Errorf("missing entry, got:\n%s\n\nwant contains:\n%s", got, want) } } func TestAppend_AppendsToExisting(t *testing.T) { dir := t.TempDir() first := Item{Kind: "k", Title: "first", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)} second := Item{Kind: "k", Title: "second", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)} if err := Append(dir, first); err != nil { t.Fatal(err) } if err := Append(dir, second); err != nil { t.Fatal(err) } b, _ := os.ReadFile(filepath.Join(dir, Filename)) got := string(b) if !strings.Contains(got, "first") || !strings.Contains(got, "second") { t.Errorf("missing entries:\n%s", got) } if strings.Count(got, "# eeco queue") != 1 { t.Errorf("header duplicated:\n%s", got) } } func TestAppend_NoTrailingNewlineGetsFixed(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, Filename) if err := os.WriteFile(path, []byte("# eeco queue\n\n- [x] **k** — old _(p, 2026-05-19)_"), 0o644); err != nil { t.Fatal(err) } item := Item{Kind: "k", Title: "new", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)} if err := Append(dir, item); err != nil { t.Fatal(err) } b, _ := os.ReadFile(path) got := string(b) if !strings.Contains(got, "old _(p, 2026-05-19)_\n- [ ] **k** — new") { t.Errorf("appended entry stuck to previous line:\n%s", got) } } func TestAppend_RejectsMissingFields(t *testing.T) { dir := t.TempDir() cases := []Item{ {Title: "no kind", Project: "p"}, {Kind: "k", Project: "p"}, } for _, item := range cases { if err := Append(dir, item); err == nil { t.Errorf("Append(%+v) succeeded; expected error", item) } } } func TestAppend_CreatesStateDir(t *testing.T) { dir := filepath.Join(t.TempDir(), "deeper", "state") item := Item{Kind: "k", Title: "t", Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)} if err := Append(dir, item); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(dir, Filename)); err != nil { t.Errorf("queue.md not created: %v", err) } } func TestAppendUnique_SkipsDuplicateOpenItem(t *testing.T) { dir := t.TempDir() item := Item{Kind: "memory-drift", Title: "fact x may be stale", Project: "p", Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)} added, err := AppendUnique(dir, item) if err != nil || !added { t.Fatalf("first AppendUnique: added=%v err=%v", added, err) } // Same finding on a later day: Project/Date differ but Kind+Title match. dup := item dup.Date = time.Date(2026, 5, 23, 0, 0, 0, 0, time.UTC) added, err = AppendUnique(dir, dup) if err != nil { t.Fatalf("second AppendUnique: %v", err) } if added { t.Error("duplicate open item was appended; want skip") } b, _ := os.ReadFile(filepath.Join(dir, Filename)) if n := strings.Count(string(b), "fact x may be stale"); n != 1 { t.Errorf("item present %d times, want 1:\n%s", n, b) } } func TestAppendUnique_AppendsWhenTitleDiffers(t *testing.T) { dir := t.TempDir() base := Item{Kind: "memory-drift", Project: "p", Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)} a := base a.Title = "fact a may be stale" b := base b.Title = "fact b may be stale" if added, err := AppendUnique(dir, a); err != nil || !added { t.Fatalf("append a: added=%v err=%v", added, err) } if added, err := AppendUnique(dir, b); err != nil || !added { t.Fatalf("append b: added=%v err=%v", added, err) } content, _ := os.ReadFile(filepath.Join(dir, Filename)) if !strings.Contains(string(content), "fact a") || !strings.Contains(string(content), "fact b") { t.Errorf("both distinct items should be present:\n%s", content) } } func TestAppendUnique_ResolvedItemDoesNotBlockRefile(t *testing.T) { dir := t.TempDir() item := Item{Kind: "doc-drift", Title: "tag v1.2.3 not documented", Project: "p", Date: time.Date(2026, 5, 22, 0, 0, 0, 0, time.UTC)} if added, err := AppendUnique(dir, item); err != nil || !added { t.Fatalf("first append: added=%v err=%v", added, err) } // Operator resolves it (checks the box) but the drift persists. path := filepath.Join(dir, Filename) b, _ := os.ReadFile(path) resolved := strings.Replace(string(b), "- [ ] **doc-drift**", "- [x] **doc-drift**", 1) if err := os.WriteFile(path, []byte(resolved), 0o644); err != nil { t.Fatal(err) } added, err := AppendUnique(dir, item) if err != nil { t.Fatal(err) } if !added { t.Error("re-file after resolve was skipped; a resolved item must not block a re-file") } } func TestParseOpenRow(t *testing.T) { cases := []struct { line string wantKind, wantTitle string wantOK bool }{ {"- [ ] **memory-drift** — fact x may be stale _(p, 2026-05-22)_", "memory-drift", "fact x may be stale", true}, {" - [ ] **k** — t _(proj, 2026-05-22)_", "k", "t", true}, {"- [x] **k** — resolved _(p, 2026-05-22)_", "", "", false}, {" indented detail line", "", "", false}, {"# eeco queue", "", "", false}, {"- [ ] no bold kind _(p, 2026-05-22)_", "", "", false}, // A title that itself contains "_(" trims at the last suffix marker. {"- [ ] **k** — weird _(x)_ title _(p, 2026-05-22)_", "k", "weird _(x)_ title", true}, // No closing "**" after the kind. {"- [ ] **k — t _(p, 2026-05-22)_", "", "", false}, // No " — " separator between kind and title. {"- [ ] **k** t _(p, 2026-05-22)_", "", "", false}, // Empty kind (matched "****" gives an empty kind → rejected). {"- [ ] **** — t _(p, 2026-05-22)_", "", "", false}, // Empty title (nothing between the separator and the suffix). {"- [ ] **k** — _(p, 2026-05-22)_", "", "", false}, } for _, c := range cases { k, ti, ok := parseOpenRow(c.line) if ok != c.wantOK || k != c.wantKind || ti != c.wantTitle { t.Errorf("parseOpenRow(%q) = (%q, %q, %v), want (%q, %q, %v)", c.line, k, ti, ok, c.wantKind, c.wantTitle, c.wantOK) } } } func TestAppendUnique_RejectsMissingFields(t *testing.T) { dir := t.TempDir() if _, err := AppendUnique(dir, Item{Title: "no kind", Project: "p"}); err == nil { t.Error("AppendUnique with no kind succeeded; expected error") } } func TestCount(t *testing.T) { dir := t.TempDir() if n, err := Count(dir); err != nil || n != 0 { t.Errorf("missing queue: n=%d err=%v", n, err) } for i, kind := range []string{"a", "b", "c"} { _ = i if err := Append(dir, Item{Kind: kind, Title: kind, Project: "p", Date: time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)}); err != nil { t.Fatal(err) } } // Resolve one item manually. path := filepath.Join(dir, Filename) b, _ := os.ReadFile(path) s := strings.Replace(string(b), "- [ ] **a**", "- [x] **a**", 1) if err := os.WriteFile(path, []byte(s), 0o644); err != nil { t.Fatal(err) } n, err := Count(dir) if err != nil { t.Fatal(err) } if n != 2 { t.Errorf("Count = %d, want 2", n) } } func TestResolved(t *testing.T) { t.Run("missing file", func(t *testing.T) { got, err := Resolved(t.TempDir(), "evolve", "done") if err != nil || got { t.Fatalf("missing file → (%v, %v), want (false, nil)", got, err) } }) dir := t.TempDir() content := "# eeco queue\n\n" + "- [x] **evolve** — done _(p, 2026-05-22)_\n" + "- [ ] **gc** — open one _(p, 2026-05-22)_\n" if err := os.WriteFile(filepath.Join(dir, Filename), []byte(content), 0o644); err != nil { t.Fatal(err) } cases := []struct { kind, title string want bool }{ {"evolve", "done", true}, {"evolve", "wrong-title", false}, {"wrong-kind", "done", false}, {"gc", "open one", false}, // an open row is not resolved } for _, c := range cases { got, err := Resolved(dir, c.kind, c.title) if err != nil { t.Errorf("Resolved(%q,%q) err = %v", c.kind, c.title, err) } if got != c.want { t.Errorf("Resolved(%q,%q) = %v, want %v", c.kind, c.title, got, c.want) } } } func TestResolved_ReadErrWrapped(t *testing.T) { dir := t.TempDir() // queue.md as a directory → ReadFile returns a non-NotExist error. if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil { t.Fatal(err) } if _, err := Resolved(dir, "k", "t"); err == nil || !strings.Contains(err.Error(), "queue.Resolved:") { t.Fatalf("err = %v, want 'queue.Resolved:'", err) } } func TestValidateItem_Defaults(t *testing.T) { if err := validateItem("", &Item{Kind: "k", Title: "t"}); err == nil || !strings.Contains(err.Error(), "queue: stateDir is empty") { t.Fatalf("empty stateDir err = %v, want 'queue: stateDir is empty'", err) } item := &Item{Kind: "k", Title: "t"} // zero Date if err := validateItem("somedir", item); err != nil { t.Fatalf("validateItem: %v", err) } if item.Date.IsZero() { t.Error("zero Date was not defaulted to now") } } func TestReadQueue_NonNotExistErrWrapped(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil { t.Fatal(err) } if _, err := readQueue(dir); err == nil || !strings.Contains(err.Error(), "queue: read:") { t.Fatalf("err = %v, want 'queue: read:'", err) } } func TestCount_ReadErrWrapped(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, Filename), 0o755); err != nil { t.Fatal(err) } if _, err := Count(dir); err == nil || !strings.Contains(err.Error(), "queue.Count:") { t.Fatalf("err = %v, want 'queue.Count:'", err) } }