@windyroad/itil 0.26.0-preview.291 → 0.27.0-preview.296
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/package.json +1 -1
- package/scripts/migrate-problems-add-type.sh +116 -0
- package/scripts/reconcile-readme.sh +30 -3
- package/scripts/test/dual-tolerant-glob-rfc-002-t2.bats +309 -0
- package/scripts/test/i2-no-type-branching.bats +320 -0
- package/scripts/test/migrate-problems-add-type.bats +280 -0
- package/scripts/test/reconcile-readme.bats +353 -0
- package/scripts/test/skill-md-dual-tolerant-coverage-rfc-002-t3.bats +374 -0
- package/skills/capture-problem/SKILL.md +53 -12
- package/skills/capture-rfc/SKILL.md +6 -3
- package/skills/close-incident/SKILL.md +2 -2
- package/skills/link-incident/SKILL.md +4 -2
- package/skills/list-problems/SKILL.md +15 -11
- package/skills/manage-problem/SKILL.md +25 -13
- package/skills/manage-rfc/SKILL.md +4 -1
- package/skills/report-upstream/SKILL.md +3 -1
- package/skills/review-problems/SKILL.md +8 -7
- package/skills/transition-problem/SKILL.md +3 -3
- package/skills/transition-problems/SKILL.md +2 -2
- package/skills/work-problem/SKILL.md +6 -1
- package/skills/work-problems/SKILL.md +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P170 — Slice 4 B7.T4 (item 8d): I2 (uniform problem
|
|
4
|
+
# ontology) load-bearing behavioural enforcement. ADR-060 architect
|
|
5
|
+
# finding 2: "I2 needs load-bearing behavioural test, not prose
|
|
6
|
+
# prohibition" — without this test ADR-060 ships I2 in name only.
|
|
7
|
+
#
|
|
8
|
+
# Contract: the type-tag is a CLASSIFICATION facet, never a workflow
|
|
9
|
+
# split. No skill or supporting script branches on the `type` field.
|
|
10
|
+
# This test asserts the property behaviourally for the pure-bash
|
|
11
|
+
# supporting-script surface — for each script that reads problem-
|
|
12
|
+
# ticket frontmatter, observable outputs (stdout / exit code / file
|
|
13
|
+
# mutations) are isomorphic across two synthetic ticket-set variants
|
|
14
|
+
# (one `type: technical`, one `type: user-business`).
|
|
15
|
+
#
|
|
16
|
+
# Coverage SCOPE — pure-bash supporting scripts only:
|
|
17
|
+
# - `reconcile-readme.sh`
|
|
18
|
+
# - `update-problem-rfcs-section.sh`
|
|
19
|
+
# - `classify-readme-drift.sh`
|
|
20
|
+
# - `reconcile-rfcs.sh`
|
|
21
|
+
# - `migrate-problems-add-type.sh` (idempotency on already-migrated
|
|
22
|
+
# tickets, regardless of type value)
|
|
23
|
+
#
|
|
24
|
+
# Coverage GAP — agent-driven SKILL.md surface:
|
|
25
|
+
# The SKILL.md files (`/wr-itil:capture-problem`,
|
|
26
|
+
# `/wr-itil:manage-problem`, `/wr-itil:work-problems`,
|
|
27
|
+
# `/wr-itil:review-problems`, `/wr-itil:transition-problem(s)`) are
|
|
28
|
+
# agent-driven instructions, not scripts. Behaviourally testing them
|
|
29
|
+
# requires invoking each skill against two type-variant ticket sets
|
|
30
|
+
# and asserting observable agent action is isomorphic — that
|
|
31
|
+
# primitive does not yet exist. **Tracked as P176** (agent-side I2
|
|
32
|
+
# coverage gap; descendant of P012 skill testing harness scope).
|
|
33
|
+
# Per ADR-052 § Surface 2 (in-file justification with cited harness-
|
|
34
|
+
# gap ticket): the deferral is named, ticketed, audit-trailed — NOT
|
|
35
|
+
# silent.
|
|
36
|
+
#
|
|
37
|
+
# P081 protection: this test is behavioural per ADR-052 (observable
|
|
38
|
+
# input → output assertions on script invocations). It does NOT grep
|
|
39
|
+
# SKILL.md / agent.md content for `type` token references — that
|
|
40
|
+
# would be the structural-test-disguised-as-behavioural anti-pattern
|
|
41
|
+
# P081 forbids.
|
|
42
|
+
#
|
|
43
|
+
# @adr ADR-060 (Phase 1 invariant I2; architect finding 2 + finding 10
|
|
44
|
+
# item 8d; Confirmation criterion 8)
|
|
45
|
+
# @adr ADR-051 (load-bearing-from-the-start — I2 ships at the same
|
|
46
|
+
# time as the type-tag, NOT later by graceful drift)
|
|
47
|
+
# @adr ADR-052 (behavioural-bats default; § Surface 2 escape-hatch
|
|
48
|
+
# for the agent-side coverage gap)
|
|
49
|
+
# @adr ADR-014 (single-purpose; one mechanical invariant guard)
|
|
50
|
+
# @problem P081 (no structural grep on SKILL.md content — protects
|
|
51
|
+
# against quick-fix workarounds that would re-introduce drift)
|
|
52
|
+
# @problem P176 (agent-side I2 coverage gap follow-up — covers what
|
|
53
|
+
# this bats explicitly does not)
|
|
54
|
+
# @jtbd JTBD-001 (extended scope: multi-commit coordinated-change
|
|
55
|
+
# governance at the change-set level — this test gates a CLASS of
|
|
56
|
+
# future changes, not just a single edit)
|
|
57
|
+
# @jtbd JTBD-008 (decompose-fix-into-coordinated-changes — preserves
|
|
58
|
+
# uniform mechanism across the type facet)
|
|
59
|
+
|
|
60
|
+
setup() {
|
|
61
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
62
|
+
TECH_DIR="$(mktemp -d)"
|
|
63
|
+
USER_DIR="$(mktemp -d)"
|
|
64
|
+
RFCS_TECH="$(mktemp -d)"
|
|
65
|
+
RFCS_USER="$(mktemp -d)"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
teardown() {
|
|
69
|
+
rm -rf "$TECH_DIR" "$USER_DIR" "$RFCS_TECH" "$RFCS_USER"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Build twin ticket-set fixtures: identical content except for the
|
|
73
|
+
# `**Type**:` field value. Three tickets per fixture (Open / Verifying
|
|
74
|
+
# / Closed) so each script's branching surface gets exercised.
|
|
75
|
+
build_twin_fixtures() {
|
|
76
|
+
for type_pair in "technical:$TECH_DIR" "user-business:$USER_DIR"; do
|
|
77
|
+
typev="${type_pair%%:*}"
|
|
78
|
+
dir="${type_pair##*:}"
|
|
79
|
+
cat > "$dir/100-foo.open.md" <<EOF
|
|
80
|
+
# Problem 100: Foo
|
|
81
|
+
|
|
82
|
+
**Status**: Open
|
|
83
|
+
**WSJF**: 5.0
|
|
84
|
+
**Type**: ${typev}
|
|
85
|
+
|
|
86
|
+
## Description
|
|
87
|
+
|
|
88
|
+
stub
|
|
89
|
+
|
|
90
|
+
## Related
|
|
91
|
+
|
|
92
|
+
stub
|
|
93
|
+
EOF
|
|
94
|
+
cat > "$dir/101-bar.verifying.md" <<EOF
|
|
95
|
+
# Problem 101: Bar
|
|
96
|
+
|
|
97
|
+
**Status**: Verification Pending
|
|
98
|
+
**WSJF**: 0
|
|
99
|
+
**Type**: ${typev}
|
|
100
|
+
|
|
101
|
+
## Description
|
|
102
|
+
|
|
103
|
+
stub
|
|
104
|
+
EOF
|
|
105
|
+
cat > "$dir/102-baz.closed.md" <<EOF
|
|
106
|
+
# Problem 102: Baz
|
|
107
|
+
|
|
108
|
+
**Status**: Closed
|
|
109
|
+
**Type**: ${typev}
|
|
110
|
+
|
|
111
|
+
## Description
|
|
112
|
+
|
|
113
|
+
stub
|
|
114
|
+
EOF
|
|
115
|
+
done
|
|
116
|
+
|
|
117
|
+
for type_pair in "technical:$TECH_DIR" "user-business:$USER_DIR"; do
|
|
118
|
+
dir="${type_pair##*:}"
|
|
119
|
+
cat > "$dir/README.md" <<'EOF'
|
|
120
|
+
# Problem Backlog
|
|
121
|
+
|
|
122
|
+
> Last reviewed: 2026-01-01.
|
|
123
|
+
|
|
124
|
+
## WSJF Rankings
|
|
125
|
+
|
|
126
|
+
| WSJF | ID | Title | Severity | Status | Effort |
|
|
127
|
+
|------|-----|-------|----------|--------|--------|
|
|
128
|
+
| 5.0 | P100 | Foo | 12 High | Open | M |
|
|
129
|
+
|
|
130
|
+
## Verification Queue
|
|
131
|
+
|
|
132
|
+
| ID | Title | Released | Likely verified? |
|
|
133
|
+
|----|-------|----------|------------------|
|
|
134
|
+
| P101 | Bar | 2026-01-01 | no (0 days) |
|
|
135
|
+
|
|
136
|
+
## Closed
|
|
137
|
+
|
|
138
|
+
| ID | Title | Closed |
|
|
139
|
+
|----|-------|--------|
|
|
140
|
+
| P102 | Baz | 2026-01-01 |
|
|
141
|
+
EOF
|
|
142
|
+
done
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Build twin RFC-set fixtures alongside the problem fixtures, so
|
|
146
|
+
# `update-problem-rfcs-section.sh` and `reconcile-rfcs.sh` get exercised
|
|
147
|
+
# on traced-RFCs across both type variants.
|
|
148
|
+
build_twin_rfc_fixtures() {
|
|
149
|
+
for type_pair in "technical:$RFCS_TECH" "user-business:$RFCS_USER"; do
|
|
150
|
+
dir="${type_pair##*:}"
|
|
151
|
+
cat > "$dir/RFC-001-foo.accepted.md" <<EOF
|
|
152
|
+
---
|
|
153
|
+
status: accepted
|
|
154
|
+
rfc-id: foo
|
|
155
|
+
reported: 2026-01-01
|
|
156
|
+
decision-makers: [test]
|
|
157
|
+
problems: [P100]
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
# RFC-001: foo
|
|
161
|
+
|
|
162
|
+
stub
|
|
163
|
+
EOF
|
|
164
|
+
cat > "$dir/README.md" <<'EOF'
|
|
165
|
+
# RFC Backlog
|
|
166
|
+
|
|
167
|
+
## WSJF Rankings
|
|
168
|
+
|
|
169
|
+
| WSJF | ID | Title | Status |
|
|
170
|
+
|------|-----|-------|--------|
|
|
171
|
+
| 5.0 | RFC-001 | foo | accepted |
|
|
172
|
+
|
|
173
|
+
## Verification Queue
|
|
174
|
+
|
|
175
|
+
| ID | Title | Released | Likely verified? |
|
|
176
|
+
|----|-------|----------|------------------|
|
|
177
|
+
|
|
178
|
+
## Closed
|
|
179
|
+
|
|
180
|
+
| ID | Title | Closed |
|
|
181
|
+
|----|-------|--------|
|
|
182
|
+
EOF
|
|
183
|
+
done
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Strip `**Type**:` lines and `type:` YAML lines from a stream so
|
|
187
|
+
# isomorphism comparison ignores the SOLE legitimate point of
|
|
188
|
+
# differentiation. Anything else differing between the two variants
|
|
189
|
+
# is an I2 violation.
|
|
190
|
+
strip_type_lines() {
|
|
191
|
+
grep -v -E '^(\*\*Type\*\*:|type: )' || true
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Diff two files modulo type lines; expect empty diff (= isomorphic).
|
|
195
|
+
assert_isomorphic_files() {
|
|
196
|
+
local a="$1" b="$2"
|
|
197
|
+
diff <(strip_type_lines < "$a") <(strip_type_lines < "$b")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# ── reconcile-readme.sh: exit code + stdout invariant across types ───────────
|
|
201
|
+
|
|
202
|
+
@test "I2: reconcile-readme.sh exits identically across type variants" {
|
|
203
|
+
build_twin_fixtures
|
|
204
|
+
set +e
|
|
205
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-rrt-tech.out 2>&1
|
|
206
|
+
ec_tech=$?
|
|
207
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-rrt-user.out 2>&1
|
|
208
|
+
ec_user=$?
|
|
209
|
+
set -e
|
|
210
|
+
[ "$ec_tech" = "$ec_user" ]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@test "I2: reconcile-readme.sh stdout isomorphic modulo type field" {
|
|
214
|
+
build_twin_fixtures
|
|
215
|
+
set +e
|
|
216
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-rrs-tech.out 2>&1 || true
|
|
217
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-rrs-user.out 2>&1 || true
|
|
218
|
+
set -e
|
|
219
|
+
assert_isomorphic_files /tmp/i2-rrs-tech.out /tmp/i2-rrs-user.out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# ── update-problem-rfcs-section.sh: file mutation invariant across types ─────
|
|
223
|
+
|
|
224
|
+
@test "I2: update-problem-rfcs-section.sh mutates ticket files identically across type variants" {
|
|
225
|
+
build_twin_fixtures
|
|
226
|
+
build_twin_rfc_fixtures
|
|
227
|
+
bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
|
|
228
|
+
"$TECH_DIR/100-foo.open.md" "$RFCS_TECH"
|
|
229
|
+
bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
|
|
230
|
+
"$USER_DIR/100-foo.open.md" "$RFCS_USER"
|
|
231
|
+
assert_isomorphic_files \
|
|
232
|
+
"$TECH_DIR/100-foo.open.md" \
|
|
233
|
+
"$USER_DIR/100-foo.open.md"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@test "I2: update-problem-rfcs-section.sh produces same ## RFCs section regardless of type" {
|
|
237
|
+
build_twin_fixtures
|
|
238
|
+
build_twin_rfc_fixtures
|
|
239
|
+
bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
|
|
240
|
+
"$TECH_DIR/100-foo.open.md" "$RFCS_TECH"
|
|
241
|
+
bash "$SCRIPTS_DIR/update-problem-rfcs-section.sh" \
|
|
242
|
+
"$USER_DIR/100-foo.open.md" "$RFCS_USER"
|
|
243
|
+
# Both files now carry the SAME ## RFCs table row.
|
|
244
|
+
grep -q '| RFC-001 | accepted | foo |' "$TECH_DIR/100-foo.open.md"
|
|
245
|
+
grep -q '| RFC-001 | accepted | foo |' "$USER_DIR/100-foo.open.md"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# ── classify-readme-drift.sh: exit code + stdout invariant across types ──────
|
|
249
|
+
|
|
250
|
+
@test "I2: classify-readme-drift.sh exits identically across type variants" {
|
|
251
|
+
build_twin_fixtures
|
|
252
|
+
# Manufacture a drift output file (the script reads stdout from
|
|
253
|
+
# reconcile-readme; here we build a minimal canned drift line).
|
|
254
|
+
drift_in="$(mktemp)"
|
|
255
|
+
echo "DRIFT P100 wsjf-rankings: claims=open actual=open" > "$drift_in"
|
|
256
|
+
set +e
|
|
257
|
+
bash "$SCRIPTS_DIR/classify-readme-drift.sh" "$drift_in" "$TECH_DIR" > /tmp/i2-cdt-tech.out 2>&1
|
|
258
|
+
ec_tech=$?
|
|
259
|
+
bash "$SCRIPTS_DIR/classify-readme-drift.sh" "$drift_in" "$USER_DIR" > /tmp/i2-cdt-user.out 2>&1
|
|
260
|
+
ec_user=$?
|
|
261
|
+
set -e
|
|
262
|
+
rm -f "$drift_in"
|
|
263
|
+
[ "$ec_tech" = "$ec_user" ]
|
|
264
|
+
assert_isomorphic_files /tmp/i2-cdt-tech.out /tmp/i2-cdt-user.out
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# ── reconcile-rfcs.sh: exit code + stdout invariant across types ─────────────
|
|
268
|
+
|
|
269
|
+
@test "I2: reconcile-rfcs.sh exits identically across problem-type variants" {
|
|
270
|
+
build_twin_fixtures
|
|
271
|
+
build_twin_rfc_fixtures
|
|
272
|
+
set +e
|
|
273
|
+
bash "$SCRIPTS_DIR/reconcile-rfcs.sh" "$RFCS_TECH" "$TECH_DIR" > /tmp/i2-rrf-tech.out 2>&1
|
|
274
|
+
ec_tech=$?
|
|
275
|
+
bash "$SCRIPTS_DIR/reconcile-rfcs.sh" "$RFCS_USER" "$USER_DIR" > /tmp/i2-rrf-user.out 2>&1
|
|
276
|
+
ec_user=$?
|
|
277
|
+
set -e
|
|
278
|
+
[ "$ec_tech" = "$ec_user" ]
|
|
279
|
+
assert_isomorphic_files /tmp/i2-rrf-tech.out /tmp/i2-rrf-user.out
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# ── migrate-problems-add-type.sh: idempotency invariant across both type values ──
|
|
283
|
+
|
|
284
|
+
@test "I2: migrate-problems-add-type.sh is no-op on already-typed tickets, both type values" {
|
|
285
|
+
build_twin_fixtures
|
|
286
|
+
hash_tech_before=$(shasum "$TECH_DIR"/*.md | shasum | cut -d' ' -f1)
|
|
287
|
+
hash_user_before=$(shasum "$USER_DIR"/*.md | shasum | cut -d' ' -f1)
|
|
288
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" --apply "$TECH_DIR"
|
|
289
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" --apply "$USER_DIR"
|
|
290
|
+
hash_tech_after=$(shasum "$TECH_DIR"/*.md | shasum | cut -d' ' -f1)
|
|
291
|
+
hash_user_after=$(shasum "$USER_DIR"/*.md | shasum | cut -d' ' -f1)
|
|
292
|
+
[ "$hash_tech_before" = "$hash_tech_after" ]
|
|
293
|
+
[ "$hash_user_before" = "$hash_user_after" ]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@test "I2: migrate-problems-add-type.sh diagnose-mode exit code identical across type variants" {
|
|
297
|
+
build_twin_fixtures
|
|
298
|
+
set +e
|
|
299
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$TECH_DIR" > /tmp/i2-mig-tech.out 2>&1
|
|
300
|
+
ec_tech=$?
|
|
301
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$USER_DIR" > /tmp/i2-mig-user.out 2>&1
|
|
302
|
+
ec_user=$?
|
|
303
|
+
set -e
|
|
304
|
+
[ "$ec_tech" = "$ec_user" ]
|
|
305
|
+
assert_isomorphic_files /tmp/i2-mig-tech.out /tmp/i2-mig-user.out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# ── Cross-script invariant: full-pipeline behaviour identical across types ───
|
|
309
|
+
|
|
310
|
+
@test "I2: full diagnose pipeline (migrate diagnose + reconcile-readme) invariant across types" {
|
|
311
|
+
build_twin_fixtures
|
|
312
|
+
set +e
|
|
313
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$TECH_DIR" > /tmp/i2-pipe-tech-mig.out 2>&1
|
|
314
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$TECH_DIR" > /tmp/i2-pipe-tech-rec.out 2>&1
|
|
315
|
+
bash "$SCRIPTS_DIR/migrate-problems-add-type.sh" "$USER_DIR" > /tmp/i2-pipe-user-mig.out 2>&1
|
|
316
|
+
bash "$SCRIPTS_DIR/reconcile-readme.sh" "$USER_DIR" > /tmp/i2-pipe-user-rec.out 2>&1
|
|
317
|
+
set -e
|
|
318
|
+
assert_isomorphic_files /tmp/i2-pipe-tech-mig.out /tmp/i2-pipe-user-mig.out
|
|
319
|
+
assert_isomorphic_files /tmp/i2-pipe-tech-rec.out /tmp/i2-pipe-user-rec.out
|
|
320
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P170 — Slice 4 B7.T2 (item 8b): bulk-migrate existing
|
|
4
|
+
# problem tickets to the type-tag schema with default `type: technical`.
|
|
5
|
+
# Script-driven, idempotent, no per-ticket judgement (per ADR-060
|
|
6
|
+
# § Type-tag schema migration line; architect finding 10 split).
|
|
7
|
+
#
|
|
8
|
+
# @adr ADR-060 (type-tag schema introduction; Phase 1 item 8b)
|
|
9
|
+
# @adr ADR-052 (behavioural bats default; observable file-state
|
|
10
|
+
# assertions, not source greps per P081)
|
|
11
|
+
# @adr ADR-014 (single-purpose script; one mechanical migration)
|
|
12
|
+
#
|
|
13
|
+
# Contract:
|
|
14
|
+
# - Input: a problems-dir containing `<NNN>-*.<status>.md` files.
|
|
15
|
+
# - Diagnose mode (default): exit 0 if every ticket carries
|
|
16
|
+
# `**Type**: <value>` body field; exit 1 if any ticket needs
|
|
17
|
+
# migration (tickets needing migration listed on stdout, one per
|
|
18
|
+
# line, ≤150 bytes per ADR-038 progressive disclosure budget).
|
|
19
|
+
# - Apply mode (--apply flag): inserts `**Type**: technical` after
|
|
20
|
+
# the LAST present body field marker among
|
|
21
|
+
# `**Status**` / `**Reported**` / `**Priority**` / `**Effort**` /
|
|
22
|
+
# `**WSJF**`. Idempotent (re-running is no-op).
|
|
23
|
+
# - Tickets with no recognisable header field block are skipped with
|
|
24
|
+
# a `SKIP` line on stderr (does not fail the run).
|
|
25
|
+
# - Default value: `technical` per ADR-060 line 92.
|
|
26
|
+
|
|
27
|
+
setup() {
|
|
28
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
29
|
+
SCRIPT="$SCRIPTS_DIR/migrate-problems-add-type.sh"
|
|
30
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
teardown() {
|
|
34
|
+
rm -rf "$FIXTURE_DIR"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
write_ticket() {
|
|
38
|
+
local id="$1" slug="$2" status="$3" body="$4"
|
|
39
|
+
local file="$FIXTURE_DIR/${id}-${slug}.${status}.md"
|
|
40
|
+
printf '%s' "$body" > "$file"
|
|
41
|
+
echo "$file"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── Existence + executable ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
@test "migrate-problems-add-type: script exists" {
|
|
47
|
+
[ -f "$SCRIPT" ]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@test "migrate-problems-add-type: script is executable" {
|
|
51
|
+
[ -x "$SCRIPT" ]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ── Diagnose mode (default) ──────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
@test "diagnose: exit 0 when every ticket already carries Type field" {
|
|
57
|
+
write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
58
|
+
# Problem 100: Foo
|
|
59
|
+
|
|
60
|
+
**Status**: Open
|
|
61
|
+
**Type**: technical
|
|
62
|
+
EOF
|
|
63
|
+
)"
|
|
64
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
65
|
+
[ "$status" -eq 0 ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "diagnose: exit 1 when at least one ticket lacks Type field" {
|
|
69
|
+
write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
70
|
+
# Problem 100: Foo
|
|
71
|
+
|
|
72
|
+
**Status**: Open
|
|
73
|
+
**WSJF**: 5.0
|
|
74
|
+
EOF
|
|
75
|
+
)"
|
|
76
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
77
|
+
[ "$status" -eq 1 ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@test "diagnose: lists each ticket needing migration on stdout" {
|
|
81
|
+
write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
82
|
+
# Problem 100: Foo
|
|
83
|
+
|
|
84
|
+
**Status**: Open
|
|
85
|
+
**WSJF**: 5.0
|
|
86
|
+
EOF
|
|
87
|
+
)"
|
|
88
|
+
write_ticket "101" "bar" "closed" "$(cat <<'EOF'
|
|
89
|
+
# Problem 101: Bar
|
|
90
|
+
|
|
91
|
+
**Status**: Closed
|
|
92
|
+
**Priority**: 6
|
|
93
|
+
EOF
|
|
94
|
+
)"
|
|
95
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
96
|
+
[ "$status" -eq 1 ]
|
|
97
|
+
echo "$output" | grep -q '100-foo'
|
|
98
|
+
echo "$output" | grep -q '101-bar'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test "diagnose: read-only — does not mutate any ticket file" {
|
|
102
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
103
|
+
# Problem 100: Foo
|
|
104
|
+
|
|
105
|
+
**Status**: Open
|
|
106
|
+
**WSJF**: 5.0
|
|
107
|
+
EOF
|
|
108
|
+
)")
|
|
109
|
+
hash_before=$(shasum "$ticket" | cut -d' ' -f1)
|
|
110
|
+
run bash "$SCRIPT" "$FIXTURE_DIR"
|
|
111
|
+
hash_after=$(shasum "$ticket" | cut -d' ' -f1)
|
|
112
|
+
[ "$hash_before" = "$hash_after" ]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# ── Apply mode (--apply) ─────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
@test "apply: inserts Type: technical after WSJF line when WSJF present" {
|
|
118
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
119
|
+
# Problem 100: Foo
|
|
120
|
+
|
|
121
|
+
**Status**: Open
|
|
122
|
+
**WSJF**: 5.0
|
|
123
|
+
|
|
124
|
+
## Description
|
|
125
|
+
EOF
|
|
126
|
+
)")
|
|
127
|
+
run bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
128
|
+
[ "$status" -eq 0 ]
|
|
129
|
+
grep -q '^\*\*Type\*\*: technical$' "$ticket"
|
|
130
|
+
# Type sits AFTER WSJF (highest field marker present).
|
|
131
|
+
wsjf_line=$(grep -n '^\*\*WSJF\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
132
|
+
type_line=$(grep -n '^\*\*Type\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
133
|
+
[ "$type_line" -gt "$wsjf_line" ]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@test "apply: inserts Type after Effort when WSJF absent" {
|
|
137
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
138
|
+
# Problem 100: Foo
|
|
139
|
+
|
|
140
|
+
**Status**: Open
|
|
141
|
+
**Reported**: 2026-01-01
|
|
142
|
+
**Priority**: 6
|
|
143
|
+
**Effort**: M
|
|
144
|
+
|
|
145
|
+
## Description
|
|
146
|
+
EOF
|
|
147
|
+
)")
|
|
148
|
+
run bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
149
|
+
[ "$status" -eq 0 ]
|
|
150
|
+
effort_line=$(grep -n '^\*\*Effort\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
151
|
+
type_line=$(grep -n '^\*\*Type\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
152
|
+
[ "$type_line" -gt "$effort_line" ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@test "apply: inserts Type after Priority when WSJF + Effort absent" {
|
|
156
|
+
ticket=$(write_ticket "100" "foo" "closed" "$(cat <<'EOF'
|
|
157
|
+
# Problem 100: Foo
|
|
158
|
+
|
|
159
|
+
**Status**: Closed
|
|
160
|
+
**Priority**: 6
|
|
161
|
+
|
|
162
|
+
## Description
|
|
163
|
+
EOF
|
|
164
|
+
)")
|
|
165
|
+
run bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
166
|
+
[ "$status" -eq 0 ]
|
|
167
|
+
priority_line=$(grep -n '^\*\*Priority\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
168
|
+
type_line=$(grep -n '^\*\*Type\*\*:' "$ticket" | head -1 | cut -d: -f1)
|
|
169
|
+
[ "$type_line" -gt "$priority_line" ]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@test "apply: idempotent — re-running with Type already present is no-op" {
|
|
173
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
174
|
+
# Problem 100: Foo
|
|
175
|
+
|
|
176
|
+
**Status**: Open
|
|
177
|
+
**WSJF**: 5.0
|
|
178
|
+
|
|
179
|
+
## Description
|
|
180
|
+
EOF
|
|
181
|
+
)")
|
|
182
|
+
bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
183
|
+
hash1=$(shasum "$ticket" | cut -d' ' -f1)
|
|
184
|
+
bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
185
|
+
hash2=$(shasum "$ticket" | cut -d' ' -f1)
|
|
186
|
+
[ "$hash1" = "$hash2" ]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@test "apply: default value is 'technical'" {
|
|
190
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
191
|
+
# Problem 100: Foo
|
|
192
|
+
|
|
193
|
+
**Status**: Open
|
|
194
|
+
**WSJF**: 5.0
|
|
195
|
+
EOF
|
|
196
|
+
)")
|
|
197
|
+
bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
198
|
+
grep -q '^\*\*Type\*\*: technical$' "$ticket"
|
|
199
|
+
! grep -q '^\*\*Type\*\*: user-business$' "$ticket"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@test "apply: handles all five lifecycle file extensions" {
|
|
203
|
+
i=100
|
|
204
|
+
for state in open known-error verifying parked closed; do
|
|
205
|
+
write_ticket "$i" "t-${state}" "$state" "$(cat <<EOF
|
|
206
|
+
# Problem $i: stub
|
|
207
|
+
|
|
208
|
+
**Status**: stub
|
|
209
|
+
**WSJF**: 5.0
|
|
210
|
+
EOF
|
|
211
|
+
)" > /dev/null
|
|
212
|
+
i=$((i+1))
|
|
213
|
+
done
|
|
214
|
+
bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
215
|
+
count=$(grep -l '^\*\*Type\*\*: technical$' "$FIXTURE_DIR"/*.md | wc -l | tr -d ' ')
|
|
216
|
+
[ "$count" -eq 5 ]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@test "apply: preserves all original content (line-count grows by exactly 1 per migrated ticket)" {
|
|
220
|
+
# Use printf with explicit trailing newline to mirror real ticket-file
|
|
221
|
+
# convention (POSIX text-file trailing newline). awk normalizes EOF
|
|
222
|
+
# to a trailing newline; fixture must match so the +1-line invariant
|
|
223
|
+
# holds.
|
|
224
|
+
ticket="$FIXTURE_DIR/100-foo.open.md"
|
|
225
|
+
printf '%s\n' \
|
|
226
|
+
'# Problem 100: Foo' \
|
|
227
|
+
'' \
|
|
228
|
+
'**Status**: Open' \
|
|
229
|
+
'**WSJF**: 5.0' \
|
|
230
|
+
'' \
|
|
231
|
+
'## Description' \
|
|
232
|
+
'' \
|
|
233
|
+
'Body text line.' \
|
|
234
|
+
'' \
|
|
235
|
+
'## Related' \
|
|
236
|
+
'' \
|
|
237
|
+
'stub' > "$ticket"
|
|
238
|
+
before=$(wc -l < "$ticket" | tr -d ' ')
|
|
239
|
+
bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
240
|
+
after=$(wc -l < "$ticket" | tr -d ' ')
|
|
241
|
+
[ "$after" -eq "$((before + 1))" ]
|
|
242
|
+
# Original content sentinels survive.
|
|
243
|
+
grep -q '^# Problem 100: Foo$' "$ticket"
|
|
244
|
+
grep -q '^## Description$' "$ticket"
|
|
245
|
+
grep -q '^Body text line\.$' "$ticket"
|
|
246
|
+
grep -q '^## Related$' "$ticket"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# ── Skip / malformed handling ────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
@test "apply: skips ticket with no recognisable header field block (warning to stderr)" {
|
|
252
|
+
ticket=$(write_ticket "100" "foo" "open" "$(cat <<'EOF'
|
|
253
|
+
# Problem 100: Foo
|
|
254
|
+
|
|
255
|
+
Just a body, no field markers at all.
|
|
256
|
+
EOF
|
|
257
|
+
)")
|
|
258
|
+
hash_before=$(shasum "$ticket" | cut -d' ' -f1)
|
|
259
|
+
run bash "$SCRIPT" --apply "$FIXTURE_DIR"
|
|
260
|
+
[ "$status" -eq 0 ]
|
|
261
|
+
hash_after=$(shasum "$ticket" | cut -d' ' -f1)
|
|
262
|
+
[ "$hash_before" = "$hash_after" ]
|
|
263
|
+
# SKIP marker on stderr.
|
|
264
|
+
echo "$output" | grep -qi 'skip'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# ── Default problems-dir ─────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
@test "diagnose: defaults problems-dir to ./docs/problems when no arg given" {
|
|
270
|
+
cd "$FIXTURE_DIR"
|
|
271
|
+
mkdir -p docs/problems
|
|
272
|
+
cat > docs/problems/100-foo.open.md <<'EOF'
|
|
273
|
+
# Problem 100: Foo
|
|
274
|
+
|
|
275
|
+
**Status**: Open
|
|
276
|
+
**Type**: technical
|
|
277
|
+
EOF
|
|
278
|
+
run bash "$SCRIPT"
|
|
279
|
+
[ "$status" -eq 0 ]
|
|
280
|
+
}
|