@thebassclef/lite 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +51 -7
- package/dist/cli.js +51 -7
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lite/.claude/hooks/session-reflection.d/00-bassclef-hook-connect.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/05-active-bet.sh +113 -0
- package/dist/lite/.claude/hooks/session-reflection.d/08-settings-drift.sh +62 -0
- package/dist/lite/.claude/hooks/session-reflection.d/10-abrupt-stop-recovery.sh +78 -0
- package/dist/lite/.claude/hooks/session-reflection.d/12-session-lock.sh +63 -0
- package/dist/lite/.claude/hooks/session-reflection.d/15-substrate-config-validate.sh +91 -0
- package/dist/lite/.claude/hooks/session-reflection.d/20-artifact-staleness.sh +98 -0
- package/dist/lite/.claude/hooks/session-reflection.d/30-metrics-staleness.sh +61 -0
- package/dist/lite/.claude/hooks/session-reflection.d/35-post-merge-whereami-self-heal.sh +180 -0
- package/dist/lite/.claude/hooks/session-reflection.d/36-whereami-size-check.sh +117 -0
- package/dist/lite/.claude/hooks/session-reflection.d/40-gate-compliance.sh +108 -0
- package/dist/lite/.claude/hooks/session-reflection.d/45-auto-sync-status.sh +99 -0
- package/dist/lite/.claude/hooks/session-reflection.d/50-evolution-issues.sh +44 -0
- package/dist/lite/.claude/hooks/session-reflection.d/55-orientation-gate.sh +187 -0
- package/dist/lite/.claude/hooks/session-reflection.d/56-roadmap-reconcile-gate.sh +88 -0
- package/dist/lite/.claude/hooks/session-reflection.d/60-deferred-actions.sh +212 -0
- package/dist/lite/.claude/hooks/session-reflection.d/70-recent-strategic-artifacts.sh +101 -0
- package/dist/lite/.claude/hooks/session-reflection.d/75-prior-r8-findings.sh +100 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-hook-heartbeat-check.sh +87 -0
- package/dist/lite/.claude/hooks/session-reflection.d/80-workflow-staleness.sh +141 -0
- package/dist/lite/.claude/hooks/session-reflection.d/81-hook-manifest-staleness.sh +67 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-release-backlog.sh +140 -0
- package/dist/lite/.claude/hooks/session-reflection.d/90-secrets-expiry.sh +104 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-settings-hook-verify.sh +131 -0
- package/dist/lite/.claude/hooks/session-reflection.d/95-telemetry-emit.sh +66 -0
- package/dist/lite/.claude/hooks/session-reflection.d/97-install-class-dispatch.sh +57 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/00-bassclef-hook-connect.test.sh +492 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/05-active-bet.test.sh +336 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/12-session-lock.test.sh +216 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/15-substrate-config-validate.test.sh +50 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh +348 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh +189 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/45-auto-sync-status.test.sh +263 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/55-orientation-gate.test.sh +595 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/56-roadmap-reconcile-gate.test.sh +227 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/75-prior-r8-findings.test.sh +142 -0
- package/dist/lite/.claude/hooks/session-reflection.d/tests/90-release-backlog.test.sh +44 -0
- package/dist/lite/.claude/hooks/trace-helper.sh +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# Tests for .claude/hooks/session-reflection.d/20-artifact-staleness.sh
|
|
4
|
+
#
|
|
5
|
+
# Per bassclef-upstream#1438: cure chronicle-freshness false-positive on
|
|
6
|
+
# cross-day marathon sessions. Marathon sessions land amendments on the
|
|
7
|
+
# prior-day chronicle instead of creating a new same-day file. Current
|
|
8
|
+
# hook (L9) reads filename date only; amendment headers are invisible.
|
|
9
|
+
#
|
|
10
|
+
# Path A cure: extend hook to also scan chronicle content for latest
|
|
11
|
+
# `^## Amendment YYYY-MM-DD` header. LAST_CHRONICLE_DATE becomes
|
|
12
|
+
# MAX(filename-date, latest-amendment-date). TODAY_CHRONICLE check
|
|
13
|
+
# extends to: file has today filename OR contains `## Amendment $TODAY`
|
|
14
|
+
# header.
|
|
15
|
+
#
|
|
16
|
+
# Tier 0 strict-TDD per .claude/rules/testing-tier-config.md.
|
|
17
|
+
# Test list per .claude/rules/test-list-discipline.md.
|
|
18
|
+
# Sufficiency criteria per .claude/rules/test-sufficiency.md.
|
|
19
|
+
#
|
|
20
|
+
# Run: bash .claude/hooks/session-reflection.d/tests/20-artifact-staleness.test.sh
|
|
21
|
+
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
# test-list:
|
|
25
|
+
# Class A — fresh chronicle (should be SILENT):
|
|
26
|
+
# [x] Same-day filename chronicle exists → silent (original behavior preserved)
|
|
27
|
+
# [x] Prior-day filename with today amendment header → silent (Path A cure)
|
|
28
|
+
# [x] Prior-day filename with multiple amendments including today → silent (Path A cure)
|
|
29
|
+
# [x] Same-day filename AND prior chronicle with amendment → silent
|
|
30
|
+
# Class B — stale chronicle (should BLOCK):
|
|
31
|
+
# [x] Prior-day filename + NO amendment headers + >=3 commits → BLOCK (original class preserved)
|
|
32
|
+
# [x] Prior-day filename + amendment for OLD date (not today) + >=3 commits → BLOCK (cure does not over-mask)
|
|
33
|
+
# [x] No chronicle files + >=3 commits → silent (current hook is silent here; do NOT change)
|
|
34
|
+
# Class C — edge cases:
|
|
35
|
+
# [x] Chronicle exists + <3 commits since → silent (threshold behavior preserved)
|
|
36
|
+
# [x] Chronicle dir does not exist → silent (defensive)
|
|
37
|
+
# [x] Amendment header with malformed date (BADDATE) → falls back to filename (defensive)
|
|
38
|
+
# [x] Amendment header with future date → treated as fresh (defensive; no crash)
|
|
39
|
+
# [x] Multiple amendment lines; latest wins by date-sort not file-order
|
|
40
|
+
# [x] Trap: no tmpfiles created in hook; no trap needed. Reason: hook is read-only on filesystem.
|
|
41
|
+
# [x] Override: no SKIP_ARTIFACT_STALENESS override today; test asserts absence. Reason: existing hook has no override; extension does not add one.
|
|
42
|
+
|
|
43
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
|
|
44
|
+
HOOK="${REPO_ROOT}/.claude/hooks/session-reflection.d/20-artifact-staleness.sh"
|
|
45
|
+
PASS=0
|
|
46
|
+
FAIL=0
|
|
47
|
+
FAILED_TESTS=()
|
|
48
|
+
|
|
49
|
+
assert() {
|
|
50
|
+
local name="$1"
|
|
51
|
+
local result="$2"
|
|
52
|
+
if [ "$result" = "ok" ]; then
|
|
53
|
+
PASS=$((PASS + 1))
|
|
54
|
+
echo " PASS $name"
|
|
55
|
+
else
|
|
56
|
+
FAIL=$((FAIL + 1))
|
|
57
|
+
FAILED_TESTS+=("$name")
|
|
58
|
+
echo " FAIL $name — $result"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setup_tmpdir() {
|
|
63
|
+
TMPDIR_TEST="$(mktemp -d)"
|
|
64
|
+
mkdir -p "$TMPDIR_TEST/chronicle"
|
|
65
|
+
# Backdate init commit to 2020-01-01 so it never falls inside test
|
|
66
|
+
# `--since` windows (tests use 2026-* dates). Otherwise init counts
|
|
67
|
+
# as a spurious "commit since chronicle" and breaks threshold tests.
|
|
68
|
+
(cd "$TMPDIR_TEST" \
|
|
69
|
+
&& git init -q \
|
|
70
|
+
&& git config user.email t@t \
|
|
71
|
+
&& git config user.name t \
|
|
72
|
+
&& touch README.md \
|
|
73
|
+
&& git add README.md \
|
|
74
|
+
&& GIT_AUTHOR_DATE="2020-01-01 12:00:00" \
|
|
75
|
+
GIT_COMMITTER_DATE="2020-01-01 12:00:00" \
|
|
76
|
+
git commit -q -m init 2>/dev/null) || true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
teardown_tmpdir() {
|
|
80
|
+
rm -rf "$TMPDIR_TEST"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Add N extra commits backdated so `git log --since=<date>` counts them.
|
|
84
|
+
add_commits_since() {
|
|
85
|
+
local base_date="$1"
|
|
86
|
+
local count="$2"
|
|
87
|
+
local i=0
|
|
88
|
+
while [ "$i" -lt "$count" ]; do
|
|
89
|
+
i=$((i + 1))
|
|
90
|
+
(cd "$TMPDIR_TEST" \
|
|
91
|
+
&& echo "content-$i-$RANDOM" > "file-$i.txt" \
|
|
92
|
+
&& git add "file-$i.txt" \
|
|
93
|
+
&& GIT_AUTHOR_DATE="$base_date 12:00:00" \
|
|
94
|
+
GIT_COMMITTER_DATE="$base_date 12:00:00" \
|
|
95
|
+
git commit -q -m "commit-$i" 2>/dev/null) || true
|
|
96
|
+
done
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Invoke hook via source with blocked_banner stubbed. Inject a `date`
|
|
100
|
+
# stub so TODAY is controllable per test.
|
|
101
|
+
run_hook() {
|
|
102
|
+
local today_override="${1:-}"
|
|
103
|
+
if [ -n "$today_override" ]; then
|
|
104
|
+
cat > "$TMPDIR_TEST/date" <<EOF
|
|
105
|
+
#!/usr/bin/env bash
|
|
106
|
+
if [ "\$1" = "+%Y-%m-%d" ]; then
|
|
107
|
+
echo "$today_override"
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
110
|
+
exec /bin/date "\$@"
|
|
111
|
+
EOF
|
|
112
|
+
chmod +x "$TMPDIR_TEST/date"
|
|
113
|
+
PATH_PREFIX="$TMPDIR_TEST:$PATH"
|
|
114
|
+
else
|
|
115
|
+
PATH_PREFIX="$PATH"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
(
|
|
119
|
+
cd "$TMPDIR_TEST" || return 1
|
|
120
|
+
export PATH="$PATH_PREFIX"
|
|
121
|
+
bash -c "blocked_banner() { echo \"🛑 BLOCKED: \$1\"; }; source '$HOOK'" 2>&1
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# =======================================================================
|
|
126
|
+
# Class A — fresh chronicle (should be SILENT)
|
|
127
|
+
# =======================================================================
|
|
128
|
+
|
|
129
|
+
echo ""
|
|
130
|
+
echo "Class A — fresh chronicle (should be SILENT)"
|
|
131
|
+
echo ""
|
|
132
|
+
|
|
133
|
+
# A1: Same-day filename chronicle → silent
|
|
134
|
+
setup_tmpdir
|
|
135
|
+
touch "$TMPDIR_TEST/chronicle/2026-08-30-todays-work.md"
|
|
136
|
+
add_commits_since "2026-08-30" 5
|
|
137
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
138
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
139
|
+
assert "A1: same-day filename chronicle → silent" "output=$OUTPUT"
|
|
140
|
+
else
|
|
141
|
+
assert "A1: same-day filename chronicle → silent" "ok"
|
|
142
|
+
fi
|
|
143
|
+
teardown_tmpdir
|
|
144
|
+
|
|
145
|
+
# A2: Prior-day filename with today amendment header → silent (Path A)
|
|
146
|
+
setup_tmpdir
|
|
147
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-29c-marathon.md" <<'EOF'
|
|
148
|
+
# 2026-08-29c chronicle
|
|
149
|
+
|
|
150
|
+
Body.
|
|
151
|
+
|
|
152
|
+
## Amendment 2026-08-30 — post-close cures landed
|
|
153
|
+
|
|
154
|
+
More work today.
|
|
155
|
+
EOF
|
|
156
|
+
add_commits_since "2026-08-30" 5
|
|
157
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
158
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
159
|
+
assert "A2: prior-day + today amendment → silent (Path A)" "STILL BLOCKED: $OUTPUT"
|
|
160
|
+
else
|
|
161
|
+
assert "A2: prior-day + today amendment → silent (Path A)" "ok"
|
|
162
|
+
fi
|
|
163
|
+
teardown_tmpdir
|
|
164
|
+
|
|
165
|
+
# A3: Multiple amendments; latest is today → silent (Path A)
|
|
166
|
+
setup_tmpdir
|
|
167
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-27a-earlier.md" <<'EOF'
|
|
168
|
+
# earlier chronicle
|
|
169
|
+
|
|
170
|
+
## Amendment 2026-08-28 — day 2
|
|
171
|
+
## Amendment 2026-08-29 — day 3
|
|
172
|
+
## Amendment 2026-08-30 — today
|
|
173
|
+
EOF
|
|
174
|
+
add_commits_since "2026-08-30" 10
|
|
175
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
176
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
177
|
+
assert "A3: multi-amendment latest is today → silent (Path A)" "STILL BLOCKED: $OUTPUT"
|
|
178
|
+
else
|
|
179
|
+
assert "A3: multi-amendment latest is today → silent (Path A)" "ok"
|
|
180
|
+
fi
|
|
181
|
+
teardown_tmpdir
|
|
182
|
+
|
|
183
|
+
# A4: Same-day filename AND prior chronicle with amendment → silent
|
|
184
|
+
setup_tmpdir
|
|
185
|
+
touch "$TMPDIR_TEST/chronicle/2026-08-30-fresh.md"
|
|
186
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-29c-marathon.md" <<'EOF'
|
|
187
|
+
# 2026-08-29c
|
|
188
|
+
## Amendment 2026-08-30 — some
|
|
189
|
+
EOF
|
|
190
|
+
add_commits_since "2026-08-30" 5
|
|
191
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
192
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
193
|
+
assert "A4: same-day + prior with amendment → silent" "output=$OUTPUT"
|
|
194
|
+
else
|
|
195
|
+
assert "A4: same-day + prior with amendment → silent" "ok"
|
|
196
|
+
fi
|
|
197
|
+
teardown_tmpdir
|
|
198
|
+
|
|
199
|
+
# =======================================================================
|
|
200
|
+
# Class B — stale chronicle (should BLOCK)
|
|
201
|
+
# =======================================================================
|
|
202
|
+
|
|
203
|
+
echo ""
|
|
204
|
+
echo "Class B — stale chronicle (should BLOCK)"
|
|
205
|
+
echo ""
|
|
206
|
+
|
|
207
|
+
# B1: Prior-day filename + NO amendment headers + >=3 commits → BLOCK
|
|
208
|
+
setup_tmpdir
|
|
209
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
210
|
+
# old chronicle
|
|
211
|
+
|
|
212
|
+
Just body content. No amendments.
|
|
213
|
+
EOF
|
|
214
|
+
add_commits_since "2026-08-30" 5
|
|
215
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
216
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
217
|
+
assert "B1: prior-day + no amendments + 5 commits → BLOCK" "ok"
|
|
218
|
+
else
|
|
219
|
+
assert "B1: prior-day + no amendments + 5 commits → BLOCK" "SILENT: $OUTPUT"
|
|
220
|
+
fi
|
|
221
|
+
teardown_tmpdir
|
|
222
|
+
|
|
223
|
+
# B2: Prior-day filename + amendment for OLD date → BLOCK (no over-mask)
|
|
224
|
+
setup_tmpdir
|
|
225
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
226
|
+
# old chronicle
|
|
227
|
+
|
|
228
|
+
## Amendment 2026-08-26 — day 2 of that session
|
|
229
|
+
|
|
230
|
+
No newer amendments.
|
|
231
|
+
EOF
|
|
232
|
+
add_commits_since "2026-08-30" 5
|
|
233
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
234
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
235
|
+
assert "B2: prior-day + old amendment + 5 commits → BLOCK (no over-mask)" "ok"
|
|
236
|
+
else
|
|
237
|
+
assert "B2: prior-day + old amendment + 5 commits → BLOCK (no over-mask)" "SILENT: $OUTPUT"
|
|
238
|
+
fi
|
|
239
|
+
teardown_tmpdir
|
|
240
|
+
|
|
241
|
+
# B3: No chronicle files at all → silent (existing behavior; preserve)
|
|
242
|
+
setup_tmpdir
|
|
243
|
+
add_commits_since "2026-08-30" 5
|
|
244
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
245
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
246
|
+
assert "B3: no chronicles at all → silent (existing behavior)" "FIRED: $OUTPUT"
|
|
247
|
+
else
|
|
248
|
+
assert "B3: no chronicles at all → silent (existing behavior)" "ok"
|
|
249
|
+
fi
|
|
250
|
+
teardown_tmpdir
|
|
251
|
+
|
|
252
|
+
# =======================================================================
|
|
253
|
+
# Class C — edge cases
|
|
254
|
+
# =======================================================================
|
|
255
|
+
|
|
256
|
+
echo ""
|
|
257
|
+
echo "Class C — edge cases"
|
|
258
|
+
echo ""
|
|
259
|
+
|
|
260
|
+
# C1: <3 commits since → silent (threshold)
|
|
261
|
+
setup_tmpdir
|
|
262
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
263
|
+
# old
|
|
264
|
+
EOF
|
|
265
|
+
add_commits_since "2026-08-30" 2
|
|
266
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
267
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
268
|
+
assert "C1: <3 commits since → silent (threshold)" "FIRED: $OUTPUT"
|
|
269
|
+
else
|
|
270
|
+
assert "C1: <3 commits since → silent (threshold)" "ok"
|
|
271
|
+
fi
|
|
272
|
+
teardown_tmpdir
|
|
273
|
+
|
|
274
|
+
# C2: Chronicle dir missing → silent (defensive)
|
|
275
|
+
setup_tmpdir
|
|
276
|
+
rm -rf "$TMPDIR_TEST/chronicle"
|
|
277
|
+
add_commits_since "2026-08-30" 5
|
|
278
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
279
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
280
|
+
assert "C2: chronicle dir missing → silent" "FIRED: $OUTPUT"
|
|
281
|
+
else
|
|
282
|
+
assert "C2: chronicle dir missing → silent" "ok"
|
|
283
|
+
fi
|
|
284
|
+
teardown_tmpdir
|
|
285
|
+
|
|
286
|
+
# C3: Malformed amendment date → falls back to filename (BLOCK)
|
|
287
|
+
setup_tmpdir
|
|
288
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
289
|
+
# old
|
|
290
|
+
## Amendment BADDATE — not a real date
|
|
291
|
+
EOF
|
|
292
|
+
add_commits_since "2026-08-30" 5
|
|
293
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
294
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
295
|
+
assert "C3: malformed amendment date → falls back to filename (BLOCK)" "ok"
|
|
296
|
+
else
|
|
297
|
+
assert "C3: malformed amendment date → falls back to filename (BLOCK)" "SILENT: $OUTPUT"
|
|
298
|
+
fi
|
|
299
|
+
teardown_tmpdir
|
|
300
|
+
|
|
301
|
+
# C4: Future-dated amendment → silent (defensive; treated as fresh)
|
|
302
|
+
setup_tmpdir
|
|
303
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
304
|
+
# old
|
|
305
|
+
## Amendment 2027-01-15 — future dated
|
|
306
|
+
EOF
|
|
307
|
+
add_commits_since "2026-08-30" 5
|
|
308
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
309
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
310
|
+
assert "C4: future-dated amendment → silent (defensive)" "FIRED: $OUTPUT"
|
|
311
|
+
else
|
|
312
|
+
assert "C4: future-dated amendment → silent (defensive)" "ok"
|
|
313
|
+
fi
|
|
314
|
+
teardown_tmpdir
|
|
315
|
+
|
|
316
|
+
# C5: Multiple amendments — latest by DATE wins (not file position)
|
|
317
|
+
setup_tmpdir
|
|
318
|
+
cat > "$TMPDIR_TEST/chronicle/2026-08-25a-old.md" <<'EOF'
|
|
319
|
+
# old
|
|
320
|
+
## Amendment 2026-08-30 — today (appears first in file)
|
|
321
|
+
## Amendment 2026-08-26 — older (appears second in file)
|
|
322
|
+
EOF
|
|
323
|
+
add_commits_since "2026-08-30" 5
|
|
324
|
+
OUTPUT=$(run_hook "2026-08-30")
|
|
325
|
+
if echo "$OUTPUT" | grep -q "chronicle —"; then
|
|
326
|
+
assert "C5: latest amendment by date wins (not file order)" "STILL BLOCKED: $OUTPUT"
|
|
327
|
+
else
|
|
328
|
+
assert "C5: latest amendment by date wins (not file order)" "ok"
|
|
329
|
+
fi
|
|
330
|
+
teardown_tmpdir
|
|
331
|
+
|
|
332
|
+
# =======================================================================
|
|
333
|
+
# Summary
|
|
334
|
+
# =======================================================================
|
|
335
|
+
|
|
336
|
+
echo ""
|
|
337
|
+
echo "----"
|
|
338
|
+
echo "PASS: $PASS"
|
|
339
|
+
echo "FAIL: $FAIL"
|
|
340
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
341
|
+
echo ""
|
|
342
|
+
echo "Failed tests:"
|
|
343
|
+
for t in "${FAILED_TESTS[@]}"; do
|
|
344
|
+
echo " - $t"
|
|
345
|
+
done
|
|
346
|
+
exit 1
|
|
347
|
+
fi
|
|
348
|
+
exit 0
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tier: standard
|
|
3
|
+
# fixtures: mock-whereami-inline
|
|
4
|
+
# Tests for .claude/hooks/session-reflection.d/36-whereami-size-check.sh
|
|
5
|
+
#
|
|
6
|
+
# Bootstrap pair per .claude/rules/bootstrap-pair-discipline.md — this test
|
|
7
|
+
# + the hook fragment ship together. RED-first per Beck — assertions written
|
|
8
|
+
# against the hook interface before any implementation lands.
|
|
9
|
+
#
|
|
10
|
+
# Anchor luminaries — michael-nygard (fail-safe defaults + stability),
|
|
11
|
+
# kent-beck (RED-first Tier 0), michael-feathers (characterization of
|
|
12
|
+
# rotate-whereami.py behavior — hook does not modify script).
|
|
13
|
+
#
|
|
14
|
+
# Run: bash .claude/hooks/session-reflection.d/tests/36-whereami-size-check.test.sh
|
|
15
|
+
|
|
16
|
+
# test-list:
|
|
17
|
+
# [x] Test 1 — Below 200KB threshold — silent (no output)
|
|
18
|
+
# [x] Test 2 — 200-250KB range — INFORM (rotation approaching; no rotate)
|
|
19
|
+
# [x] Test 3 — Above 250KB — auto-rotate via rotate-whereami.py --days 3
|
|
20
|
+
# [x] Test 4 — SKIP_WHEREAMI_SIZE_CHECK=1 — hook returns without action
|
|
21
|
+
# [x] Test 5 — Missing whereami file — hook returns silently
|
|
22
|
+
# [x] Test 6 — Missing rotate-whereami.py script — hook returns silently
|
|
23
|
+
# [x] Test 7 — Uncommitted whereami changes — hook skips rotation (R1 pre-mortem)
|
|
24
|
+
# [x] Test 8 — Fragment sourced under set -e (coordinator invocation) — parent survives
|
|
25
|
+
|
|
26
|
+
set -o pipefail
|
|
27
|
+
|
|
28
|
+
HOOK="$(cd "$(dirname "$0")/../../../.." && pwd)/.claude/hooks/session-reflection.d/36-whereami-size-check.sh"
|
|
29
|
+
ROTATE_SCRIPT="$(cd "$(dirname "$0")/../../../.." && pwd)/scripts/rotate-whereami.py"
|
|
30
|
+
|
|
31
|
+
TMPDIR=$(mktemp -d)
|
|
32
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
33
|
+
|
|
34
|
+
PASS=0
|
|
35
|
+
FAIL=0
|
|
36
|
+
|
|
37
|
+
assert() {
|
|
38
|
+
local name="$1"
|
|
39
|
+
local result="$2"
|
|
40
|
+
if [ "$result" = "ok" ]; then
|
|
41
|
+
PASS=$((PASS + 1))
|
|
42
|
+
echo "PASS: $name"
|
|
43
|
+
else
|
|
44
|
+
FAIL=$((FAIL + 1))
|
|
45
|
+
echo "FAIL: $name — $result"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Build a whereami of a given approximate size (bytes) with matched recap blocks
|
|
50
|
+
build_whereami_of_size() {
|
|
51
|
+
local file="$1"
|
|
52
|
+
local target_bytes="$2"
|
|
53
|
+
local blocks_needed=$((target_bytes / 1500)) # ~1500 bytes per block
|
|
54
|
+
[ "$blocks_needed" -lt 1 ] && blocks_needed=1
|
|
55
|
+
|
|
56
|
+
cat > "$file" <<'FIXHEAD'
|
|
57
|
+
---
|
|
58
|
+
project: test
|
|
59
|
+
phase: Construction
|
|
60
|
+
last_updated:
|
|
61
|
+
at: 2026-08-19T00:00:00Z
|
|
62
|
+
FIXHEAD
|
|
63
|
+
|
|
64
|
+
# Add 100 lines of frontmatter padding (small key: value lines) to hit lower sizes
|
|
65
|
+
for i in $(seq 1 20); do
|
|
66
|
+
echo "frontmatter_field_$i: value_$i" >> "$file"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
# Add recap blocks with unique old dates (all < 2026-08-16 for archivability
|
|
70
|
+
# under --days 3 cutoff on 2026-08-19). Distribute across 7 months to keep
|
|
71
|
+
# keys unique for up to ~196 blocks.
|
|
72
|
+
# Each block ~1400 bytes (matches 1500 divisor for approximate sizing)
|
|
73
|
+
for i in $(seq 1 $blocks_needed); do
|
|
74
|
+
local month=$(( (i - 1) / 28 + 1 ))
|
|
75
|
+
local day=$(( (i - 1) % 28 + 1 ))
|
|
76
|
+
[ $month -gt 7 ] && month=7 # wrap tail to July for large N
|
|
77
|
+
local d
|
|
78
|
+
d="2026-$(printf '%02d' $month)-$(printf '%02d' $day)"
|
|
79
|
+
echo "operator_recap_${d}: |" >> "$file"
|
|
80
|
+
for j in $(seq 1 15); do
|
|
81
|
+
echo " padding line $j for block $i to reach target size — lorem ipsum lorem ipsum lorem ipsum" >> "$file"
|
|
82
|
+
done
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
echo "---" >> "$file"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# === Test 1 — Below 200KB threshold — silent ===
|
|
89
|
+
WHEREAMI="$TMPDIR/w1.md"
|
|
90
|
+
build_whereami_of_size "$WHEREAMI" 100000 # 100KB
|
|
91
|
+
SIZE=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
92
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
|
|
93
|
+
if [ -z "$STDOUT_OUT" ]; then
|
|
94
|
+
assert "Test 1: below 200KB — silent (size=$SIZE)" "ok"
|
|
95
|
+
else
|
|
96
|
+
assert "Test 1: below 200KB — silent" "size=$SIZE output='$STDOUT_OUT'"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# === Test 2 — 200-250KB range — INFORM approaching ===
|
|
100
|
+
WHEREAMI="$TMPDIR/w2.md"
|
|
101
|
+
build_whereami_of_size "$WHEREAMI" 220000 # 220KB
|
|
102
|
+
SIZE=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
103
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
|
|
104
|
+
if echo "$STDOUT_OUT" | grep -qi "approaching\|inform\|whereami" && ! echo "$STDOUT_OUT" | grep -qi "rotated"; then
|
|
105
|
+
assert "Test 2: 200-250KB — INFORM without rotate (size=$SIZE)" "ok"
|
|
106
|
+
else
|
|
107
|
+
assert "Test 2: 200-250KB — INFORM without rotate" "size=$SIZE output='$STDOUT_OUT'"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# === Test 3 — Above 250KB — auto-rotate ===
|
|
111
|
+
WHEREAMI="$TMPDIR/w3.md"
|
|
112
|
+
build_whereami_of_size "$WHEREAMI" 300000 # 300KB
|
|
113
|
+
SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
114
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive3" bash "$HOOK" 2>&1)
|
|
115
|
+
SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
116
|
+
if [ "$SIZE_AFTER" -lt "$SIZE_BEFORE" ] && echo "$STDOUT_OUT" | grep -qi "rotated\|archive"; then
|
|
117
|
+
assert "Test 3: >250KB — auto-rotate (before=$SIZE_BEFORE after=$SIZE_AFTER)" "ok"
|
|
118
|
+
else
|
|
119
|
+
assert "Test 3: >250KB — auto-rotate" "before=$SIZE_BEFORE after=$SIZE_AFTER output='$STDOUT_OUT'"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# === Test 4 — SKIP_WHEREAMI_SIZE_CHECK=1 respects opt-out ===
|
|
123
|
+
WHEREAMI="$TMPDIR/w4.md"
|
|
124
|
+
build_whereami_of_size "$WHEREAMI" 300000
|
|
125
|
+
SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
126
|
+
STDOUT_OUT=$(SKIP_WHEREAMI_SIZE_CHECK=1 WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive4" bash "$HOOK" 2>&1)
|
|
127
|
+
SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
128
|
+
if [ "$SIZE_AFTER" = "$SIZE_BEFORE" ] && [ -z "$STDOUT_OUT" ]; then
|
|
129
|
+
assert "Test 4: SKIP_WHEREAMI_SIZE_CHECK=1 — no action" "ok"
|
|
130
|
+
else
|
|
131
|
+
assert "Test 4: SKIP_WHEREAMI_SIZE_CHECK=1 — no action" "size_before=$SIZE_BEFORE size_after=$SIZE_AFTER output='$STDOUT_OUT'"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# === Test 5 — Missing whereami file — silent ===
|
|
135
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$TMPDIR/nonexistent.md" ROTATE_SCRIPT="$ROTATE_SCRIPT" bash "$HOOK" 2>&1)
|
|
136
|
+
if [ -z "$STDOUT_OUT" ]; then
|
|
137
|
+
assert "Test 5: missing whereami file — silent" "ok"
|
|
138
|
+
else
|
|
139
|
+
assert "Test 5: missing whereami file — silent" "output='$STDOUT_OUT'"
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# === Test 6 — Missing rotate-whereami.py — silent ===
|
|
143
|
+
WHEREAMI="$TMPDIR/w6.md"
|
|
144
|
+
build_whereami_of_size "$WHEREAMI" 300000
|
|
145
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" ROTATE_SCRIPT="$TMPDIR/nonexistent-script.py" bash "$HOOK" 2>&1)
|
|
146
|
+
if [ -z "$STDOUT_OUT" ] || echo "$STDOUT_OUT" | grep -qi "skip\|not found"; then
|
|
147
|
+
assert "Test 6: missing rotate script — silent or skip note" "ok"
|
|
148
|
+
else
|
|
149
|
+
assert "Test 6: missing rotate script — silent or skip note" "output='$STDOUT_OUT'"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# === Test 7 — Uncommitted whereami changes — hook skips rotation ===
|
|
153
|
+
# Setup: whereami is 'modified' relative to git baseline
|
|
154
|
+
WHEREAMI="$TMPDIR/w7.md"
|
|
155
|
+
build_whereami_of_size "$WHEREAMI" 300000
|
|
156
|
+
SIZE_BEFORE=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
157
|
+
# Signal via WHEREAMI_MODIFIED=1 that the hook should treat whereami as dirty
|
|
158
|
+
STDOUT_OUT=$(WHEREAMI_PATH="$WHEREAMI" WHEREAMI_MODIFIED=1 ROTATE_SCRIPT="$ROTATE_SCRIPT" ARCHIVE_DIR="$TMPDIR/archive7" bash "$HOOK" 2>&1)
|
|
159
|
+
SIZE_AFTER=$(wc -c < "$WHEREAMI" | tr -d ' ')
|
|
160
|
+
if [ "$SIZE_AFTER" = "$SIZE_BEFORE" ] && echo "$STDOUT_OUT" | grep -qi "modified\|skip"; then
|
|
161
|
+
assert "Test 7: uncommitted whereami — skip rotation + inform" "ok"
|
|
162
|
+
else
|
|
163
|
+
assert "Test 7: uncommitted whereami — skip rotation + inform" "size_before=$SIZE_BEFORE size_after=$SIZE_AFTER output='$STDOUT_OUT'"
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# === Test 8 — Fragment sourced under set -e — parent survives ===
|
|
167
|
+
CANARY="$TMPDIR/canary8.txt"
|
|
168
|
+
WHEREAMI="$TMPDIR/w8.md"
|
|
169
|
+
build_whereami_of_size "$WHEREAMI" 100000
|
|
170
|
+
(
|
|
171
|
+
set -e
|
|
172
|
+
export WHEREAMI_PATH="$WHEREAMI"
|
|
173
|
+
export ROTATE_SCRIPT="$ROTATE_SCRIPT"
|
|
174
|
+
# shellcheck disable=SC1090
|
|
175
|
+
source "$HOOK" 2>/dev/null
|
|
176
|
+
echo "CANARY_REACHED" > "$CANARY"
|
|
177
|
+
) 2>&1
|
|
178
|
+
if [ -f "$CANARY" ] && grep -q "CANARY_REACHED" "$CANARY"; then
|
|
179
|
+
assert "Test 8: sourced under set -e — parent survives" "ok"
|
|
180
|
+
else
|
|
181
|
+
assert "Test 8: sourced under set -e — parent survives" "canary not written; fragment likely used exit"
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# === Summary ===
|
|
185
|
+
echo ""
|
|
186
|
+
echo "==========================================="
|
|
187
|
+
echo "Result: $PASS passed, $FAIL failed"
|
|
188
|
+
echo "==========================================="
|
|
189
|
+
[ "$FAIL" -eq 0 ]
|