@windyroad/retrospective 0.16.0 → 0.17.0-preview.277

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.
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env bats
2
+ #
3
+ # packages/retrospective/scripts/test/check-readme-jtbd-currency.bats
4
+ #
5
+ # Behavioural tests for `check-readme-jtbd-currency.sh` — the JTBD-anchored
6
+ # README drift advisory (ADR-051 / P152 Phase 1). Mirrors the fixture-based
7
+ # pattern of sibling detectors (`check-tickets-deferred-cause.bats`,
8
+ # `check-briefing-budgets.bats`, `check-ask-hygiene.bats`).
9
+ #
10
+ # Tests are behavioural per ADR-005 / ADR-037 / P081 — they exercise the
11
+ # script end-to-end against fixture packages/ + docs/jtbd/ trees and assert
12
+ # on stdout / exit code shape. No structural greps of the script source.
13
+ #
14
+ # @problem P152 (No pressure or nudge for documentation currency)
15
+ # @problem P081 (Structural-content tests are wasteful — behavioural preferred)
16
+ # @adr ADR-051 (JTBD-anchored README rule with declarative drift advisory)
17
+ # @adr ADR-013 Rule 6 (non-interactive fail-safe)
18
+ # @adr ADR-005 / ADR-037 (Plugin testing strategy — behavioural tests)
19
+ # @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just Installed)
20
+ # @jtbd JTBD-007 (Keep Plugins Current Across Projects — currency expansion)
21
+
22
+ SCRIPT="${BATS_TEST_DIRNAME}/../check-readme-jtbd-currency.sh"
23
+
24
+ setup() {
25
+ TEST_DIR="$(mktemp -d)"
26
+ PKG_DIR="$TEST_DIR/packages"
27
+ JTBD_DIR="$TEST_DIR/docs/jtbd"
28
+ mkdir -p "$PKG_DIR" "$JTBD_DIR/plugin-user" "$JTBD_DIR/solo-developer"
29
+ }
30
+
31
+ teardown() {
32
+ rm -rf "$TEST_DIR"
33
+ }
34
+
35
+ # Helper: write a synthetic plugin layout into PKG_DIR
36
+ make_plugin() {
37
+ local name="$1"
38
+ local readme_content="$2"
39
+ mkdir -p "$PKG_DIR/$name"
40
+ printf '%s\n' "$readme_content" > "$PKG_DIR/$name/README.md"
41
+ }
42
+
43
+ # Helper: write a synthetic JTBD job file into JTBD_DIR
44
+ make_jtbd() {
45
+ local persona="$1"
46
+ local id="$2"
47
+ local slug="$3"
48
+ local status="$4"
49
+ mkdir -p "$JTBD_DIR/$persona"
50
+ cat > "$JTBD_DIR/$persona/$id-$slug.$status.md" <<EOF
51
+ ---
52
+ status: $status
53
+ job-id: $slug
54
+ persona: $persona
55
+ date-created: 2026-05-03
56
+ ---
57
+
58
+ # $id: stub job for fixture
59
+ EOF
60
+ }
61
+
62
+ # ── Pre-checks ──────────────────────────────────────────────────────────────
63
+
64
+ @test "script file exists and is executable" {
65
+ [ -f "$SCRIPT" ]
66
+ [ -x "$SCRIPT" ]
67
+ }
68
+
69
+ @test "missing packages dir exits 2 with error message on stderr" {
70
+ run bash "$SCRIPT" "$TEST_DIR/does-not-exist" "$JTBD_DIR"
71
+ [ "$status" -eq 2 ]
72
+ [[ "$output" == *"packages dir not found"* ]]
73
+ }
74
+
75
+ @test "missing jtbd dir exits 2 with error message on stderr" {
76
+ run bash "$SCRIPT" "$PKG_DIR" "$TEST_DIR/does-not-exist"
77
+ [ "$status" -eq 2 ]
78
+ [[ "$output" == *"jtbd dir not found"* ]]
79
+ }
80
+
81
+ @test "empty packages dir exits 0 with empty stdout" {
82
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
83
+ [ "$status" -eq 0 ]
84
+ [ -z "$output" ]
85
+ }
86
+
87
+ # ── Drift fixture: README has no JTBD citation ──────────────────────────────
88
+
89
+ @test "drift fixture: README with no JTBD-NNN cite emits has_jtbd_anchor=no + missing-jtbd-section drift hint" {
90
+ make_plugin "stub" "# @windyroad/stub
91
+ This README documents nothing about JTBD."
92
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
93
+
94
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
95
+ [ "$status" -eq 0 ]
96
+ [[ "$output" == *"package=stub"* ]]
97
+ [[ "$output" == *"has_jtbd_anchor=no"* ]]
98
+ [[ "$output" == *"cited_jobs=0"* ]]
99
+ [[ "$output" == *"drift_hints="*"missing-jtbd-section"* ]]
100
+ [[ "$output" == *"TOTAL packages=1 with_jtbd=0 drift_instances=1"* ]]
101
+ }
102
+
103
+ # ── Clean fixture: README cites a current JTBD ID ───────────────────────────
104
+
105
+ @test "clean fixture: README citing a resolving JTBD-NNN emits has_jtbd_anchor=yes with empty drift_hints" {
106
+ make_plugin "stub" "# @windyroad/stub
107
+ This plugin serves JTBD-302 and JTBD-007."
108
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
109
+ make_jtbd "solo-developer" "JTBD-007" "keep-plugins-current" "proposed"
110
+
111
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
112
+ [ "$status" -eq 0 ]
113
+ [[ "$output" == *"package=stub"* ]]
114
+ [[ "$output" == *"has_jtbd_anchor=yes"* ]]
115
+ [[ "$output" == *"cited_jobs=2"* ]]
116
+ [[ "$output" == *"known_jobs=2"* ]]
117
+ [[ "$output" == *"drift_hints="$'\n'* || "$output" == *"drift_hints= "* || "$output" == *"drift_hints="*$'\n'* ]]
118
+ [[ "$output" == *"TOTAL packages=1 with_jtbd=1 drift_instances=0"* ]]
119
+ }
120
+
121
+ # ── Stale-ID fixture: cited JTBD does not resolve ───────────────────────────
122
+
123
+ @test "stale-ID fixture: README citing JTBD-NNN with no resolving file emits stale-jtbd-citation hint" {
124
+ make_plugin "stub" "# @windyroad/stub
125
+ This plugin serves JTBD-999 (which doesn't exist)."
126
+ # No JTBD-999 file; only JTBD-302 exists in the fixture
127
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
128
+
129
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
130
+ [ "$status" -eq 0 ]
131
+ [[ "$output" == *"package=stub"* ]]
132
+ [[ "$output" == *"has_jtbd_anchor=yes"* ]]
133
+ [[ "$output" == *"cited_jobs=1"* ]]
134
+ [[ "$output" == *"known_jobs=0"* ]]
135
+ [[ "$output" == *"stale-jtbd-citation"* ]]
136
+ [[ "$output" == *"TOTAL packages=1 with_jtbd=1 drift_instances=1"* ]]
137
+ }
138
+
139
+ # ── Deprecated-only fixture: cited JTBD resolves only to deprecated ─────────
140
+
141
+ @test "deprecated-only fixture: cited JTBD resolves to .deprecated.md emits deprecated-jtbd-citation hint" {
142
+ make_plugin "stub" "# @windyroad/stub
143
+ This plugin serves JTBD-888."
144
+ make_jtbd "plugin-user" "JTBD-888" "old-job" "deprecated"
145
+
146
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
147
+ [ "$status" -eq 0 ]
148
+ [[ "$output" == *"package=stub"* ]]
149
+ [[ "$output" == *"has_jtbd_anchor=yes"* ]]
150
+ [[ "$output" == *"cited_jobs=1"* ]]
151
+ [[ "$output" == *"known_jobs=1"* ]]
152
+ [[ "$output" == *"deprecated-jtbd-citation"* ]]
153
+ [[ "$output" == *"drift_instances=1"* ]]
154
+ }
155
+
156
+ # ── Skill-inventory-drift fixture ───────────────────────────────────────────
157
+
158
+ @test "skill-inventory-drift fixture: skills/ directory not named in README emits skill-inventory-drift hint" {
159
+ mkdir -p "$PKG_DIR/stub/skills/orphan-widget"
160
+ printf '%s\n' "# @windyroad/stub
161
+ This plugin serves JTBD-302." > "$PKG_DIR/stub/README.md"
162
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
163
+
164
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
165
+ [ "$status" -eq 0 ]
166
+ [[ "$output" == *"package=stub"* ]]
167
+ [[ "$output" == *"skill-inventory-drift"* ]]
168
+ [[ "$output" == *"drift_instances=1"* ]]
169
+ }
170
+
171
+ @test "skill-inventory-drift NOT flagged when skills/ directories are all named in README" {
172
+ mkdir -p "$PKG_DIR/stub/skills/manage-secret"
173
+ printf '%s\n' "# @windyroad/stub
174
+ This plugin serves JTBD-302 via the manage-secret skill." > "$PKG_DIR/stub/README.md"
175
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
176
+
177
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
178
+ [ "$status" -eq 0 ]
179
+ [[ "$output" == *"package=stub"* ]]
180
+ [[ "$output" != *"skill-inventory-drift"* ]]
181
+ }
182
+
183
+ # ── Multi-package aggregation ───────────────────────────────────────────────
184
+
185
+ @test "multi-package aggregation: emits one README line per package + TOTAL summary" {
186
+ make_plugin "alpha" "# alpha
187
+ Serves JTBD-302."
188
+ make_plugin "bravo" "# bravo
189
+ No anchor here."
190
+ make_plugin "charlie" "# charlie
191
+ Serves JTBD-302 and JTBD-007."
192
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
193
+ make_jtbd "solo-developer" "JTBD-007" "keep-plugins-current" "proposed"
194
+
195
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
196
+ [ "$status" -eq 0 ]
197
+ [[ "$output" == *"package=alpha"* ]]
198
+ [[ "$output" == *"package=bravo"* ]]
199
+ [[ "$output" == *"package=charlie"* ]]
200
+ # bravo has missing-jtbd-section, alpha + charlie are clean
201
+ [[ "$output" == *"TOTAL packages=3 with_jtbd=2 drift_instances=1"* ]]
202
+ }
203
+
204
+ # ── Package without README is skipped ───────────────────────────────────────
205
+
206
+ @test "package without README.md is silently skipped" {
207
+ mkdir -p "$PKG_DIR/no-readme"
208
+ make_plugin "with-readme" "# with-readme
209
+ Serves JTBD-302."
210
+ make_jtbd "plugin-user" "JTBD-302" "trust-readme" "proposed"
211
+
212
+ run bash "$SCRIPT" "$PKG_DIR" "$JTBD_DIR"
213
+ [ "$status" -eq 0 ]
214
+ [[ "$output" != *"package=no-readme"* ]]
215
+ [[ "$output" == *"package=with-readme"* ]]
216
+ [[ "$output" == *"TOTAL packages=1"* ]]
217
+ }
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # @problem P097 — SKILL.md files mix runtime-necessary steps with maintainer-
4
+ # facing rationale, bloating every skill invocation. ADR-054 codifies the
5
+ # classification taxonomy ([runtime] / [reference] / [deprecated]), the
6
+ # sibling REFERENCE.md lazy-load pattern, and the byte budgets (WARN ≥
7
+ # 8192, MUST_SPLIT ≥ 16384). This bats fixture covers the advisory
8
+ # detector script that surfaces SKILL.md files exceeding the budget.
9
+ #
10
+ # Contract: `check-skill-md-budgets.sh [<root-dir>]` is a diagnose-only
11
+ # advisory script. It walks `<root-dir>/packages/*/skills/*/SKILL.md` and
12
+ # `<root-dir>/.claude/skills/*/SKILL.md` (default `<root-dir>` is `.`),
13
+ # measures byte size per file, and reports each SKILL.md whose size is at
14
+ # or above the WARN threshold (default 8192 bytes, overridable via
15
+ # SKILL_MD_WARN_BYTES env var).
16
+ #
17
+ # Exit codes:
18
+ # 0 = always (advisory only — overflow is signal, not failure)
19
+ # 2 = parse error (root dir missing or unreadable)
20
+ #
21
+ # Output format on overflow (one line per file, terse machine-readable
22
+ # per ADR-038 progressive-disclosure budget):
23
+ # OVER <plugin>/<skill> bytes=<N> threshold=<N>
24
+ #
25
+ # Files at >= MUST_SPLIT (default 16384, overridable via SKILL_MD_MUST_SPLIT_BYTES)
26
+ # also emit a second line:
27
+ # MUST_SPLIT <plugin>/<skill> reason=<code>
28
+ #
29
+ # This mirrors the OVER / MUST_SPLIT pair shape from `check-briefing-budgets.sh`
30
+ # (P099 / P145 / ADR-040) deliberately so adopters learn one concept across
31
+ # two surfaces.
32
+ #
33
+ # Output ordering (deterministic for stable retro-summary diffs):
34
+ # 1. All OVER lines, sorted by `<plugin>/<skill>` identifier.
35
+ # 2. Then all MUST_SPLIT lines, sorted by identifier.
36
+ #
37
+ # Output is empty (no lines) when no SKILL.md exceeds the WARN threshold.
38
+ # REFERENCE.md sibling files (per ADR-054) are excluded from the scan —
39
+ # they are intentionally lazy-loaded and not subject to the runtime budget.
40
+ #
41
+ # Read-only — does NOT mutate any SKILL.md file. Extraction priority is
42
+ # surfaced to the maintainer; rotation is opportunistic per ADR-052
43
+ # migration shape.
44
+ #
45
+ # This fixture is BEHAVIOURAL per ADR-052 — it asserts script output on
46
+ # temp-fixture skill trees, NOT script source content. No greps of
47
+ # check-skill-md-budgets.sh source.
48
+ #
49
+ # @jtbd JTBD-001 (Enforce Governance Without Slowing Down — solo dev;
50
+ # read-only, no interactive friction on the happy path)
51
+ # @jtbd JTBD-006 (Progress the Backlog While I'm Away — AFK-safe advisory)
52
+ # @jtbd JTBD-101 (Extend the Suite with Clear Patterns — reusable
53
+ # advisory-script + bats + ADR-amendment shape for context-budget surfaces)
54
+ #
55
+ # Cross-reference:
56
+ # P097: docs/problems/097-skill-md-runtime-size-mixes-policy-with-runtime-steps.*.md
57
+ # ADR-054 — SKILL.md runtime budget policy (taxonomy + sibling pattern + budget)
58
+ # ADR-040 — Session-start briefing surface (Tier 3 OVER / MUST_SPLIT precedent)
59
+ # ADR-038 — Progressive disclosure (per-row byte budget on diff output)
60
+ # ADR-052 — Behavioural-tests-default (this fixture's pattern)
61
+ # ADR-005 — Plugin testing strategy (script-level bats governance)
62
+
63
+ setup() {
64
+ SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
65
+ SCRIPT="$SCRIPTS_DIR/check-skill-md-budgets.sh"
66
+ FIXTURE_ROOT="$(mktemp -d)"
67
+ }
68
+
69
+ teardown() {
70
+ rm -rf "$FIXTURE_ROOT"
71
+ }
72
+
73
+ # Helper: write a SKILL.md with N bytes of body content under a fixture
74
+ # plugin/skill path. Creates the directory tree as needed.
75
+ write_skill() {
76
+ local plugin="$1"
77
+ local skill="$2"
78
+ local target_bytes="$3"
79
+ local skill_dir="$FIXTURE_ROOT/packages/$plugin/skills/$skill"
80
+ mkdir -p "$skill_dir"
81
+ local path="$skill_dir/SKILL.md"
82
+ : > "$path"
83
+ printf '# Skill\n\n' >> "$path"
84
+ local header_size
85
+ header_size=$(wc -c < "$path" | tr -d ' ')
86
+ local body_target=$(( target_bytes - header_size ))
87
+ if [ "$body_target" -gt 0 ]; then
88
+ local line="- step text padded out to a known length for byte-budget testing. "
89
+ local line_size=${#line}
90
+ line+=$'\n'
91
+ local line_count=$(( (body_target + line_size) / (line_size + 1) ))
92
+ local i=0
93
+ while [ "$i" -lt "$line_count" ]; do
94
+ printf '%s' "$line" >> "$path"
95
+ i=$(( i + 1 ))
96
+ done
97
+ fi
98
+ }
99
+
100
+ # Helper: write a project-local SKILL.md under .claude/skills/<skill>/SKILL.md
101
+ # (the project-local skill surface, in scope per ADR-054 §Phase 1 deliverable).
102
+ write_local_skill() {
103
+ local skill="$1"
104
+ local target_bytes="$2"
105
+ local skill_dir="$FIXTURE_ROOT/.claude/skills/$skill"
106
+ mkdir -p "$skill_dir"
107
+ local path="$skill_dir/SKILL.md"
108
+ : > "$path"
109
+ printf '# Skill\n\n' >> "$path"
110
+ local header_size
111
+ header_size=$(wc -c < "$path" | tr -d ' ')
112
+ local body_target=$(( target_bytes - header_size ))
113
+ if [ "$body_target" -gt 0 ]; then
114
+ local line="- step text padded out to a known length for byte-budget testing. "
115
+ local line_size=${#line}
116
+ line+=$'\n'
117
+ local line_count=$(( (body_target + line_size) / (line_size + 1) ))
118
+ local i=0
119
+ while [ "$i" -lt "$line_count" ]; do
120
+ printf '%s' "$line" >> "$path"
121
+ i=$(( i + 1 ))
122
+ done
123
+ fi
124
+ }
125
+
126
+ # ── Existence + executable ──────────────────────────────────────────────────
127
+
128
+ @test "check-skill-md-budgets: script exists" {
129
+ [ -f "$SCRIPT" ]
130
+ }
131
+
132
+ @test "check-skill-md-budgets: script is executable" {
133
+ [ -x "$SCRIPT" ]
134
+ }
135
+
136
+ # ── Default-threshold behaviour (WARN 8192, MUST_SPLIT 16384) ───────────────
137
+
138
+ @test "check-skill-md-budgets: empty tree produces no output and exits 0" {
139
+ mkdir -p "$FIXTURE_ROOT/packages"
140
+ run "$SCRIPT" "$FIXTURE_ROOT"
141
+ [ "$status" -eq 0 ]
142
+ [ -z "$output" ]
143
+ }
144
+
145
+ @test "check-skill-md-budgets: all skills under WARN produces no output" {
146
+ write_skill "alpha" "small" 1024
147
+ write_skill "beta" "medium" 4096
148
+ write_skill "gamma" "near-warn" 7000
149
+ run "$SCRIPT" "$FIXTURE_ROOT"
150
+ [ "$status" -eq 0 ]
151
+ [ -z "$output" ]
152
+ }
153
+
154
+ @test "check-skill-md-budgets: skill at WARN band emits OVER line with bytes + threshold" {
155
+ write_skill "alpha" "warn-band" 10000
156
+ run "$SCRIPT" "$FIXTURE_ROOT"
157
+ [ "$status" -eq 0 ]
158
+ echo "$output" | grep -E "^OVER alpha/warn-band bytes=[0-9]+ threshold=8192$"
159
+ }
160
+
161
+ @test "check-skill-md-budgets: skill exactly at WARN threshold emits OVER (>= boundary)" {
162
+ local skill_dir="$FIXTURE_ROOT/packages/alpha/skills/edge"
163
+ mkdir -p "$skill_dir"
164
+ printf '%.0s.' $(seq 1 8192) > "$skill_dir/SKILL.md"
165
+ run "$SCRIPT" "$FIXTURE_ROOT"
166
+ [ "$status" -eq 0 ]
167
+ echo "$output" | grep -E "^OVER alpha/edge bytes=8192 threshold=8192$"
168
+ }
169
+
170
+ @test "check-skill-md-budgets: skill at WARN band but under MUST_SPLIT emits OVER only (no MUST_SPLIT)" {
171
+ write_skill "alpha" "warn-only" 12000
172
+ run "$SCRIPT" "$FIXTURE_ROOT"
173
+ [ "$status" -eq 0 ]
174
+ echo "$output" | grep -E "^OVER alpha/warn-only bytes=[0-9]+ threshold=8192"
175
+ ! echo "$output" | grep -q "MUST_SPLIT"
176
+ }
177
+
178
+ @test "check-skill-md-budgets: skill at exactly MUST_SPLIT emits OVER + MUST_SPLIT" {
179
+ local skill_dir="$FIXTURE_ROOT/packages/alpha/skills/exactly-must-split"
180
+ mkdir -p "$skill_dir"
181
+ printf '%.0s.' $(seq 1 16384) > "$skill_dir/SKILL.md"
182
+ run "$SCRIPT" "$FIXTURE_ROOT"
183
+ [ "$status" -eq 0 ]
184
+ echo "$output" | grep -E "^OVER alpha/exactly-must-split bytes=16384 threshold=8192"
185
+ echo "$output" | grep -E "^MUST_SPLIT alpha/exactly-must-split reason=ratio-exceeds-must-split$"
186
+ }
187
+
188
+ @test "check-skill-md-budgets: skill well over MUST_SPLIT emits both lines" {
189
+ write_skill "alpha" "very-bloated" 80000
190
+ run "$SCRIPT" "$FIXTURE_ROOT"
191
+ [ "$status" -eq 0 ]
192
+ echo "$output" | grep -E "^OVER alpha/very-bloated bytes=[0-9]+ threshold=8192"
193
+ echo "$output" | grep -E "^MUST_SPLIT alpha/very-bloated reason=ratio-exceeds-must-split$"
194
+ }
195
+
196
+ @test "check-skill-md-budgets: only over-threshold skills appear in output" {
197
+ write_skill "alpha" "under" 2000
198
+ write_skill "beta" "over-warn" 10000
199
+ write_skill "gamma" "over-must-split" 20000
200
+ run "$SCRIPT" "$FIXTURE_ROOT"
201
+ [ "$status" -eq 0 ]
202
+ echo "$output" | grep -E "^OVER beta/over-warn bytes=[0-9]+ threshold=8192"
203
+ echo "$output" | grep -E "^OVER gamma/over-must-split bytes=[0-9]+ threshold=8192"
204
+ ! echo "$output" | grep -q "alpha/under"
205
+ }
206
+
207
+ # ── Sibling REFERENCE.md exclusion (ADR-054) ────────────────────────────────
208
+
209
+ @test "check-skill-md-budgets: REFERENCE.md sibling is excluded from the scan" {
210
+ write_skill "alpha" "with-ref" 1000
211
+ # Write a bloated REFERENCE.md that would otherwise trip MUST_SPLIT
212
+ printf '%.0s.' $(seq 1 50000) > "$FIXTURE_ROOT/packages/alpha/skills/with-ref/REFERENCE.md"
213
+ run "$SCRIPT" "$FIXTURE_ROOT"
214
+ [ "$status" -eq 0 ]
215
+ # No output — only SKILL.md is measured, not REFERENCE.md
216
+ [ -z "$output" ]
217
+ }
218
+
219
+ # ── Project-local .claude/skills discovery ──────────────────────────────────
220
+
221
+ @test "check-skill-md-budgets: .claude/skills/* SKILL.md files are scanned" {
222
+ write_local_skill "install-updates" 12000
223
+ run "$SCRIPT" "$FIXTURE_ROOT"
224
+ [ "$status" -eq 0 ]
225
+ # The project-local prefix is .claude in the identifier
226
+ echo "$output" | grep -E "^OVER .claude/install-updates bytes=[0-9]+ threshold=8192"
227
+ }
228
+
229
+ @test "check-skill-md-budgets: project-local + plugin-skill outputs both appear" {
230
+ write_skill "alpha" "plugin-skill" 10000
231
+ write_local_skill "local-skill" 12000
232
+ run "$SCRIPT" "$FIXTURE_ROOT"
233
+ [ "$status" -eq 0 ]
234
+ echo "$output" | grep -E "^OVER .claude/local-skill"
235
+ echo "$output" | grep -E "^OVER alpha/plugin-skill"
236
+ }
237
+
238
+ # ── Configurable thresholds via env vars ────────────────────────────────────
239
+
240
+ @test "check-skill-md-budgets: SKILL_MD_WARN_BYTES env var overrides default" {
241
+ write_skill "alpha" "small-by-default" 4096
242
+ # Default 8192: no output. With env var 2000: over threshold.
243
+ SKILL_MD_WARN_BYTES=2000 run "$SCRIPT" "$FIXTURE_ROOT"
244
+ [ "$status" -eq 0 ]
245
+ echo "$output" | grep -E "^OVER alpha/small-by-default bytes=[0-9]+ threshold=2000"
246
+ }
247
+
248
+ @test "check-skill-md-budgets: SKILL_MD_MUST_SPLIT_BYTES env var overrides default" {
249
+ write_skill "alpha" "moderate" 9000
250
+ # Default WARN 8192: emits OVER. Default MUST_SPLIT 16384: no MUST_SPLIT.
251
+ # With MUST_SPLIT override 8500: should emit MUST_SPLIT too.
252
+ SKILL_MD_MUST_SPLIT_BYTES=8500 run "$SCRIPT" "$FIXTURE_ROOT"
253
+ [ "$status" -eq 0 ]
254
+ echo "$output" | grep -E "^OVER alpha/moderate"
255
+ echo "$output" | grep -E "^MUST_SPLIT alpha/moderate reason=ratio-exceeds-must-split$"
256
+ }
257
+
258
+ @test "check-skill-md-budgets: env var threshold of 0 emits every skill (sanity)" {
259
+ write_skill "alpha" "tiny-one" 100
260
+ write_skill "beta" "tiny-two" 200
261
+ SKILL_MD_WARN_BYTES=0 run "$SCRIPT" "$FIXTURE_ROOT"
262
+ [ "$status" -eq 0 ]
263
+ echo "$output" | grep -q "OVER alpha/tiny-one"
264
+ echo "$output" | grep -q "OVER beta/tiny-two"
265
+ }
266
+
267
+ # ── Argument and error handling ─────────────────────────────────────────────
268
+
269
+ @test "check-skill-md-budgets: defaults to current directory when no arg provided" {
270
+ cd "$FIXTURE_ROOT"
271
+ write_skill "alpha" "default-arg" 12000
272
+ run "$SCRIPT"
273
+ [ "$status" -eq 0 ]
274
+ echo "$output" | grep -E "^OVER alpha/default-arg bytes=[0-9]+ threshold=8192"
275
+ }
276
+
277
+ @test "check-skill-md-budgets: missing root dir exits 2 with parse error on stderr" {
278
+ run "$SCRIPT" "$FIXTURE_ROOT/does-not-exist"
279
+ [ "$status" -eq 2 ]
280
+ echo "$output" | grep -iE "not found|missing|does not exist"
281
+ }
282
+
283
+ @test "check-skill-md-budgets: ignores non-SKILL.md files in skill dirs" {
284
+ local skill_dir="$FIXTURE_ROOT/packages/alpha/skills/with-extras"
285
+ mkdir -p "$skill_dir"
286
+ # Bloated non-SKILL.md files should not trip the scan
287
+ printf '%.0s.' $(seq 1 50000) > "$skill_dir/NOTES.md"
288
+ printf '%.0s.' $(seq 1 50000) > "$skill_dir/scratch.txt"
289
+ # Small SKILL.md under threshold
290
+ printf '%.0s.' $(seq 1 1000) > "$skill_dir/SKILL.md"
291
+ run "$SCRIPT" "$FIXTURE_ROOT"
292
+ [ "$status" -eq 0 ]
293
+ [ -z "$output" ]
294
+ }
295
+
296
+ # ── Output stability ────────────────────────────────────────────────────────
297
+
298
+ @test "check-skill-md-budgets: output is sorted by identifier for stable diffs" {
299
+ write_skill "zeta" "one" 10000
300
+ write_skill "alpha" "one" 10000
301
+ write_skill "middle" "one" 10000
302
+ run "$SCRIPT" "$FIXTURE_ROOT"
303
+ [ "$status" -eq 0 ]
304
+ first_line=$(echo "$output" | head -1)
305
+ last_line=$(echo "$output" | tail -1)
306
+ echo "$first_line" | grep -q "alpha/one"
307
+ echo "$last_line" | grep -q "zeta/one"
308
+ }
309
+
310
+ @test "check-skill-md-budgets: mixed OVER + MUST_SPLIT output uses block ordering" {
311
+ # Three OVER files; two of them also MUST_SPLIT. Output must be
312
+ # deterministic so retro summary diffs stay stable across cycles.
313
+ # Contract: OVER block (sorted by identifier) followed by MUST_SPLIT
314
+ # block (sorted by identifier).
315
+ write_skill "zeta" "over-only" 10000
316
+ write_skill "alpha" "must-split" 20000
317
+ write_skill "middle" "must-split" 25000
318
+ run "$SCRIPT" "$FIXTURE_ROOT"
319
+ [ "$status" -eq 0 ]
320
+ # Three OVER lines — alpha first, middle next, zeta last
321
+ over_lines=$(echo "$output" | grep "^OVER ")
322
+ [ "$(echo "$over_lines" | wc -l | tr -d ' ')" = "3" ]
323
+ echo "$over_lines" | sed -n '1p' | grep -q "alpha/must-split"
324
+ echo "$over_lines" | sed -n '2p' | grep -q "middle/must-split"
325
+ echo "$over_lines" | sed -n '3p' | grep -q "zeta/over-only"
326
+ # Two MUST_SPLIT lines — alpha first, middle second; zeta-over-only NOT present
327
+ must_lines=$(echo "$output" | grep "^MUST_SPLIT ")
328
+ [ "$(echo "$must_lines" | wc -l | tr -d ' ')" = "2" ]
329
+ echo "$must_lines" | sed -n '1p' | grep -q "alpha/must-split"
330
+ echo "$must_lines" | sed -n '2p' | grep -q "middle/must-split"
331
+ ! echo "$must_lines" | grep -q "zeta/over-only"
332
+ # All OVER lines come before any MUST_SPLIT line (block ordering)
333
+ first_must_line_no=$(echo "$output" | grep -n "^MUST_SPLIT " | head -1 | cut -d: -f1)
334
+ last_over_line_no=$(echo "$output" | grep -n "^OVER " | tail -1 | cut -d: -f1)
335
+ [ "$last_over_line_no" -lt "$first_must_line_no" ]
336
+ }