Files
Maven/internal/tasks/stall_test.go
claude 85456d3833 tasks: count overdue by calendar day like the ranker does (V-581)
Stalls compared the due instant to now while Rank compares whole calendar days, so a task due at 09:00 was counted overdue from 09:01 while its own row on the same page still read the reason as today. Stalls now reads dayDelta.

A row with no capture time also counted as sitting, because the zero time is January of year 1 and every span from it clears ten days. Rank already guarded that and Stalls did not.

Folded the open-versus-candidate partition FormatRU and Spoken each carried into one split helper. The two have to agree on where that line falls.
2026-08-06 03:12:28 +04:00

90 lines
3.4 KiB
Go

package tasks
import (
"strings"
"testing"
"time"
)
func stallNow() time.Time { return time.Date(2026, 8, 5, 12, 0, 0, 0, time.UTC) }
func TestStallsCountsTheThreeShapes(t *testing.T) {
now := stallNow()
day := 24 * time.Hour
yesterday := now.Add(-day)
items := []Item{
// Overdue and sitting at once: it counts in both, because they are two
// different things wrong with one task.
{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Created: now.Add(-20 * day), Due: &yesterday},
{ID: 2, Text: "купить молоко", Status: StatusOpen, Created: now.Add(-12 * day)},
{ID: 3, Text: "позвонить маме", Status: StatusOpen, Created: now.Add(-time.Hour)},
{ID: 4, Text: "продлить домен", Status: StatusCandidate, Created: now.Add(-30 * day), Due: &yesterday},
}
got := Stalls(items, now)
if len(got) != 3 {
t.Fatalf("shapes = %+v, want overdue, sitting, unconfirmed", got)
}
if got[0].N != 1 {
t.Errorf("overdue = %d, want 1 — a candidate's due date is Maven's reading of a mail", got[0].N)
}
if got[1].N != 2 {
t.Errorf("sitting = %d, want 2", got[1].N)
}
if got[2].N != 1 {
t.Errorf("unconfirmed = %d, want 1", got[2].N)
}
line := StallsRU(got)
for _, want := range []string{"просрочено", "лежит", "подтверждения"} {
if !strings.Contains(line, want) {
t.Errorf("StallsRU = %q, want it to mention %q", line, want)
}
}
}
func TestStallsSaysNothingWhenThereIsNothing(t *testing.T) {
now := stallNow()
items := []Item{{ID: 1, Text: "купить молоко", Status: StatusOpen, Created: now.Add(-time.Hour)}}
if got := Stalls(items, now); len(got) != 0 {
t.Fatalf("shapes = %+v, want none", got)
}
// "ничего не залежалось" appended to every list read is a nag with a
// friendly face, so the empty case renders as nothing at all.
if got := StallsRU(nil); got != "" {
t.Errorf("StallsRU(nil) = %q, want empty", got)
}
}
func TestStallsCountsOverdueByCalendarDay(t *testing.T) {
// Same reading as Rank, or the count above the table contradicts the reason
// in the row: due at 09:00, asked at 12:00, and it is still due today.
now := stallNow()
earlier := now.Add(-3 * time.Hour)
items := []Item{{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Created: now.Add(-time.Hour), Due: &earlier}}
if got := Stalls(items, now); len(got) != 0 {
t.Fatalf("shapes = %+v, want none — a task due today is not overdue", got)
}
}
func TestStallsIgnoresATaskWithNoCaptureTime(t *testing.T) {
// The zero time is January of year 1, so an unstamped row would count as
// sitting forever.
now := stallNow()
items := []Item{{ID: 1, Text: "купить молоко", Status: StatusOpen}}
if got := Stalls(items, now); len(got) != 0 {
t.Fatalf("shapes = %+v, want none", got)
}
}
func TestStallsCountsNoJudgement(t *testing.T) {
// The line this shape may not cross. Every sentence states a count; none of
// them says whether the work matters or should be dropped.
now := stallNow()
items := []Item{{ID: 1, Text: "оплатить интернет", Status: StatusOpen, Created: now.Add(-40 * 24 * time.Hour)}}
line := StallsRU(Stalls(items, now))
for _, banned := range []string{"стоит", "лучше", "надо", "брось", "важно"} {
if strings.Contains(line, banned) {
t.Errorf("StallsRU = %q — %q is an assessment, and counting is the whole licence here", line, banned)
}
}
}