aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests/task-review-staleness.bats
blob: 79aad79402f334f9eb8cb41b4939ded5aefbb662 (plain)
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
#!/usr/bin/env bats
#
# Tests for claude-templates/.ai/scripts/task-review-staleness.sh —
# counts top-level todo.org tasks whose review has gone stale.
#
# Strategy: write a synthetic todo.org into a temp dir per test, with
# LAST_REVIEWED dates generated relative to the real `date` (never
# hardcoded). Run the real script against it and assert the count it
# prints on stdout.
#
# Staleness rule under test:
#   - A qualifying task is a depth-2 (**) heading with a TODO/DOING/VERIFY
#     keyword and an [#A]/[#B]/[#C] priority cookie.
#   - It is stale when LAST_REVIEWED is missing/malformed (NIL → oldest),
#     or when its age strictly exceeds the threshold (age > N days).
#   - age == N exactly is fresh (the spec's wording is ">N days").

# The script under test is always the sibling-of-parent of this test file
# (scripts/task-review-staleness.sh next to scripts/tests/). This holds in
# both the canonical claude-templates/ tree and the rsync'd project mirror,
# so the suite runs identically from either location.
SCRIPT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/task-review-staleness.sh"

setup() {
  TEST_DIR="$(mktemp -d -t task-review-bats.XXXXXX)"
  TODO="$TEST_DIR/todo.org"

  TODAY="$(date +%F)"
  D5="$(date -d '5 days ago' +%F)"
  D30="$(date -d '30 days ago' +%F)"
  D31="$(date -d '31 days ago' +%F)"
  D40="$(date -d '40 days ago' +%F)"
}

teardown() {
  rm -rf "$TEST_DIR"
}

# Emit a qualifying task with an explicit LAST_REVIEWED date.
task_reviewed() {
  local keyword="$1" prio="$2" title="$3" date="$4"
  printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \
    "$keyword" "$prio" "$title" "$date" >> "$TODO"
}

# Emit a qualifying task with no PROPERTIES drawer at all.
task_unreviewed() {
  local keyword="$1" prio="$2" title="$3"
  printf '** %s [#%s] %s\nBody.\n\n' "$keyword" "$prio" "$title" >> "$TODO"
}

# Emit a qualifying task whose LAST_REVIEWED is an org-native inactive
# timestamp — [YYYY-MM-DD Day] — matching the CREATED:/CLOSED: cookies that
# sit in the same drawer. The date is derived from an ISO date via `date`.
task_reviewed_org() {
  local keyword="$1" prio="$2" title="$3" isodate="$4"
  local org="[$(date -d "$isodate" '+%F %a')]"
  printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \
    "$keyword" "$prio" "$title" "$org" >> "$TODO"
}

# ---- Normal cases ----------------------------------------------------

@test "staleness: empty file reports zero" {
  : > "$TODO"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: all tasks fresh reports zero" {
  task_reviewed TODO A "Fresh one" "$D5"
  task_reviewed TODO B "Fresh two" "$D5"
  task_reviewed DOING A "Fresh three" "$TODAY"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: all tasks stale reports full count" {
  task_reviewed TODO A "Stale one" "$D40"
  task_reviewed TODO B "Stale two" "$D40"
  task_reviewed VERIFY C "Stale three" "$D40"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "3" ]
}

@test "staleness: mixed fresh, stale, and unreviewed counts only the latter two" {
  task_reviewed TODO A "Fresh" "$D5"
  task_reviewed TODO B "Stale" "$D40"
  task_unreviewed DOING A "Never reviewed"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "2" ]
}

@test "staleness: org-native bracketed LAST_REVIEWED parses — recent is fresh" {
  task_reviewed_org TODO A "Reviewed five days ago, org stamp" "$D5"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: org-native bracketed LAST_REVIEWED parses — old is stale" {
  task_reviewed_org TODO A "Reviewed forty days ago, org stamp" "$D40"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "1" ]
}

# ---- Boundary cases --------------------------------------------------

@test "staleness: age exactly equal to threshold is fresh" {
  task_reviewed TODO A "Exactly at cutoff" "$D30"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: age one day past threshold is stale" {
  task_reviewed TODO A "One day over" "$D31"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "1" ]
}

@test "staleness: unreviewed task (no drawer) counts as stale" {
  task_unreviewed TODO A "Never reviewed"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "1" ]
}

@test "staleness: threshold of 7 is softer than 30 on the same list" {
  task_reviewed TODO A "Reviewed five days ago" "$D5"
  task_reviewed TODO B "Reviewed thirty-one days ago" "$D31"
  run bash "$SCRIPT" "$TODO" 7
  [ "$status" -eq 0 ]
  [ "$output" = "1" ]
}

