@ramarivera/coding-buddy 0.9.0 → 0.9.1
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/adapters/omp/ui.ts +13 -31
- package/adapters/pi/ui.ts +13 -31
- package/adapters/shared/file-storage.ts +1 -0
- package/adapters/shared/widget-layout.ts +125 -11
- package/core/model.ts +1 -0
- package/package.json +1 -2
- package/scripts/ci/check-statusline.sh +231 -118
- package/scripts/ci/render-statusline-pty.py +12 -4
- package/scripts/ci/render-vhs-goldens.sh +10 -19
- package/scripts/ci/statusline.tape +28 -2
- package/scripts/ci/vhs-density-fixtures.sh +27 -0
- package/scripts/ci/vhs-run-statusline.sh +13 -1
- package/scripts/ci/write-density-status.ts +86 -0
- package/server/art.ts +29 -8
- package/server/state.ts +85 -25
- package/statusline/buddy-status.sh +161 -41
- package/scripts/postinstall.cjs +0 -42
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# CI golden check for the Claude Code statusline.
|
|
3
3
|
#
|
|
4
|
-
# Primary signal: loud text assertions over a
|
|
5
|
-
# fixtures. A broken render fails the build.
|
|
6
|
-
# step (scripts/ci/check-vhs-goldens.sh).
|
|
4
|
+
# Primary signal: loud text assertions over a row/column density matrix +
|
|
5
|
+
# CJK / wide-emoji / corrupt-eye fixtures. A broken render fails the build.
|
|
6
|
+
# VHS image goldens are a separate step (scripts/ci/check-vhs-goldens.sh).
|
|
7
7
|
#
|
|
8
8
|
# Run from the repo root:
|
|
9
9
|
# bash scripts/ci/check-statusline.sh
|
|
@@ -16,11 +16,24 @@ fixture_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/statusline-fixture" && pwd)"
|
|
|
16
16
|
pty_helper="$ROOT/scripts/ci/render-statusline-pty.py"
|
|
17
17
|
payload='{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}'
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
|
|
19
|
+
# Width/row budgets for each density tier. Full frames are 5 art rows + name;
|
|
20
|
+
# compact is 3 art rows + name; minimal is a single sprite/face + name line.
|
|
21
|
+
DENSITY_FULL_MIN=6
|
|
22
|
+
DENSITY_FULL_MAX=8
|
|
23
|
+
DENSITY_COMPACT_MIN=4
|
|
24
|
+
DENSITY_COMPACT_MAX=4
|
|
25
|
+
DENSITY_MINIMAL_MIN=1
|
|
26
|
+
DENSITY_MINIMAL_MAX=1
|
|
21
27
|
|
|
22
28
|
# Claude Code kills statusline scripts around 1s; stay under that budget.
|
|
23
|
-
TIME_BUDGET_MS
|
|
29
|
+
TIME_BUDGET_MS=${BUDDY_STATUSLINE_TIME_BUDGET_MS:-1000}
|
|
30
|
+
|
|
31
|
+
# Density matrix uses BUDDY_STATUSLINE_ROWS to make CI independent of the
|
|
32
|
+
# ambient terminal size. Auto tier: full >=40 rows, compact 20-39, minimal <20.
|
|
33
|
+
# Very narrow terminal widths force minimal regardless of rows.
|
|
34
|
+
NARROW_COLS=40
|
|
35
|
+
|
|
36
|
+
CLEANUP_DIRS=()
|
|
24
37
|
|
|
25
38
|
fail() {
|
|
26
39
|
echo "FAIL: $1" >&2
|
|
@@ -30,6 +43,17 @@ fail() {
|
|
|
30
43
|
exit 1
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
cleanup() {
|
|
47
|
+
for d in "${CLEANUP_DIRS[@]}"; do
|
|
48
|
+
rm -rf "$d" 2>/dev/null || true
|
|
49
|
+
done
|
|
50
|
+
}
|
|
51
|
+
trap cleanup EXIT
|
|
52
|
+
|
|
53
|
+
add_cleanup() {
|
|
54
|
+
CLEANUP_DIRS+=("$1")
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
# Strip SGR / CSI color sequences. Keep the rest of the glyphs intact so
|
|
34
58
|
# display-width math matches what a terminal would paint.
|
|
35
59
|
strip_ansi() {
|
|
@@ -89,34 +113,61 @@ now_ms() {
|
|
|
89
113
|
fi
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
|
|
116
|
+
# Augment a committed status.json with compactFrames/minimalFrames arrays.
|
|
117
|
+
# Falls back to full frames when the current getStatusFrames() does not yet
|
|
118
|
+
# emit the new arrays, so the CI harness is honest about missing density.
|
|
119
|
+
write_density_status() {
|
|
120
|
+
local src_status="$1"
|
|
121
|
+
local dest_status="$2"
|
|
122
|
+
mkdir -p "$(dirname "$dest_status")"
|
|
123
|
+
bun "$ROOT/scripts/ci/write-density-status.ts" "$dest_status" < "$src_status"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Build a temporary baseline fixture with density-aware status arrays.
|
|
127
|
+
make_baseline_dir() {
|
|
128
|
+
local dest="$1"
|
|
129
|
+
rm -rf "$dest"
|
|
130
|
+
mkdir -p "$dest/buddy-state"
|
|
131
|
+
cp "$fixture_dir/buddy-state/config.json" "$dest/buddy-state/config.json"
|
|
132
|
+
write_density_status "$fixture_dir/buddy-state/status.json" "$dest/buddy-state/status.json"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Render statusline against a fixture dir at a fixed terminal width/rows.
|
|
136
|
+
# Sets globals: OUTPUT, PLAIN, ELAPSED_MS, EFFECTIVE_COLS, EFFECTIVE_ROWS.
|
|
96
137
|
render_at() {
|
|
97
|
-
local
|
|
138
|
+
local source_dir="$1"
|
|
98
139
|
local cols="$2"
|
|
99
|
-
local
|
|
140
|
+
local rows="${3:-50}"
|
|
141
|
+
local density="${4:-auto}"
|
|
142
|
+
local test_dir rendered errfile
|
|
143
|
+
test_dir=$(mktemp -d)
|
|
144
|
+
add_cleanup "$test_dir"
|
|
145
|
+
mkdir -p "$test_dir/buddy-state"
|
|
146
|
+
cp "$source_dir/buddy-state/status.json" "$test_dir/buddy-state/status.json"
|
|
147
|
+
cp "$source_dir/buddy-state/config.json" "$test_dir/buddy-state/config.json"
|
|
148
|
+
for _react in "$source_dir"/buddy-state/reaction.*.json; do
|
|
149
|
+
[ -f "$_react" ] && cp "$_react" "$test_dir/buddy-state/"
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
if [ "$density" != "auto" ]; then
|
|
153
|
+
jq --arg d "$density" '.statuslineDensity = $d' "$test_dir/buddy-state/config.json" \
|
|
154
|
+
> "$test_dir/buddy-state/config.json.tmp" \
|
|
155
|
+
&& mv "$test_dir/buddy-state/config.json.tmp" "$test_dir/buddy-state/config.json"
|
|
156
|
+
fi
|
|
157
|
+
|
|
100
158
|
errfile=$(mktemp)
|
|
101
|
-
# The fixtures write reaction.default.json, but paths.sh derives the
|
|
102
|
-
# per-session filename from CLAUDE_CODE_SESSION_ID / TMUX_PANE. Whichever of
|
|
103
|
-
# those happens to be set in the ambient environment decides which reaction
|
|
104
|
-
# file is read, so the check passed locally and failed on CI (no bubble
|
|
105
|
-
# rendered, because the reaction was looked up under a different session id).
|
|
106
|
-
# Pin them empty so the session id is always "default" and the result does
|
|
107
|
-
# not depend on where the check runs.
|
|
108
159
|
rendered="$(
|
|
109
160
|
CLAUDE_CODE_SESSION_ID="" \
|
|
110
161
|
TMUX_PANE="" \
|
|
162
|
+
BUDDY_STATUSLINE_ROWS="$rows" \
|
|
111
163
|
STATUSLINE_PAYLOAD="$payload" \
|
|
112
164
|
python3 "$pty_helper" \
|
|
113
165
|
"$cols" \
|
|
114
166
|
"$ROOT" \
|
|
115
|
-
"$
|
|
167
|
+
"$test_dir" \
|
|
116
168
|
"$ROOT/statusline/buddy-status.sh" \
|
|
117
169
|
2>"$errfile"
|
|
118
170
|
)"
|
|
119
|
-
# Prefer the child-only timer from the PTY helper (excludes harness import).
|
|
120
171
|
ELAPSED_MS=$(sed -n 's/^STATUSLINE_ELAPSED_MS=//p' "$errfile" | tail -n1)
|
|
121
172
|
rm -f "$errfile"
|
|
122
173
|
case "$ELAPSED_MS" in
|
|
@@ -124,43 +175,67 @@ render_at() {
|
|
|
124
175
|
esac
|
|
125
176
|
OUTPUT="$(printf '%s' "$rendered" | tr -d '\r')"
|
|
126
177
|
PLAIN="$(strip_ansi "$OUTPUT")"
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
178
|
+
# Assert the real terminal width budget; old <40 special-casing is gone.
|
|
179
|
+
EFFECTIVE_COLS="$cols"
|
|
180
|
+
EFFECTIVE_ROWS="$rows"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Resolve the expected density tier for a (density, rows, cols) tuple.
|
|
184
|
+
expected_tier() {
|
|
185
|
+
local density="${1:-auto}"
|
|
186
|
+
local rows="$2"
|
|
187
|
+
local cols="$3"
|
|
188
|
+
case "$density" in
|
|
189
|
+
full|compact|minimal)
|
|
190
|
+
printf '%s\n' "$density"
|
|
191
|
+
;;
|
|
192
|
+
*)
|
|
193
|
+
if [ "$rows" -lt 20 ] || [ "$cols" -lt "$NARROW_COLS" ]; then
|
|
194
|
+
printf 'minimal\n'
|
|
195
|
+
elif [ "$rows" -lt 40 ]; then
|
|
196
|
+
printf 'compact\n'
|
|
197
|
+
else
|
|
198
|
+
printf 'full\n'
|
|
199
|
+
fi
|
|
200
|
+
;;
|
|
201
|
+
esac
|
|
135
202
|
}
|
|
136
203
|
|
|
137
204
|
assert_render() {
|
|
138
205
|
local label="$1"
|
|
139
206
|
local cols="$2"
|
|
140
|
-
local
|
|
141
|
-
local
|
|
142
|
-
local
|
|
207
|
+
local rows="$3"
|
|
208
|
+
local expect_name="$4"
|
|
209
|
+
local density="${5:-auto}"
|
|
210
|
+
local expect_substr="${6:-}"
|
|
211
|
+
|
|
212
|
+
local tier min_lines max_lines line_count max_w w line
|
|
213
|
+
tier="$(expected_tier "$density" "$rows" "$cols")"
|
|
214
|
+
case "$tier" in
|
|
215
|
+
full) min_lines=$DENSITY_FULL_MIN; max_lines=$DENSITY_FULL_MAX ;;
|
|
216
|
+
compact) min_lines=$DENSITY_COMPACT_MIN; max_lines=$DENSITY_COMPACT_MAX ;;
|
|
217
|
+
minimal) min_lines=$DENSITY_MINIMAL_MIN; max_lines=$DENSITY_MINIMAL_MAX ;;
|
|
218
|
+
*) fail "[$label] unknown expected tier '$tier'" "$PLAIN" ;;
|
|
219
|
+
esac
|
|
143
220
|
|
|
144
|
-
|
|
145
|
-
plain_lines=$(printf '%s\n' "$PLAIN")
|
|
146
|
-
line_count=$(printf '%s\n' "$plain_lines" | wc -l | tr -d ' ')
|
|
221
|
+
line_count=$(printf '%s' "$PLAIN" | awk 'END {print NR}')
|
|
147
222
|
|
|
148
|
-
printf '%s
|
|
223
|
+
printf '%s' "$PLAIN" | grep -Fq "$expect_name" \
|
|
149
224
|
|| fail "[$label] pet name '$expect_name' missing from output" "$PLAIN"
|
|
150
225
|
|
|
151
|
-
[ "$line_count" -ge
|
|
152
|
-
|| fail "[$label] expected
|
|
226
|
+
[ "$line_count" -ge "$min_lines" ] && [ "$line_count" -le "$max_lines" ] \
|
|
227
|
+
|| fail "[$label] expected $tier line count $min_lines-$max_lines, got $line_count" "$PLAIN"
|
|
153
228
|
|
|
154
229
|
if [ -n "$expect_substr" ]; then
|
|
155
|
-
printf '%s
|
|
230
|
+
printf '%s' "$PLAIN" | grep -Fq "$expect_substr" \
|
|
156
231
|
|| fail "[$label] expected substring missing: $expect_substr" "$PLAIN"
|
|
157
232
|
fi
|
|
158
233
|
|
|
159
234
|
# Sprite content must stay art, not descriptor words.
|
|
160
|
-
if printf '%s
|
|
235
|
+
if printf '%s' "$PLAIN" | grep -Eq '<\([[:alpha:]]{2,}'; then
|
|
161
236
|
fail "[$label] sprite looks corrupted (word leaked into eye slot)" "$PLAIN"
|
|
162
237
|
fi
|
|
163
|
-
if printf '%s
|
|
238
|
+
if printf '%s' "$PLAIN" | grep -Fq '{E}'; then
|
|
164
239
|
fail "[$label] unresolved {E} eye placeholder in output" "$PLAIN"
|
|
165
240
|
fi
|
|
166
241
|
|
|
@@ -171,16 +246,16 @@ assert_render() {
|
|
|
171
246
|
if [ "$w" -gt "$max_w" ]; then
|
|
172
247
|
max_w=$w
|
|
173
248
|
fi
|
|
174
|
-
if [ "$w" -gt "$
|
|
175
|
-
fail "[$label] row display width $w exceeds
|
|
249
|
+
if [ "$w" -gt "$cols" ]; then
|
|
250
|
+
fail "[$label] row display width $w exceeds requested cols=$cols: $(printf '%q' "$line")" "$PLAIN"
|
|
176
251
|
fi
|
|
177
|
-
done
|
|
252
|
+
done < <(printf '%s' "$PLAIN")
|
|
178
253
|
|
|
179
254
|
# Bubble box: border rows and text rows must agree in outer width.
|
|
180
255
|
# Bubble and art share a line ("| text |-- <(° ..."), so measure only the
|
|
181
|
-
# box segment. Ignore short art glyphs like the duck feet (
|
|
256
|
+
# box segment. Ignore short art glyphs like the duck feet (\`--') that also
|
|
182
257
|
# start with a backtick.
|
|
183
|
-
local box_w=""
|
|
258
|
+
local box_w="" trimmed first bubble_only seg_w
|
|
184
259
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
185
260
|
[ -z "$line" ] && continue
|
|
186
261
|
trimmed=$(printf '%s' "$line" | sed $'s/^[ \t\xE2\xA0\x80]*//')
|
|
@@ -188,15 +263,12 @@ assert_render() {
|
|
|
188
263
|
bubble_only=""
|
|
189
264
|
if [ "$first" = "." ]; then
|
|
190
265
|
bubble_only=$(printf '%s' "$trimmed" | sed -n 's/^\(\.[-][-]*\.\).*/\1/p')
|
|
191
|
-
elif [ "$first" = '
|
|
192
|
-
bubble_only=$(printf '%s' "$trimmed" | sed -n "s/^\\(
|
|
266
|
+
elif [ "$first" = '\`' ]; then
|
|
267
|
+
bubble_only=$(printf '%s' "$trimmed" | sed -n "s/^\\(\\\`[-][-]*'\\).*/\\1/p")
|
|
193
268
|
elif [ "$first" = "|" ]; then
|
|
194
269
|
bubble_only=$(printf '%s' "$trimmed" | sed -n 's/^\(|[^|]*|\).*/\1/p')
|
|
195
270
|
fi
|
|
196
271
|
[ -z "$bubble_only" ] && continue
|
|
197
|
-
# Real speech boxes are wide (INNER_W defaults to 44 → outer 48). Skip
|
|
198
|
-
# any short accidental match.
|
|
199
|
-
local seg_w
|
|
200
272
|
seg_w=$(dwidth "$bubble_only")
|
|
201
273
|
if [ "$seg_w" -lt 20 ]; then
|
|
202
274
|
continue
|
|
@@ -212,15 +284,22 @@ assert_render() {
|
|
|
212
284
|
fail "[$label] bubble border width $seg_w != $box_w" "$PLAIN"
|
|
213
285
|
fi
|
|
214
286
|
fi
|
|
215
|
-
done
|
|
287
|
+
done < <(printf '%s' "$PLAIN")
|
|
216
288
|
|
|
217
|
-
# Name row must
|
|
218
|
-
|
|
219
|
-
|
|
289
|
+
# Name row must stay within the panel budget. Minimal is a single line, so
|
|
290
|
+
# allow the full terminal width there; full/compact keep the existing panel
|
|
291
|
+
# sanity check.
|
|
292
|
+
local name_line name_w name_limit
|
|
293
|
+
name_line=$(printf '%s' "$PLAIN" | grep -F "$expect_name" | head -n1 || true)
|
|
220
294
|
if [ -n "$name_line" ]; then
|
|
221
295
|
name_w=$(dwidth "$(printf '%s' "$name_line" | sed $'s/^[ \t\xE2\xA0\x80]*//')")
|
|
222
|
-
if [ "$
|
|
223
|
-
|
|
296
|
+
if [ "$tier" = "minimal" ]; then
|
|
297
|
+
name_limit=$cols
|
|
298
|
+
else
|
|
299
|
+
name_limit=24
|
|
300
|
+
fi
|
|
301
|
+
if [ "$name_w" -gt "$name_limit" ]; then
|
|
302
|
+
fail "[$label] name row display width $name_w exceeds limit $name_limit" "$PLAIN"
|
|
224
303
|
fi
|
|
225
304
|
fi
|
|
226
305
|
|
|
@@ -228,59 +307,74 @@ assert_render() {
|
|
|
228
307
|
fail "[$label] statusline took ${ELAPSED_MS}ms (budget ${TIME_BUDGET_MS}ms)" "$PLAIN"
|
|
229
308
|
fi
|
|
230
309
|
|
|
231
|
-
echo "OK: $label requested_cols=$cols
|
|
310
|
+
echo "OK: $label density=$tier requested_cols=$cols rows=$rows lines=$line_count max_row_w=$max_w time=${ELAPSED_MS}ms name='$expect_name'"
|
|
232
311
|
}
|
|
233
312
|
|
|
234
313
|
# ─── Fixture 1: committed baseline (pikachu / Cobalt) ────────────────────────
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
314
|
+
baseline_dir=$(mktemp -d)
|
|
315
|
+
add_cleanup "$baseline_dir"
|
|
316
|
+
make_baseline_dir "$baseline_dir"
|
|
317
|
+
base_name="$(jq -r .name "$baseline_dir/buddy-state/status.json")"
|
|
318
|
+
|
|
319
|
+
# Warm-up (not timed). Claude Code keeps the process hot after the first tick.
|
|
320
|
+
render_at "$baseline_dir" 80 50 auto >/dev/null || true
|
|
321
|
+
|
|
322
|
+
# Row/column density matrix. BUDDY_STATUSLINE_ROWS is always explicit so the
|
|
323
|
+
# results do not depend on the CI runner's terminal size.
|
|
324
|
+
LABELS=(
|
|
325
|
+
"full@80x50"
|
|
326
|
+
"full@120x50"
|
|
327
|
+
"compact@60x30"
|
|
328
|
+
"compact@80x25"
|
|
329
|
+
"minimal@80x10"
|
|
330
|
+
"narrow-minimal@20x50"
|
|
331
|
+
"explicit-full@80x10"
|
|
332
|
+
"explicit-compact@80x50"
|
|
333
|
+
"explicit-minimal@80x50"
|
|
334
|
+
"invalid-normalizes@80x50"
|
|
335
|
+
)
|
|
336
|
+
ROWS=(50 50 30 25 10 50 10 50 50 50)
|
|
337
|
+
COLS=(80 120 60 80 80 20 80 80 80 80)
|
|
338
|
+
DENSITIES=(auto auto auto auto auto auto full compact minimal invalid)
|
|
339
|
+
|
|
340
|
+
for i in "${!LABELS[@]}"; do
|
|
341
|
+
label="${LABELS[$i]}"
|
|
342
|
+
rows="${ROWS[$i]}"
|
|
343
|
+
cols="${COLS[$i]}"
|
|
344
|
+
density="${DENSITIES[$i]}"
|
|
345
|
+
render_at "$baseline_dir" "$cols" "$rows" "$density"
|
|
346
|
+
assert_render "$label" "$cols" "$rows" "$base_name" "$density"
|
|
242
347
|
done
|
|
243
348
|
|
|
244
349
|
# ─── Fixture 2: duck + CJK reaction (real prior breakage) ────────────────────
|
|
245
350
|
cjk_dir=$(mktemp -d)
|
|
246
|
-
|
|
247
|
-
trap cleanup EXIT
|
|
351
|
+
add_cleanup "$cjk_dir"
|
|
248
352
|
mkdir -p "$cjk_dir/buddy-state"
|
|
249
353
|
cp "$fixture_dir/buddy-state/config.json" "$cjk_dir/buddy-state/config.json"
|
|
250
354
|
|
|
251
|
-
bun -
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const bones = {
|
|
255
|
-
rarity: 'common', species: 'duck', eye: '°', hat: 'none', shiny: false,
|
|
256
|
-
stats: { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
|
|
257
|
-
peak: 'DEBUGGING', dump: 'PATIENCE',
|
|
258
|
-
};
|
|
259
|
-
const { frames, frameSequence } = getStatusFrames(bones);
|
|
260
|
-
writeFileSync(process.argv[1], JSON.stringify({
|
|
261
|
-
name: 'Daffodil', species: 'duck', rarity: 'common', stars: '★',
|
|
262
|
-
face: '(°>', eye: '°', shiny: false, hat: 'none', reaction: '',
|
|
263
|
-
muted: false, achievement: '', frames, frameSequence,
|
|
264
|
-
level: 1, xp: 0, mood: 'focused',
|
|
265
|
-
}));
|
|
266
|
-
" "$cjk_dir/buddy-state/status.json"
|
|
355
|
+
cat <<'JSON' | bun "$ROOT/scripts/ci/write-density-status.ts" "$cjk_dir/buddy-state/status.json"
|
|
356
|
+
{"name":"Daffodil","species":"duck","rarity":"common","stars":"★","face":"(°>","eye":"°","shiny":false,"hat":"none","reaction":"","muted":false,"achievement":"","level":1,"xp":0,"mood":"focused"}
|
|
357
|
+
JSON
|
|
267
358
|
|
|
268
359
|
_ts=$(now_ms)
|
|
269
360
|
printf '{"reaction":"编译通过了 你好世界","timestamp":%s,"reason":"turn"}\n' "$_ts" \
|
|
270
361
|
> "$cjk_dir/buddy-state/reaction.default.json"
|
|
271
362
|
|
|
272
|
-
#
|
|
273
|
-
# shrinking/dropping bubbles under that floor. Exercise CJK width accounting
|
|
274
|
-
# at budgets that can actually hold the current box.
|
|
363
|
+
# Full width CJK/emoji bubble checks.
|
|
275
364
|
for cols in 80 93 120 200; do
|
|
276
|
-
render_at "$cjk_dir" "$cols"
|
|
277
|
-
assert_render "cjk@${cols}" "$cols" "Daffodil" "你好"
|
|
278
|
-
printf '%s
|
|
365
|
+
render_at "$cjk_dir" "$cols" 50 auto
|
|
366
|
+
assert_render "cjk@${cols}" "$cols" 50 "Daffodil" auto "你好"
|
|
367
|
+
printf '%s' "$PLAIN" | grep -Fq '<(°' \
|
|
279
368
|
|| fail "[cjk@${cols}] duck eye row missing degree-sign glyph" "$PLAIN"
|
|
280
369
|
done
|
|
281
370
|
|
|
371
|
+
# Minimal sanity: name stays visible and nothing escapes the narrow budget.
|
|
372
|
+
render_at "$cjk_dir" 20 10 auto
|
|
373
|
+
assert_render "cjk-minimal@20x10" 20 10 "Daffodil" auto
|
|
374
|
+
|
|
282
375
|
# ─── Fixture 3: wide-emoji reaction (VS16 / presentation) ────────────────────
|
|
283
376
|
emoji_dir=$(mktemp -d)
|
|
377
|
+
add_cleanup "$emoji_dir"
|
|
284
378
|
mkdir -p "$emoji_dir/buddy-state"
|
|
285
379
|
cp "$cjk_dir/buddy-state/status.json" "$emoji_dir/buddy-state/status.json"
|
|
286
380
|
cp "$fixture_dir/buddy-state/config.json" "$emoji_dir/buddy-state/config.json"
|
|
@@ -289,50 +383,69 @@ printf '{"reaction":"ship it 🎉 ❤️ ✨","timestamp":%s,"reason":"success"}
|
|
|
289
383
|
> "$emoji_dir/buddy-state/reaction.default.json"
|
|
290
384
|
|
|
291
385
|
for cols in 80 93 120 200; do
|
|
292
|
-
render_at "$emoji_dir" "$cols"
|
|
293
|
-
assert_render "emoji@${cols}" "$cols" "Daffodil" "🎉"
|
|
386
|
+
render_at "$emoji_dir" "$cols" 50 auto
|
|
387
|
+
assert_render "emoji@${cols}" "$cols" 50 "Daffodil" auto "🎉"
|
|
294
388
|
done
|
|
295
389
|
|
|
390
|
+
render_at "$emoji_dir" 20 10 auto
|
|
391
|
+
assert_render "emoji-minimal@20x10" 20 10 "Daffodil" auto
|
|
392
|
+
|
|
296
393
|
# ─── Fixture 4: corrupt eye descriptor must not reach the sprite ─────────────
|
|
297
394
|
# Regression for the live "<(normal ..." bug: even if bones.eye is the word
|
|
298
|
-
# "normal", getStatusFrames must bake a real glyph.
|
|
395
|
+
# "normal", getStatusFrames must bake a real glyph across every density array.
|
|
299
396
|
corrupt_dir=$(mktemp -d)
|
|
397
|
+
add_cleanup "$corrupt_dir"
|
|
300
398
|
mkdir -p "$corrupt_dir/buddy-state"
|
|
301
399
|
cp "$fixture_dir/buddy-state/config.json" "$corrupt_dir/buddy-state/config.json"
|
|
302
400
|
|
|
401
|
+
cat <<'JSON' | bun "$ROOT/scripts/ci/write-density-status.ts" "$corrupt_dir/buddy-state/status.json"
|
|
402
|
+
{"name":"Daffodil","species":"duck","rarity":"common","stars":"★","face":"(°>","eye":"normal","shiny":false,"hat":"none","reaction":"","muted":false,"achievement":"","level":1,"xp":0,"mood":"focused"}
|
|
403
|
+
JSON
|
|
404
|
+
|
|
405
|
+
# Ensure the corrupt eye descriptor was sanitized in every density array.
|
|
303
406
|
bun -e "
|
|
304
|
-
import {
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.error('unresolved placeholder:', body);
|
|
319
|
-
process.exit(2);
|
|
407
|
+
import { readFileSync } from 'fs';
|
|
408
|
+
const status = JSON.parse(readFileSync(process.argv[1], 'utf8'));
|
|
409
|
+
for (const key of ['frames', 'compactFrames', 'minimalFrames']) {
|
|
410
|
+
const arr = status[key];
|
|
411
|
+
if (!arr) continue;
|
|
412
|
+
for (const body of arr) {
|
|
413
|
+
if (body.includes('normal')) {
|
|
414
|
+
console.error('getStatusFrames leaked descriptor into', key, ':', body);
|
|
415
|
+
process.exit(2);
|
|
416
|
+
}
|
|
417
|
+
if (body.includes('{E}')) {
|
|
418
|
+
console.error('unresolved {E} placeholder in', key);
|
|
419
|
+
process.exit(2);
|
|
420
|
+
}
|
|
320
421
|
}
|
|
321
422
|
}
|
|
322
|
-
writeFileSync(process.argv[1], JSON.stringify({
|
|
323
|
-
name: 'Daffodil', species: 'duck', rarity: 'common', stars: '★',
|
|
324
|
-
face: '(°>', eye: '°', shiny: false, hat: 'none', reaction: '',
|
|
325
|
-
muted: false, achievement: '', frames, frameSequence,
|
|
326
|
-
level: 1, xp: 0, mood: 'focused',
|
|
327
|
-
}));
|
|
328
423
|
" "$corrupt_dir/buddy-state/status.json"
|
|
329
424
|
|
|
330
|
-
render_at "$corrupt_dir" 80
|
|
331
|
-
assert_render "corrupt-eye@80" 80 "Daffodil"
|
|
332
|
-
if printf '%s
|
|
425
|
+
render_at "$corrupt_dir" 80 50 auto
|
|
426
|
+
assert_render "corrupt-eye@80" 80 50 "Daffodil" auto
|
|
427
|
+
if printf '%s' "$PLAIN" | grep -Fq 'normal'; then
|
|
333
428
|
fail "[corrupt-eye@80] literal normal appeared in render" "$PLAIN"
|
|
334
429
|
fi
|
|
335
|
-
printf '%s
|
|
430
|
+
printf '%s' "$PLAIN" | grep -Fq '<(°' \
|
|
336
431
|
|| fail "[corrupt-eye@80] expected sanitized degree-sign eye" "$PLAIN"
|
|
337
432
|
|
|
433
|
+
render_at "$corrupt_dir" 20 10 auto
|
|
434
|
+
assert_render "corrupt-eye-minimal@20x10" 20 10 "Daffodil" auto
|
|
435
|
+
|
|
436
|
+
# ─── Fixture 5: row override precedence smoke test ───────────────────────────
|
|
437
|
+
# PTY rows are set to 50 by the helper, but BUDDY_STATUSLINE_ROWS=10 forces
|
|
438
|
+
# minimal selection. This proves the env var wins over the PTY probe.
|
|
439
|
+
render_at "$baseline_dir" 80 50 auto
|
|
440
|
+
# Sanity: without the override the previous rows=50 call already exercised full.
|
|
441
|
+
# Now pin minimal with the same terminal rows to assert override behavior.
|
|
442
|
+
render_at "$baseline_dir" 80 10 auto
|
|
443
|
+
assert_render "row-override@80x10" 80 10 "$base_name" auto
|
|
444
|
+
|
|
445
|
+
# ─── Fixture 6: width invariants at the historic narrow width ──────────────────
|
|
446
|
+
# COLUMNS=20 used to be special-cased; the new density path must keep the name
|
|
447
|
+
# and sprite intact while honoring the actual 20-column budget.
|
|
448
|
+
render_at "$baseline_dir" 20 50 auto
|
|
449
|
+
assert_render "narrow-width@20x50" 20 50 "$base_name" auto
|
|
450
|
+
|
|
338
451
|
echo "OK: all statusline golden assertions passed"
|
|
@@ -24,6 +24,14 @@ import termios
|
|
|
24
24
|
import time
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
def _positive_int(env_name: str, default: int) -> int:
|
|
28
|
+
value = os.environ.get(env_name, "")
|
|
29
|
+
if value and value.isdigit():
|
|
30
|
+
parsed = int(value)
|
|
31
|
+
if parsed > 0:
|
|
32
|
+
return parsed
|
|
33
|
+
return default
|
|
34
|
+
|
|
27
35
|
def main() -> int:
|
|
28
36
|
if len(sys.argv) != 5:
|
|
29
37
|
print(
|
|
@@ -36,7 +44,7 @@ def main() -> int:
|
|
|
36
44
|
root = sys.argv[2]
|
|
37
45
|
config_dir = sys.argv[3]
|
|
38
46
|
script = sys.argv[4]
|
|
39
|
-
rows = 30
|
|
47
|
+
rows = _positive_int("BUDDY_STATUSLINE_ROWS", 30)
|
|
40
48
|
payload = os.environ.get(
|
|
41
49
|
"STATUSLINE_PAYLOAD",
|
|
42
50
|
'{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}',
|
|
@@ -47,7 +55,7 @@ def main() -> int:
|
|
|
47
55
|
"printf %s "
|
|
48
56
|
+ repr(payload)
|
|
49
57
|
+ " | "
|
|
50
|
-
+ f"COLUMNS={cols} BUDDY_FAKE_NOW=0 "
|
|
58
|
+
+ f"COLUMNS={cols} BUDDY_FAKE_NOW=0 BUDDY_STATUSLINE_ROWS={rows} "
|
|
51
59
|
+ f"CLAUDE_CONFIG_DIR={config_dir!r} "
|
|
52
60
|
+ "CLAUDE_CODE_SESSION_ID= TMUX_PANE= BUDDY_SHELL= "
|
|
53
61
|
+ f"bash {script!r}"
|
|
@@ -57,11 +65,11 @@ def main() -> int:
|
|
|
57
65
|
pid, fd = pty.fork()
|
|
58
66
|
if pid == 0:
|
|
59
67
|
os.chdir(root)
|
|
60
|
-
os.execvp("bash", ["bash", "-
|
|
68
|
+
os.execvp("bash", ["bash", "-c", cmd])
|
|
61
69
|
|
|
62
70
|
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
|
|
63
71
|
out = bytearray()
|
|
64
|
-
deadline = time.time() +
|
|
72
|
+
deadline = time.time() + 15.0
|
|
65
73
|
while time.time() < deadline:
|
|
66
74
|
readable, _, _ = select.select([fd], [], [], 0.1)
|
|
67
75
|
if fd in readable:
|
|
@@ -118,6 +118,9 @@ mkdir -p "$OUT_DIR"
|
|
|
118
118
|
env_ci_was="${CI-}"
|
|
119
119
|
unset CI || true
|
|
120
120
|
export COLORTERM=truecolor
|
|
121
|
+
# Pin rows so VHS golden renders stay at full density even when the terminal
|
|
122
|
+
# emulator reports a smaller size. vhs-run-statusline.sh falls back to 50 too.
|
|
123
|
+
export BUDDY_STATUSLINE_ROWS=50
|
|
121
124
|
|
|
122
125
|
# Fixture generation uses only in-repo modules (server/art.ts). Never touch
|
|
123
126
|
# the developer's live ~/.config/claude/buddy-state — CLAUDE_CONFIG_DIR is
|
|
@@ -127,32 +130,20 @@ mkdir -p "$gen_dir/cjk/buddy-state" "$gen_dir/emoji/buddy-state"
|
|
|
127
130
|
cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/cjk/buddy-state/config.json"
|
|
128
131
|
cp scripts/ci/statusline-fixture/buddy-state/config.json "$gen_dir/emoji/buddy-state/config.json"
|
|
129
132
|
|
|
133
|
+
bun "$ROOT/scripts/ci/write-density-status.ts" "$gen_dir/cjk/buddy-state/status.json" <<'JSON'
|
|
134
|
+
{"name":"Daffodil","species":"duck","rarity":"common","stars":"★","face":"(°>","eye":"°","shiny":false,"hat":"none","reaction":"","muted":false,"achievement":"","level":1,"xp":0,"mood":"focused"}
|
|
135
|
+
JSON
|
|
136
|
+
cp "$gen_dir/cjk/buddy-state/status.json" "$gen_dir/emoji/buddy-state/status.json"
|
|
137
|
+
|
|
130
138
|
bun -e "
|
|
131
139
|
import { writeFileSync } from 'fs';
|
|
132
|
-
|
|
133
|
-
const bones = {
|
|
134
|
-
rarity: 'common', species: 'duck', eye: '°', hat: 'none', shiny: false,
|
|
135
|
-
stats: { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
|
|
136
|
-
peak: 'DEBUGGING', dump: 'PATIENCE',
|
|
137
|
-
};
|
|
138
|
-
const { frames, frameSequence } = getStatusFrames(bones);
|
|
139
|
-
const status = {
|
|
140
|
-
name: 'Daffodil', species: 'duck', rarity: 'common', stars: '★',
|
|
141
|
-
face: '(°>', eye: '°', shiny: false, hat: 'none', reaction: '',
|
|
142
|
-
muted: false, achievement: '', frames, frameSequence,
|
|
143
|
-
level: 1, xp: 0, mood: 'focused',
|
|
144
|
-
};
|
|
145
|
-
writeFileSync(process.argv[1], JSON.stringify(status));
|
|
146
|
-
writeFileSync(process.argv[2], JSON.stringify(status));
|
|
147
|
-
writeFileSync(process.argv[3], JSON.stringify({
|
|
140
|
+
writeFileSync(process.argv[1], JSON.stringify({
|
|
148
141
|
reaction: '编译通过了 你好世界', timestamp: Date.now(), reason: 'turn',
|
|
149
142
|
}));
|
|
150
|
-
writeFileSync(process.argv[
|
|
143
|
+
writeFileSync(process.argv[2], JSON.stringify({
|
|
151
144
|
reaction: 'ship it 🎉 ❤️ ✨', timestamp: Date.now(), reason: 'success',
|
|
152
145
|
}));
|
|
153
146
|
" \
|
|
154
|
-
"$gen_dir/cjk/buddy-state/status.json" \
|
|
155
|
-
"$gen_dir/emoji/buddy-state/status.json" \
|
|
156
147
|
"$gen_dir/cjk/buddy-state/reaction.default.json" \
|
|
157
148
|
"$gen_dir/emoji/buddy-state/reaction.default.json"
|
|
158
149
|
|
|
@@ -3,7 +3,33 @@ Set Width 1400
|
|
|
3
3
|
Set Height 500
|
|
4
4
|
Set FontSize 14
|
|
5
5
|
Set Shell "bash"
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
# Generate temporary full/compact/minimal fixtures from the committed baseline
|
|
8
|
+
# without touching production fixtures. The helper augments status.json with
|
|
9
|
+
# compactFrames/minimalFrames when getStatusFrames() emits them.
|
|
10
|
+
Hide
|
|
11
|
+
Type "bash scripts/ci/vhs-density-fixtures.sh"
|
|
7
12
|
Enter
|
|
8
|
-
Sleep
|
|
13
|
+
Sleep 2s
|
|
14
|
+
Show
|
|
15
|
+
|
|
16
|
+
Type "clear"
|
|
17
|
+
Enter
|
|
18
|
+
Sleep 500ms
|
|
19
|
+
|
|
20
|
+
# Full density: 50 rows gives the six-ish-line art + optional bubble.
|
|
21
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/full"
|
|
22
|
+
Enter
|
|
23
|
+
Sleep 2s
|
|
24
|
+
|
|
25
|
+
# Compact density: three art rows + name, no bubble.
|
|
26
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/compact"
|
|
27
|
+
Enter
|
|
28
|
+
Sleep 2s
|
|
29
|
+
|
|
30
|
+
# Minimal density: a single sprite/face + name line.
|
|
31
|
+
Type "COLUMNS=80 BUDDY_STATUSLINE_ROWS=50 bash scripts/ci/vhs-run-statusline.sh 80 scripts/ci/vhs-out/density-fixtures/minimal"
|
|
32
|
+
Enter
|
|
33
|
+
Sleep 2s
|
|
34
|
+
|
|
9
35
|
Screenshot scripts/ci/statusline.png
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Generate temporary full/compact/minimal statusline fixtures for the VHS tape.
|
|
3
|
+
# Leaves the committed production fixture untouched and writes into the ignored
|
|
4
|
+
# scripts/ci/vhs-out/ workspace.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
cd "$ROOT"
|
|
9
|
+
|
|
10
|
+
src_status="$ROOT/scripts/ci/statusline-fixture/buddy-state/status.json"
|
|
11
|
+
out_dir="$ROOT/scripts/ci/vhs-out/density-fixtures"
|
|
12
|
+
|
|
13
|
+
rm -rf "$out_dir"
|
|
14
|
+
mkdir -p "$out_dir"/{full,compact,minimal}/buddy-state
|
|
15
|
+
|
|
16
|
+
for d in full compact minimal; do
|
|
17
|
+
cp "$src_status" "$out_dir/$d/buddy-state/status.json"
|
|
18
|
+
printf '{"useCombinedStatus":false,"statuslineDensity":"%s"}\n' "$d" \
|
|
19
|
+
> "$out_dir/$d/buddy-state/config.json"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# Augment the committed fixture with full/compact/minimal frame arrays.
|
|
23
|
+
cat "$src_status" | bun "$ROOT/scripts/ci/write-density-status.ts" "$out_dir/full/buddy-state/status.json"
|
|
24
|
+
cp "$out_dir/full/buddy-state/status.json" "$out_dir/compact/buddy-state/status.json"
|
|
25
|
+
cp "$out_dir/full/buddy-state/status.json" "$out_dir/minimal/buddy-state/status.json"
|
|
26
|
+
|
|
27
|
+
echo "$out_dir"
|