-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathv2_test.go
More file actions
184 lines (167 loc) · 6.68 KB
/
Copy pathv2_test.go
File metadata and controls
184 lines (167 loc) · 6.68 KB
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package main
import (
"fmt"
"testing"
"time"
)
var t0 = time.Date(2026, 7, 12, 9, 0, 0, 0, time.Local)
// gen emits n samples 5s apart starting at start; titleFn varies the title.
func gen(start time.Time, app string, n int, titleFn func(i int) string) []Sample {
out := make([]Sample, n)
for i := range out {
out[i] = Sample{TS: start.Add(time.Duration(i) * 5 * time.Second), App: app, Title: titleFn(i)}
}
return out
}
func fixed(title string) func(int) string { return func(int) string { return title } }
func after(samples []Sample) time.Time {
return samples[len(samples)-1].TS.Add(5 * time.Second)
}
func TestRulePrecedence(t *testing.T) {
rules := defaultRules()
cases := []struct {
app, title string
want Category
}{
{"chrome", "cat videos - YouTube", Distracting}, // title beats app
{"chrome", "lacheisis - GitHub", Productive},
{"chrome", "weather forecast", Neutral}, // unmatched
{"code", "main.go - lacheisis", Productive},
{"code", "watching YouTube in an editor tab", Distracting}, // title pass runs first
}
for _, c := range cases {
got := classifySample(Sample{App: c.app, Title: c.title}, rules)
if got != c.want {
t.Errorf("classify(%s, %q) = %s, want %s", c.app, c.title, got, c.want)
}
}
}
func TestGlanceTolerance(t *testing.T) {
rules := defaultRules()
work := gen(t0, "code", 100, fixed("main.go"))
glance := gen(after(work), "chrome", 2, fixed("docs"))
more := gen(after(glance), "code", 100, fixed("main.go"))
m := sessionize(append(append(work, glance...), more...), 5, rules)
if len(m.Blocks) != 1 {
t.Fatalf("2-sample glance should not break the block, got %d blocks", len(m.Blocks))
}
if m.Switches != 2 {
t.Errorf("glance still counts as switches, got %d want 2", m.Switches)
}
interrupt := gen(after(work), "chrome", 3, fixed("docs"))
more = gen(after(interrupt), "code", 100, fixed("main.go"))
m = sessionize(append(append(work, interrupt...), more...), 5, rules)
if len(m.Blocks) != 3 {
t.Errorf("3-sample interruption should break the block, got %d blocks", len(m.Blocks))
}
}
func TestIdleAndGapBreakBlocks(t *testing.T) {
rules := defaultRules()
a := gen(t0, "code", 50, fixed("main.go"))
idle := []Sample{{TS: after(a), Idle: true}}
b := gen(after(a).Add(5*time.Second), "code", 50, fixed("main.go"))
m := sessionize(append(append(a, idle...), b...), 5, rules)
if len(m.Blocks) != 2 {
t.Errorf("idle sample must break blocks, got %d", len(m.Blocks))
}
// 10s gap (2 missed samples) tolerated, 20s gap breaks
c := gen(after(a).Add(10*time.Second), "code", 50, fixed("main.go"))
m = sessionize(append(a, c...), 5, rules)
if len(m.Blocks) != 1 {
t.Errorf("10s gap should not break block, got %d", len(m.Blocks))
}
d := gen(after(a).Add(20*time.Second), "code", 50, fixed("main.go"))
m = sessionize(append(a, d...), 5, rules)
if len(m.Blocks) != 2 {
t.Errorf("20s gap must break block, got %d", len(m.Blocks))
}
}
func TestEntropy(t *testing.T) {
rules := defaultRules()
loop := sessionize(gen(t0, "code", 400, fixed("same task")), 5, rules)
varied := sessionize(gen(t0, "code", 400, func(i int) string {
return fmt.Sprintf("file%d.go - lacheisis", i/20)
}), 5, rules)
if e := loop.Blocks[0].Entropy(); e >= lowEntropyThreshold {
t.Errorf("looping block entropy %.3f should be below %.2f", e, lowEntropyThreshold)
}
if e := varied.Blocks[0].Entropy(); e < lowEntropyThreshold {
t.Errorf("varied block entropy %.3f should be above %.2f", e, lowEntropyThreshold)
}
}
func TestCommitAttribution(t *testing.T) {
rules := defaultRules()
work := gen(t0, "code", 400, fixed("main.go")) // 33m20s deep block
m := sessionize(work, 5, rules)
if len(m.Blocks) != 1 || !m.Blocks[0].Deep() {
t.Fatalf("expected one deep block")
}
end := m.Blocks[0].End
commits := []Commit{
{Hash: "a", TS: t0.Add(10 * time.Minute)}, // inside
{Hash: "b", TS: end.Add(10 * time.Minute)}, // within 30m after
{Hash: "c", TS: end.Add(40 * time.Minute)}, // too late — orphan
{Hash: "d", TS: t0.Add(-10 * time.Minute)}, // before — orphan
}
orphans := attributeCommits(m.Blocks, commits)
if len(m.Blocks[0].Commits) != 2 {
t.Errorf("block should get commits a,b — got %d", len(m.Blocks[0].Commits))
}
if len(orphans) != 2 {
t.Errorf("expected 2 orphans, got %d", len(orphans))
}
if yieldRatio(m.Blocks) != 1 {
t.Errorf("single linked deep block should yield 1, got %.2f", yieldRatio(m.Blocks))
}
}
// TestFarmerVsDeepWorker is the anti-gaming acceptance test: the farmer sits
// 6 uniform hours in one app with one looping title and no commits; the deep
// worker does less total time across three evolving blocks with commits. The
// farmer must score materially lower despite more "productive" hours.
func TestFarmerVsDeepWorker(t *testing.T) {
rules := defaultRules()
w := defaultWeights()
farmer := gen(t0, "code", 6*720, fixed("kiro board")) // 6h, one title
fm := sessionize(farmer, 5, rules)
attributeCommits(fm.Blocks, nil)
farmerScore := computeScore(fm, w)
blockA := gen(t0, "code", 720, func(i int) string { return fmt.Sprintf("blocks.go:%d", i/10) }) // 1h
browse := gen(after(blockA), "chrome", 60, fixed("weather forecast")) // 5m neutral
blockB := gen(after(browse), "code", 540, func(i int) string { return fmt.Sprintf("report.go:%d", i/10) }) // 45m
pause := []Sample{{TS: after(blockB), Idle: true}}
blockC := gen(after(blockB).Add(10*time.Minute), "windowsterminal", 360, func(i int) string {
return fmt.Sprintf("go test run %d", i/10)
}) // 30m
deep := append(append(append(append(blockA, browse...), blockB...), pause...), blockC...)
dm := sessionize(deep, 5, rules)
dmA, dmB := dm.Blocks[0], dm.Blocks[2]
attributeCommits(dm.Blocks, []Commit{
{Hash: "a", TS: dmA.End.Add(2 * time.Minute), Files: 3},
{Hash: "b", TS: dmB.Start.Add(20 * time.Minute), Files: 5},
})
deepScore := computeScore(dm, w)
if farmerScore.Total >= deepScore.Total-10 {
t.Errorf("farmer (%d) must score materially below deep worker (%d)",
farmerScore.Total, deepScore.Total)
}
if len(farmerWarnings(fm.Blocks)) == 0 {
t.Error("farmer's looping block must carry a low-entropy warning")
}
if len(farmerWarnings(dm.Blocks)) != 0 {
t.Error("deep worker should carry no low-entropy warnings")
}
}
func TestScoreBounds(t *testing.T) {
w := defaultWeights()
if s := computeScore(DayMetrics{}, w); s.Total != 0 {
t.Errorf("empty day should score 0, got %d", s.Total)
}
perfect := DayMetrics{
Active: 4 * time.Hour, Productive: 4 * time.Hour, DeepTotal: 4 * time.Hour,
Blocks: []Block{{App: "code", Category: Productive, Dur: 4 * time.Hour,
Commits: []Commit{{Hash: "x"}}}},
}
if s := computeScore(perfect, w); s.Total > 100 || s.Total < 99 {
t.Errorf("perfect day should score ~100, got %d", s.Total)
}
}