# ---- Error / exclusion cases -----------------------------------------

@test "staleness: DONE and CANCELLED tasks are excluded even when old" {
  task_reviewed DONE A "Shipped long ago" "$D40"
  task_reviewed CANCELLED B "Abandoned long ago" "$D40"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: deeper headings and cookie-less headings are excluded" {
  # Depth-3 child with an old review date — not a review unit.
  printf '*** TODO [#A] Child task\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\n\n' "$D40" >> "$TODO"
  # Depth-2 but no priority cookie — not a review unit.
  printf '** TODO Cookie-less task\nBody.\n\n' >> "$TODO"
  run bash "$SCRIPT" "$TODO" 30
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
}

@test "staleness: malformed LAST_REVIEWED warns to stderr and is not counted" {
  task_reviewed TODO A "Bad date" "not-a-date"
  # stdout carries only the count — the malformed stamp is not folded in.
  run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null"
  [ "$status" -eq 0 ]
  [ "$output" = "0" ]
  # stderr carries the loud warning naming the offending value.
  run bash -c "bash '$SCRIPT' '$TODO' 30 2>&1 1>/dev/null"
  [ "$status" -eq 0 ]
  [[ "$output" == *"not-a-date"* ]]
  [[ "$output" == *"LAST_REVIEWED"* ]]
}

@test "staleness: malformed stamp is excluded while real stale tasks still count" {
  task_reviewed TODO A "Real stale" "$D40"
  task_reviewed TODO B "Broken stamp" "garbage"
  run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null"
  [ "$status" -eq 0 ]
  [ "$output" = "1" ]
}

@test "staleness: missing todo file exits non-zero" {
  run bash "$SCRIPT" "$TEST_DIR/does-not-exist.org" 30
  [ "$status" -ne 0 ]
}

# ---- --list mode -----------------------------------------------------

@test "staleness --list: orders oldest-reviewed first, unreviewed before dated" {
  task_reviewed TODO A "Reviewed recently" "$D5"
  task_reviewed TODO B "Reviewed long ago" "$D40"
  task_unreviewed DOING A "Never reviewed"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  [[ "${lines[0]}" == *"Never reviewed"* ]]
  [[ "${lines[1]}" == *"Reviewed long ago"* ]]
  [[ "${lines[2]}" == *"Reviewed recently"* ]]
}

@test "staleness --list: org-native bracketed stamp sorts by its real date" {
  task_reviewed TODO A "Bare recent" "$D5"
  task_reviewed_org TODO B "Org-stamped old" "$D40"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  # The org-bracketed old stamp must sort ahead of the bare recent one —
  # proof it parsed to a real date rather than falling to 0000-00-00.
  [[ "${lines[0]}" == *"Org-stamped old"* ]]
  [[ "${lines[1]}" == *"Bare recent"* ]]
}

@test "staleness --list: takes only the requested count" {
  task_unreviewed TODO A "First"
  task_reviewed TODO B "Second" "$D40"
  task_reviewed TODO C "Third" "$D30"
  run bash "$SCRIPT" --list "$TODO" 2
  [ "$status" -eq 0 ]
  [ "${#lines[@]}" -eq 2 ]
}

@test "staleness --list: count larger than available returns all candidates" {
  task_unreviewed TODO A "Only one"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  [ "${#lines[@]}" -eq 1 ]
}

@test "staleness --list: excludes DONE, deeper headings, and cookie-less headings" {
  task_reviewed DONE A "Shipped" "$D40"
  printf '*** TODO [#A] Child task\n\n' >> "$TODO"
  printf '** TODO Cookie-less\n\n' >> "$TODO"
  task_unreviewed TODO B "Real one"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  [ "${#lines[@]}" -eq 1 ]
  [[ "${lines[0]}" == *"Real one"* ]]
}

@test "staleness --list: emits line, review value, and heading tab-separated" {
  task_unreviewed TODO A "Never reviewed"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  line_no=$(printf '%s' "${lines[0]}" | cut -f1)
  value=$(printf '%s' "${lines[0]}" | cut -f2)
  heading=$(printf '%s' "${lines[0]}" | cut -f3)
  [ "$line_no" = "1" ]
  [ "$value" = "NONE" ]
  [ "$heading" = "** TODO [#A] Never reviewed" ]
}

@test "staleness --list: empty file produces no output" {
  : > "$TODO"
  run bash "$SCRIPT" --list "$TODO" 10
  [ "$status" -eq 0 ]
  [ -z "$output" ]
}

@test "staleness --list: missing file exits non-zero" {
  run bash "$SCRIPT" --list "$TEST_DIR/nope.org" 10
  [ "$status" -ne 0 ]
}