@windyroad/itil 0.30.2-preview.317 → 0.30.3-preview.319
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/.claude-plugin/plugin.json +1 -1
- package/bin/wr-itil-plugin-exercise-index +2 -0
- package/bin/wr-itil-skill-invocations +2 -0
- package/hooks/hooks.json +2 -1
- package/hooks/itil-mid-loop-ask-detect.sh +142 -0
- package/hooks/test/itil-mid-loop-ask-detect.bats +220 -0
- package/package.json +1 -1
- package/scripts/plugin-exercise-index.sh +347 -0
- package/scripts/skill-invocations.sh +255 -0
- package/scripts/test/plugin-exercise-index.bats +386 -0
- package/scripts/test/skill-invocations.bats +320 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P087 — Phase 2b git-axis script behavioural confirmation.
|
|
4
|
+
#
|
|
5
|
+
# Contract under test: `packages/itil/scripts/plugin-exercise-index.sh` runs
|
|
6
|
+
# `git log --since=<N>d --name-only --pretty=format:%H|%aI|%s` once at the
|
|
7
|
+
# project root, auto-discovers plugins by listing `packages/*/`, and emits
|
|
8
|
+
# one NDJSON record per plugin with the v1.0 schema fields per ADR-058
|
|
9
|
+
# §Script contracts (line 87-113). Exit code 0 always per ADR-013 Rule 6
|
|
10
|
+
# fail-safe (outside-git-repo, missing `packages/`, opt-out marker all hit
|
|
11
|
+
# the zero-records/stderr-comment path).
|
|
12
|
+
#
|
|
13
|
+
# Confirmation criteria 6-8 from ADR-058 §Confirmation are the load-bearing
|
|
14
|
+
# behavioural assertions in this file. Sibling to Phase 2a's
|
|
15
|
+
# `skill-invocations.bats` (criteria 1-5).
|
|
16
|
+
#
|
|
17
|
+
# @adr ADR-058 (Plugin maturity measurement mechanism)
|
|
18
|
+
# @adr ADR-049 (Shim grammar — `wr-itil-plugin-exercise-index` on $PATH)
|
|
19
|
+
# @adr ADR-035 (Privacy posture — opt-out marker, no network primitive,
|
|
20
|
+
# content sanitisation — commit subjects parsed only for `BREAKING|feat!|fix!`
|
|
21
|
+
# token presence, never echoed to stdout)
|
|
22
|
+
# @adr ADR-052 (Behavioural tests default — NDJSON-output-driven against
|
|
23
|
+
# fixture git repos, not source-greps on script body; the no-network
|
|
24
|
+
# negative-grep at Confirmation #3 is the documented carve-out)
|
|
25
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always)
|
|
26
|
+
# @jtbd JTBD-101 (Extend the Suite — hardening-prioritisation outcome,
|
|
27
|
+
# commits_window + closed_tickets_window + days_shipped serve the
|
|
28
|
+
# git-axis half of the 2026-05-04 outcome amendment)
|
|
29
|
+
# @jtbd JTBD-201 (Restore Service Fast — audit-trail composition)
|
|
30
|
+
|
|
31
|
+
setup() {
|
|
32
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
33
|
+
SCRIPT="$SCRIPTS_DIR/plugin-exercise-index.sh"
|
|
34
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
35
|
+
REPO_ROOT="$FIXTURE_DIR/repo"
|
|
36
|
+
mkdir -p "$REPO_ROOT"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
teardown() {
|
|
40
|
+
rm -rf "$FIXTURE_DIR"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Helper: initialise a temp git repo at REPO_ROOT with a stable author/email
|
|
44
|
+
# (otherwise `git commit` aborts on systems without user.name configured).
|
|
45
|
+
init_repo() {
|
|
46
|
+
(
|
|
47
|
+
cd "$REPO_ROOT"
|
|
48
|
+
git init -q -b main
|
|
49
|
+
git config user.email "bats@example.com"
|
|
50
|
+
git config user.name "bats"
|
|
51
|
+
git config commit.gpgsign false
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Helper: stage a file under packages/<plugin>/ and commit at a given date.
|
|
56
|
+
# Date is ISO 8601 (e.g. 2026-05-01T12:00:00) — fed to GIT_AUTHOR_DATE /
|
|
57
|
+
# GIT_COMMITTER_DATE so log --since works deterministically.
|
|
58
|
+
commit_under_plugin() {
|
|
59
|
+
local plugin="$1"; local relpath="$2"; local subject="$3"; local date="$4"
|
|
60
|
+
local full="$REPO_ROOT/packages/$plugin/$relpath"
|
|
61
|
+
mkdir -p "$(dirname "$full")"
|
|
62
|
+
echo "content-$RANDOM" >> "$full"
|
|
63
|
+
(
|
|
64
|
+
cd "$REPO_ROOT"
|
|
65
|
+
GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
|
|
66
|
+
git -c user.email=bats@example.com -c user.name=bats \
|
|
67
|
+
add "packages/$plugin/$relpath" >/dev/null
|
|
68
|
+
GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
|
|
69
|
+
git -c user.email=bats@example.com -c user.name=bats \
|
|
70
|
+
commit -q -m "$subject"
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Helper: ISO timestamp N days ago (UTC). Cross-platform — uses python3.
|
|
75
|
+
days_ago_iso() {
|
|
76
|
+
python3 -c "import sys, datetime; print((datetime.datetime.utcnow() - datetime.timedelta(days=int(sys.argv[1]))).strftime('%Y-%m-%dT%H:%M:%S'))" "$1"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ── Existence / executable ──────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@test "plugin-exercise-index: canonical script exists" {
|
|
82
|
+
[ -f "$SCRIPT" ]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@test "plugin-exercise-index: canonical script is executable" {
|
|
86
|
+
[ -x "$SCRIPT" ]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@test "plugin-exercise-index: shim file exists with ADR-049 grammar" {
|
|
90
|
+
local shim="$SCRIPTS_DIR/../bin/wr-itil-plugin-exercise-index"
|
|
91
|
+
[ -f "$shim" ]
|
|
92
|
+
[ -x "$shim" ]
|
|
93
|
+
grep -q 'exec.*scripts/plugin-exercise-index.sh' "$shim"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ── Confirmation #6: git-axis composite fixture ─────────────────────────────
|
|
97
|
+
# Seed a temp git repo with packages/dummy/ + three commits (one in window,
|
|
98
|
+
# two out of window). Assert NDJSON record for plugin="dummy" has
|
|
99
|
+
# commits_window=1 under the 60-day default window.
|
|
100
|
+
|
|
101
|
+
@test "Confirmation #6: three commits, one in window, commits_window=1" {
|
|
102
|
+
init_repo
|
|
103
|
+
local in_window=$(days_ago_iso 10)
|
|
104
|
+
local out_window_1=$(days_ago_iso 90)
|
|
105
|
+
local out_window_2=$(days_ago_iso 120)
|
|
106
|
+
commit_under_plugin "dummy" "src/a.txt" "feat: in-window change" "$in_window"
|
|
107
|
+
commit_under_plugin "dummy" "src/b.txt" "feat: out-of-window 1" "$out_window_1"
|
|
108
|
+
commit_under_plugin "dummy" "src/c.txt" "feat: out-of-window 2" "$out_window_2"
|
|
109
|
+
|
|
110
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
111
|
+
[ "$status" -eq 0 ]
|
|
112
|
+
# Exactly one NDJSON record for plugin="dummy"
|
|
113
|
+
local rec
|
|
114
|
+
rec="$(echo "$output" | grep '"plugin":"dummy"')"
|
|
115
|
+
[ -n "$rec" ]
|
|
116
|
+
echo "$rec" | python3 -c "
|
|
117
|
+
import json, sys
|
|
118
|
+
r = json.loads(sys.stdin.read().strip())
|
|
119
|
+
assert r['schema_version'] == '1.0', r
|
|
120
|
+
assert r['axis'] == 'plugin-exercise-index', r
|
|
121
|
+
assert r['plugin'] == 'dummy', r
|
|
122
|
+
assert r['commits_window'] == 1, r
|
|
123
|
+
assert r['window_days'] == 60, r
|
|
124
|
+
assert r['days_shipped'] >= 120, r
|
|
125
|
+
"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "Confirmation #6: emits one record per discovered plugin" {
|
|
129
|
+
init_repo
|
|
130
|
+
local ts=$(days_ago_iso 5)
|
|
131
|
+
commit_under_plugin "alpha" "src/a.txt" "feat: alpha change" "$ts"
|
|
132
|
+
commit_under_plugin "beta" "src/b.txt" "feat: beta change" "$ts"
|
|
133
|
+
|
|
134
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
135
|
+
[ "$status" -eq 0 ]
|
|
136
|
+
echo "$output" | grep -q '"plugin":"alpha"'
|
|
137
|
+
echo "$output" | grep -q '"plugin":"beta"'
|
|
138
|
+
# Line count = number of discovered plugins (alpha + beta)
|
|
139
|
+
local line_count
|
|
140
|
+
line_count="$(printf '%s' "$output" | grep -c .)"
|
|
141
|
+
[ "$line_count" -eq 2 ]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "Confirmation #6: commit subject containing literal pipe parses correctly" {
|
|
145
|
+
# ADR-058 line 89 pins `--pretty=format:%H|%aI|%s` — the parser must
|
|
146
|
+
# split on first two `|` only (line.split('|', 2)) so subjects with
|
|
147
|
+
# literal `|` characters do not corrupt parsing. Architect advisory.
|
|
148
|
+
init_repo
|
|
149
|
+
local ts=$(days_ago_iso 5)
|
|
150
|
+
commit_under_plugin "pipey" "src/a.txt" "feat: subject with | pipe in it" "$ts"
|
|
151
|
+
|
|
152
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
153
|
+
[ "$status" -eq 0 ]
|
|
154
|
+
echo "$output" | grep -q '"plugin":"pipey"'
|
|
155
|
+
echo "$output" | grep -q '"commits_window":1'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ── Confirmation #7: outside-git-repo fixture ───────────────────────────────
|
|
159
|
+
|
|
160
|
+
@test "Confirmation #7: outside-git-repo emits zero records, stderr comment, exit 0" {
|
|
161
|
+
# REPO_ROOT exists but no `git init` ran — it is not a git repo.
|
|
162
|
+
local stdout_file="$FIXTURE_DIR/nogit.out"
|
|
163
|
+
local stderr_file="$FIXTURE_DIR/nogit.err"
|
|
164
|
+
"$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
|
|
165
|
+
local rc=$?
|
|
166
|
+
[ "$rc" -eq 0 ]
|
|
167
|
+
[ ! -s "$stdout_file" ]
|
|
168
|
+
grep -q "not a git repository" "$stderr_file"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@test "Confirmation #7: missing packages/ directory emits zero records, exit 0" {
|
|
172
|
+
init_repo
|
|
173
|
+
# Repo has no packages/ directory at all. Make a non-packages commit so
|
|
174
|
+
# `git log` returns something but no plugins are discovered.
|
|
175
|
+
(
|
|
176
|
+
cd "$REPO_ROOT"
|
|
177
|
+
echo readme > README.md
|
|
178
|
+
GIT_AUTHOR_DATE="$(days_ago_iso 5)" GIT_COMMITTER_DATE="$(days_ago_iso 5)" \
|
|
179
|
+
git -c user.email=bats@example.com -c user.name=bats add README.md >/dev/null
|
|
180
|
+
GIT_AUTHOR_DATE="$(days_ago_iso 5)" GIT_COMMITTER_DATE="$(days_ago_iso 5)" \
|
|
181
|
+
git -c user.email=bats@example.com -c user.name=bats commit -q -m "feat: readme"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
local stdout_file="$FIXTURE_DIR/nopkg.out"
|
|
185
|
+
local stderr_file="$FIXTURE_DIR/nopkg.err"
|
|
186
|
+
"$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
|
|
187
|
+
local rc=$?
|
|
188
|
+
[ "$rc" -eq 0 ]
|
|
189
|
+
[ ! -s "$stdout_file" ]
|
|
190
|
+
grep -q "no packages/ directory" "$stderr_file"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# ── Confirmation #8: schema-version contract ────────────────────────────────
|
|
194
|
+
|
|
195
|
+
@test "Confirmation #8: every NDJSON record has schema_version=1.0" {
|
|
196
|
+
init_repo
|
|
197
|
+
local ts=$(days_ago_iso 5)
|
|
198
|
+
commit_under_plugin "p1" "src/a.txt" "feat: p1" "$ts"
|
|
199
|
+
commit_under_plugin "p2" "src/b.txt" "feat: p2" "$ts"
|
|
200
|
+
|
|
201
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
202
|
+
[ "$status" -eq 0 ]
|
|
203
|
+
# Validate every line carries schema_version="1.0"
|
|
204
|
+
echo "$output" | python3 -c "
|
|
205
|
+
import json, sys
|
|
206
|
+
for line in sys.stdin:
|
|
207
|
+
line = line.strip()
|
|
208
|
+
if not line: continue
|
|
209
|
+
r = json.loads(line)
|
|
210
|
+
assert r['schema_version'] == '1.0', r
|
|
211
|
+
assert r['axis'] == 'plugin-exercise-index', r
|
|
212
|
+
"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# ── Privacy: opt-out marker ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
@test "opt-out marker disables reads, stderr comment, exit 0" {
|
|
218
|
+
init_repo
|
|
219
|
+
local ts=$(days_ago_iso 5)
|
|
220
|
+
commit_under_plugin "x" "src/a.txt" "feat: would-be-recorded" "$ts"
|
|
221
|
+
mkdir -p "$REPO_ROOT/.claude"
|
|
222
|
+
touch "$REPO_ROOT/.claude/.skill-metrics-opt-out"
|
|
223
|
+
|
|
224
|
+
local stdout_file="$FIXTURE_DIR/opt.out"
|
|
225
|
+
local stderr_file="$FIXTURE_DIR/opt.err"
|
|
226
|
+
"$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
|
|
227
|
+
local rc=$?
|
|
228
|
+
[ "$rc" -eq 0 ]
|
|
229
|
+
[ ! -s "$stdout_file" ]
|
|
230
|
+
grep -q "opt-out marker present at" "$stderr_file"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# ── Privacy: no-network-primitive (ADR-052 negative-grep carve-out) ─────────
|
|
234
|
+
|
|
235
|
+
@test "canonical body contains no network primitives" {
|
|
236
|
+
run grep -nE '\bcurl\b|\bwget\b|\bnc[[:space:]]|\bfetch\b|http\.client|\burllib\b' "$SCRIPT"
|
|
237
|
+
[ "$status" -eq 1 ]
|
|
238
|
+
[ -z "$output" ]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# ── Privacy: commit-subject prose never leaks beyond breaking-marker test ───
|
|
242
|
+
|
|
243
|
+
@test "commit subjects never appear in NDJSON output beyond BREAKING/feat!/fix! parse" {
|
|
244
|
+
init_repo
|
|
245
|
+
local ts=$(days_ago_iso 5)
|
|
246
|
+
# Subject contains a recognisable token that must NOT echo to stdout.
|
|
247
|
+
commit_under_plugin "secrety" "src/a.txt" "feat: contains SECRETPROSEXYZ token" "$ts"
|
|
248
|
+
|
|
249
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
250
|
+
[ "$status" -eq 0 ]
|
|
251
|
+
echo "$output" | grep -q '"plugin":"secrety"'
|
|
252
|
+
! echo "$output" | grep -q "SECRETPROSEXYZ"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# ── composite_index calculation ─────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
@test "composite_index = log10(commits+1) + log10(closed+1) + days_shipped_bonus" {
|
|
258
|
+
init_repo
|
|
259
|
+
# Set the OLDEST commit > 60 days ago to earn the days_shipped >= 60 bonus
|
|
260
|
+
# of +1.0. Then add 9 in-window commits so log10(9+1) = 1.0.
|
|
261
|
+
commit_under_plugin "calc" "src/old.txt" "feat: old" "$(days_ago_iso 90)"
|
|
262
|
+
for i in 1 2 3 4 5 6 7 8 9; do
|
|
263
|
+
commit_under_plugin "calc" "src/n$i.txt" "feat: n$i" "$(days_ago_iso 5)"
|
|
264
|
+
done
|
|
265
|
+
|
|
266
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
267
|
+
[ "$status" -eq 0 ]
|
|
268
|
+
# commits_window=9 (only the 9 recent ones), days_shipped >= 60 (90 days
|
|
269
|
+
# since oldest), closed_tickets_window=0 (no docs/problems/).
|
|
270
|
+
# composite_index = log10(10) + log10(1) + 1.0 = 1.0 + 0.0 + 1.0 = 2.0
|
|
271
|
+
echo "$output" | grep '"plugin":"calc"' | python3 -c "
|
|
272
|
+
import json, sys
|
|
273
|
+
r = json.loads(sys.stdin.read().strip())
|
|
274
|
+
assert r['commits_window'] == 9, r
|
|
275
|
+
assert r['days_shipped'] >= 60, r
|
|
276
|
+
assert r['closed_tickets_window'] == 0, r
|
|
277
|
+
assert abs(r['composite_index'] - 2.0) < 0.01, r
|
|
278
|
+
"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@test "composite_index respects days_shipped < 60 (no bonus)" {
|
|
282
|
+
init_repo
|
|
283
|
+
# All commits within last 30 days; days_shipped < 60.
|
|
284
|
+
for i in 1 2 3 4 5 6 7 8 9; do
|
|
285
|
+
commit_under_plugin "young" "src/n$i.txt" "feat: n$i" "$(days_ago_iso 5)"
|
|
286
|
+
done
|
|
287
|
+
|
|
288
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
289
|
+
[ "$status" -eq 0 ]
|
|
290
|
+
# commits_window=9, days_shipped=5, closed_tickets_window=0
|
|
291
|
+
# composite_index = log10(10) + log10(1) + 0.0 = 1.0
|
|
292
|
+
echo "$output" | grep '"plugin":"young"' | python3 -c "
|
|
293
|
+
import json, sys
|
|
294
|
+
r = json.loads(sys.stdin.read().strip())
|
|
295
|
+
assert r['commits_window'] == 9, r
|
|
296
|
+
assert r['days_shipped'] < 60, r
|
|
297
|
+
assert abs(r['composite_index'] - 1.0) < 0.01, r
|
|
298
|
+
"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# ── closed_tickets_window ──────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
@test "closed_tickets_window counts .closed/.verifying tickets citing the plugin" {
|
|
304
|
+
init_repo
|
|
305
|
+
local ts=$(days_ago_iso 5)
|
|
306
|
+
commit_under_plugin "cited" "src/a.txt" "feat: cited" "$ts"
|
|
307
|
+
# Seed two closed tickets in docs/problems/ — one cites packages/cited/,
|
|
308
|
+
# one does not. Both have recent mtime so the 90-day window includes them.
|
|
309
|
+
mkdir -p "$REPO_ROOT/docs/problems"
|
|
310
|
+
echo "## Related
|
|
311
|
+
- packages/cited/scripts/foo.sh" > "$REPO_ROOT/docs/problems/100-something.closed.md"
|
|
312
|
+
echo "## Related
|
|
313
|
+
- packages/other/scripts/bar.sh" > "$REPO_ROOT/docs/problems/101-other.closed.md"
|
|
314
|
+
|
|
315
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
316
|
+
[ "$status" -eq 0 ]
|
|
317
|
+
echo "$output" | grep '"plugin":"cited"' | python3 -c "
|
|
318
|
+
import json, sys
|
|
319
|
+
r = json.loads(sys.stdin.read().strip())
|
|
320
|
+
assert r['closed_tickets_window'] == 1, r
|
|
321
|
+
"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# ── breaking_change_age_days ───────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
@test "breaking_change_age_days surfaces BREAKING/feat!/fix! marker presence" {
|
|
327
|
+
init_repo
|
|
328
|
+
local recent=$(days_ago_iso 3)
|
|
329
|
+
local older=$(days_ago_iso 20)
|
|
330
|
+
commit_under_plugin "brk" "src/a.txt" "feat: normal change" "$older"
|
|
331
|
+
commit_under_plugin "brk" "src/b.txt" "feat!: breaking change" "$recent"
|
|
332
|
+
|
|
333
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
334
|
+
[ "$status" -eq 0 ]
|
|
335
|
+
echo "$output" | grep '"plugin":"brk"' | python3 -c "
|
|
336
|
+
import json, sys
|
|
337
|
+
r = json.loads(sys.stdin.read().strip())
|
|
338
|
+
v = r['breaking_change_age_days']
|
|
339
|
+
assert v is not None, r
|
|
340
|
+
assert 0 <= v <= 7, r
|
|
341
|
+
"
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
@test "breaking_change_age_days is null when no breaking marker in window" {
|
|
345
|
+
init_repo
|
|
346
|
+
local ts=$(days_ago_iso 5)
|
|
347
|
+
commit_under_plugin "nobreak" "src/a.txt" "feat: ordinary" "$ts"
|
|
348
|
+
commit_under_plugin "nobreak" "src/b.txt" "fix: ordinary" "$ts"
|
|
349
|
+
|
|
350
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
|
|
351
|
+
[ "$status" -eq 0 ]
|
|
352
|
+
echo "$output" | grep '"plugin":"nobreak"' | python3 -c "
|
|
353
|
+
import json, sys
|
|
354
|
+
r = json.loads(sys.stdin.read().strip())
|
|
355
|
+
assert r['breaking_change_age_days'] is None, r
|
|
356
|
+
"
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# ── Forward-extension flag: --category-overrides ────────────────────────────
|
|
360
|
+
|
|
361
|
+
@test "category-overrides flag is accepted without functional effect" {
|
|
362
|
+
init_repo
|
|
363
|
+
commit_under_plugin "cat" "src/a.txt" "feat: cat" "$(days_ago_iso 5)"
|
|
364
|
+
echo '{}' > "$FIXTURE_DIR/overrides.json"
|
|
365
|
+
|
|
366
|
+
run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" --category-overrides="$FIXTURE_DIR/overrides.json"
|
|
367
|
+
[ "$status" -eq 0 ]
|
|
368
|
+
echo "$output" | grep -q '"plugin":"cat"'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# ── Window-days filter ──────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
@test "window-days filter excludes commits older than the window" {
|
|
374
|
+
init_repo
|
|
375
|
+
commit_under_plugin "win" "src/old.txt" "feat: old" "$(days_ago_iso 30)"
|
|
376
|
+
commit_under_plugin "win" "src/new.txt" "feat: new" "$(days_ago_iso 1)"
|
|
377
|
+
|
|
378
|
+
run "$SCRIPT" --window-days=7 --project-root="$REPO_ROOT"
|
|
379
|
+
[ "$status" -eq 0 ]
|
|
380
|
+
echo "$output" | grep '"plugin":"win"' | python3 -c "
|
|
381
|
+
import json, sys
|
|
382
|
+
r = json.loads(sys.stdin.read().strip())
|
|
383
|
+
assert r['commits_window'] == 1, r
|
|
384
|
+
assert r['window_days'] == 7, r
|
|
385
|
+
"
|
|
386
|
+
}
|