Files
Maven/internal/tasks/stall_test.go
T
claude be62660be9 /tasks counts stall shapes, and assesses none of them (V-512)
Step 5 of the board build. internal/tasks/stall.go counts three shapes —
overdue, sitting longer than StallDays, waiting for confirmation — and states
nothing about what any of them means. That is the line
internal/memory/behavior.go already drew for habits, and the reason is the
same: a 1.7B asked to judge will agree fluently and launder a guess into a
decision. A test asserts the wording carries no assessment.

Sitting is measured from created_ts, the only clock a live row carries: the
store stamps resolved_ts and nothing else. So "no state change in eleven days"
is exactly "captured eleven days ago and still live", which is narrower than
the plan's wording and is the claim the data supports. A candidate is never
counted as overdue, because its due date is Maven's reading of a mail rather
than a deadline he set.

Not a nag. No tick rule reads the counts; they go on /tasks and into the list
reply when he asks, and tickLoop.dayPlan still does not read tasks at all. The
empty case renders as nothing: "ничего не залежалось" appended to every list
read is a nag with a friendly face.

Three say entries, so the page and the spoken list cannot word it differently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 19:54:21 +04:00

69 lines
2.5 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 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)
}
}
}