1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
package grid
import (
"testing"
"ticktock/internal/activity"
"ticktock/internal/store"
)
func TestPaintLoggedLabelAndContinuation(t *testing.T) {
g := BuildGrid(30, 7*60, 9*60, nil) // FirstMin 420, Rows 4 (07:00..09:00)
es := []store.Entry{{
Key: "2026-07-07-01", Start: at(7, 0), End: at(8, 30),
Project: "ARCHER", Description: "work", Duration: 90 * 60 * 1e9,
}}
cells := paintLogged(g, es)
if cells[0].Kind != CellLabel {
t.Fatalf("row0 kind=%v, want Label", cells[0].Kind)
}
if cells[0].Project != "ARCHER" || cells[1].Project != "ARCHER" {
t.Errorf("project not carried onto cells: %q / %q", cells[0].Project, cells[1].Project)
}
if cells[1].Kind != CellCont || cells[2].Kind != CellCont {
t.Errorf("rows1-2 should be continuation")
}
if cells[3].Kind != CellEmpty {
t.Errorf("row3 should be empty (session ends 08:30)")
}
if cells[0].Key != "2026-07-07-01" || cells[2].Key != "2026-07-07-01" {
t.Errorf("key not propagated to covered slots")
}
}
func TestEntryKeyAtRow(t *testing.T) {
g := BuildGrid(30, 7*60, 9*60, nil)
es := []store.Entry{{Key: "K", Start: at(7, 30), End: at(8, 0), Description: "x"}}
cells := paintLogged(g, es)
if k, ok := entryKeyAtRow(cells, 1); !ok || k != "K" {
t.Errorf("row1 → %q,%v want K,true", k, ok)
}
if _, ok := entryKeyAtRow(cells, 0); ok {
t.Errorf("row0 empty should be false")
}
}
func TestLabelForFormatsDurationAndTags(t *testing.T) {
e := store.Entry{Description: "emails", Tags: []string{"admin"}, Duration: 50 * 60 * 1e9}
got := labelFor(e)
if got != "✓ emails [admin] (0:50)" {
t.Errorf("labelFor=%q", got)
}
}
func TestPaintActivityLabelsAndContinuation(t *testing.T) {
g := BuildGrid(30, 7*60, 10*60, nil) // rows 07:00..10:00 = 6
segs := []activity.Segment{
{Start: at(7, 0), End: at(8, 0), App: "Safari", Title: "GitHub", Domain: "github.com"},
{Start: at(8, 0), End: at(8, 30), App: "Trace"}, // empty title+domain → app fallback
{Start: at(8, 30), End: at(9, 30), Idle: true},
}
cells := paintActivity(g, segs, nil)
if cells[0].Kind != CellLabel || cells[0].Text != "github.com" || cells[0].Act != ActActive {
t.Errorf("row0 = %+v, want github.com active label", cells[0])
}
if cells[1].Kind != CellCont || cells[1].Act != ActActive {
t.Errorf("row1 = %+v, want active continuation", cells[1])
}
if cells[2].Text != "Trace" {
t.Errorf("row2 = %+v, want app fallback label", cells[2])
}
if cells[3].Kind != CellLabel || cells[3].Text != "(idle)" || cells[3].Act != ActIdle {
t.Errorf("row3 = %+v, want (idle) label", cells[3])
}
if cells[4].Kind != CellCont || cells[4].Act != ActIdle {
t.Errorf("row4 = %+v, want idle continuation", cells[4])
}
if cells[5].Kind != CellEmpty {
t.Errorf("row5 = %+v, want empty gap", cells[5])
}
}
func TestPaintActivityTitleWhenNoDomain(t *testing.T) {
g := BuildGrid(30, 7*60, 8*60, nil)
segs := []activity.Segment{{Start: at(7, 0), End: at(7, 30), App: "Trace", Title: "notes.md"}}
cells := paintActivity(g, segs, nil)
if cells[0].Text != "notes.md" {
t.Errorf("row0 = %+v, want title label", cells[0])
}
}
func TestPaintActivityOverlapPrecedence(t *testing.T) {
g := BuildGrid(30, 9*60, 11*60, nil) // rows 09:00,09:30,10:00,10:30
segs := []activity.Segment{
{Start: at(9, 30), End: at(10, 0), App: "Safari", Domain: "github.com"},
{Start: at(10, 0), End: at(10, 30), Idle: true},
}
events := []activity.Event{
{Start: at(9, 0), End: at(11, 0), Title: "Sprint review", Calendar: "Archer"},
}
cells := paintActivity(g, segs, events)
// row0: calendar label — the human-meaningful anchor always wins
if cells[0].Kind != CellLabel || cells[0].Text != "▪ Sprint review" || cells[0].Act != ActCalendar {
t.Errorf("row0 = %+v, want calendar label", cells[0])
}
// row1: active segment beats the calendar continuation
if cells[1].Act != ActActive || cells[1].Text != "github.com" {
t.Errorf("row1 = %+v, want active over cal-cont", cells[1])
}
// row2: idle beats the calendar continuation
if cells[2].Act != ActIdle {
t.Errorf("row2 = %+v, want idle over cal-cont", cells[2])
}
// row3: nothing else there — calendar continuation fills the slot
if cells[3].Kind != CellCont || cells[3].Act != ActCalendar {
t.Errorf("row3 = %+v, want calendar continuation", cells[3])
}
}
func TestPaintActivityCalendarLabelBeatsActive(t *testing.T) {
g := BuildGrid(30, 9*60, 10*60, nil)
segs := []activity.Segment{{Start: at(9, 0), End: at(10, 0), App: "Safari", Domain: "github.com"}}
events := []activity.Event{{Start: at(9, 0), End: at(9, 30), Title: "Standup", Calendar: "Archer"}}
cells := paintActivity(g, segs, events)
if cells[0].Text != "▪ Standup" || cells[0].Act != ActCalendar {
t.Errorf("row0 = %+v, calendar label must beat active", cells[0])
}
if cells[1].Act != ActActive {
t.Errorf("row1 = %+v, active continues past the event", cells[1])
}
}
|