forge-orkes 0.71.0 → 0.72.2

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,235 @@
1
+ #!/usr/bin/env sh
2
+ # Fixture-repo test suite for .claude/hooks/forge-reserve.sh (m-41, plan 81-01).
3
+ #
4
+ # Contract under test (ADR-021/023/029 + FR-236/237/238, NFR-045):
5
+ # phase — computed P-0NN; in-tree scan = live roadmap `- id: N` ints ∪ archived
6
+ # phase dirs .forge/archive/milestone-*/phases/<N>-* (burned ids never
7
+ # reallocated); ledger + main:reservations.yml floors as for all kinds.
8
+ # wave — computed W-0NN; in-tree scan = live waves-map KEYS only (never the
9
+ # phase ids inside the brackets).
10
+ # version — RECORD-ONLY (--value): clean claim dual-writes ledger +
11
+ # reservations.yml and echoes the value; the SAME value claimed by a
12
+ # DIFFERENT milestone → exit 1, stderr names the holder, nothing
13
+ # written; re-claim by the SAME milestone → exit 0, idempotent, no
14
+ # duplicate line; missing --value → usage exit 2, nothing written.
15
+ # regression (NFR-045) — the pre-existing kinds' ids and file shapes are
16
+ # unchanged; the lock releases so back-to-back calls never time out.
17
+ #
18
+ # Every case builds its own mktemp fixture repo (real `git init`) — the suite
19
+ # never touches the enclosing dev repo's state. Output: one line per case,
20
+ # `case N <tag> PASS` or `case N <tag> FAIL: <why>`; non-zero exit if any FAIL.
21
+ #
22
+ # Usage: sh .forge/checks/tests/forge-reserve.test.sh
23
+ set -u
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
+ RESERVE="$SCRIPT_DIR/../../../.claude/hooks/forge-reserve.sh"
27
+ [ -f "$RESERVE" ] || { printf 'FATAL: %s missing\n' "$RESERVE" >&2; exit 1; }
28
+
29
+ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
30
+ export GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM
31
+
32
+ ROOT="$(mktemp -d)"
33
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
34
+
35
+ fails=0
36
+ n=0
37
+
38
+ # new_fixture <name> → prints the fixture repo path
39
+ new_fixture() {
40
+ fx="$ROOT/$1"
41
+ mkdir -p "$fx"
42
+ git -C "$fx" init -q
43
+ printf '%s' "$fx"
44
+ }
45
+
46
+ report() { # report <tag> <pass|fail> [why]
47
+ n=$((n + 1))
48
+ if [ "$2" = pass ]; then
49
+ printf 'case %d %s PASS\n' "$n" "$1"
50
+ else
51
+ printf 'case %d %s FAIL: %s\n' "$n" "$1" "${3:-unspecified}"
52
+ fails=$((fails + 1))
53
+ fi
54
+ }
55
+
56
+ # ── FR-236: phase kind ────────────────────────────────────────────────────────
57
+
58
+ # case 1 phase-archive-aware — roadmap max 12, archived phase dir 14 → P-015
59
+ fx=$(new_fixture c1)
60
+ mkdir -p "$fx/.forge/archive/milestone-9/phases/14-old"
61
+ printf 'roadmap:\n phases:\n - id: 11\n - id: 12\n' > "$fx/.forge/roadmap.yml"
62
+ out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
63
+ if [ "$out" = "P-015" ]; then report phase-archive-aware pass
64
+ else report phase-archive-aware fail "expected P-015 (archived 14 must stay burned), got: $out"; fi
65
+
66
+ # case 2 phase-dual-write — case 1's claim landed in BOTH registries
67
+ if grep -q "phase P-015 m-t" "$fx/.git/forge/id-reservations" 2>/dev/null \
68
+ && grep -q "id: P-015" "$fx/.forge/reservations.yml" 2>/dev/null; then
69
+ report phase-dual-write pass
70
+ else report phase-dual-write fail "P-015 missing from ledger or reservations.yml"; fi
71
+
72
+ # case 3 phase-sibling-ledger — uncommitted sibling ledger claim is visible
73
+ fx=$(new_fixture c3)
74
+ mkdir -p "$fx/.forge" "$fx/.git/forge"
75
+ printf 'roadmap:\n phases:\n - id: 12\n' > "$fx/.forge/roadmap.yml"
76
+ printf 'phase\tP-015\tm-sib\t2026-07-23T00:00:00Z\n' >> "$fx/.git/forge/id-reservations"
77
+ out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
78
+ if [ "$out" = "P-016" ]; then report phase-sibling-ledger pass
79
+ else report phase-sibling-ledger fail "sibling claim P-015 ignored — expected P-016, got: $out"; fi
80
+
81
+ # case 4 phase-fresh — no roadmap, no archive → P-001
82
+ fx=$(new_fixture c4)
83
+ out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
84
+ if [ "$out" = "P-001" ]; then report phase-fresh pass
85
+ else report phase-fresh fail "fresh repo expected P-001, got: $out"; fi
86
+
87
+ # case 5 phase-wave-keys-not-counted — waves KEYS and bracket ids must not leak
88
+ # into the phase max: roadmap phase max 3, waves key 9 pointing at [3] → P-004.
89
+ fx=$(new_fixture c5)
90
+ mkdir -p "$fx/.forge"
91
+ printf 'roadmap:\n phases:\n - id: 3\n waves:\n 9: [3]\n' > "$fx/.forge/roadmap.yml"
92
+ out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
93
+ if [ "$out" = "P-004" ]; then report phase-wave-keys-not-counted pass
94
+ else report phase-wave-keys-not-counted fail "expected P-004 (wave key 9 is not a phase id), got: $out"; fi
95
+
96
+ # ── FR-237: wave kind ────────────────────────────────────────────────────────
97
+
98
+ # case 6 wave-max-key — waves keys 1,7 (bracket ids larger) → W-008
99
+ fx=$(new_fixture c6)
100
+ mkdir -p "$fx/.forge"
101
+ printf 'roadmap:\n phases:\n - id: 1\n waves:\n 1: [40]\n 7: [41]\n' > "$fx/.forge/roadmap.yml"
102
+ out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
103
+ if [ "$out" = "W-008" ]; then report wave-max-key pass
104
+ else report wave-max-key fail "expected W-008 (max KEY 7, ids in brackets ignored), got: $out"; fi
105
+
106
+ # case 7 wave-fresh — no waves block anywhere → W-001
107
+ fx=$(new_fixture c7)
108
+ out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
109
+ if [ "$out" = "W-001" ]; then report wave-fresh pass
110
+ else report wave-fresh fail "expected W-001, got: $out"; fi
111
+
112
+ # ── FR-238: version kind (record-only) ───────────────────────────────────────
113
+
114
+ # case 8 version-clean-claim — records in BOTH registries, echoes the value
115
+ fx=$(new_fixture c8)
116
+ out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
117
+ if [ "$out" = "0.9.0" ] \
118
+ && grep -q "version 0.9.0 m-a" "$fx/.git/forge/id-reservations" 2>/dev/null \
119
+ && grep -q 'id: 0.9.0' "$fx/.forge/reservations.yml" 2>/dev/null; then
120
+ report version-clean-claim pass
121
+ else report version-clean-claim fail "claim not recorded in both registries (stdout: $out)"; fi
122
+
123
+ # case 9 version-conflict-refused — different milestone, same value → exit 1,
124
+ # stderr names the holder, NOTHING written
125
+ err=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-b 2>&1 >/dev/null)
126
+ rc=$?
127
+ lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
128
+ case "$err" in
129
+ *m-a*) named=yes ;;
130
+ *) named=no ;;
131
+ esac
132
+ if [ "$rc" -ne 0 ] && [ "$named" = yes ] && [ "$lines" = "1" ]; then
133
+ report version-conflict-refused pass
134
+ else report version-conflict-refused fail "rc=$rc named-holder=$named ledger-version-lines=$lines (want rc!=0, holder m-a named, 1 line)"; fi
135
+
136
+ # case 10 version-idempotent-reclaim — same milestone re-claims → exit 0, no dup
137
+ out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
138
+ rc=$?
139
+ lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
140
+ if [ "$rc" -eq 0 ] && [ "$out" = "0.9.0" ] && [ "$lines" = "1" ]; then
141
+ report version-idempotent-reclaim pass
142
+ else report version-idempotent-reclaim fail "rc=$rc stdout=$out ledger-version-lines=$lines (want 0 / 0.9.0 / 1)"; fi
143
+
144
+ # case 11 version-missing-value — usage exit 2, nothing written
145
+ fx=$(new_fixture c11)
146
+ err=$(cd "$fx" && "$RESERVE" version --milestone m-a 2>&1 >/dev/null)
147
+ rc=$?
148
+ if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
149
+ report version-missing-value pass
150
+ else report version-missing-value fail "rc=$rc (want 2) or files written on usage error"; fi
151
+
152
+ # ── NFR-045: existing kinds regression + lock release ────────────────────────
153
+
154
+ # case 12 regression-existing-kinds — fr x2 / milestone / refactor sequence and
155
+ # reservations.yml shape identical to pre-m-41 behavior
156
+ fx=$(new_fixture c12)
157
+ a=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
158
+ b=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
159
+ m=$(cd "$fx" && "$RESERVE" milestone --milestone m-new 2>/dev/null)
160
+ r=$(cd "$fx" && "$RESERVE" refactor --milestone m-t 2>/dev/null)
161
+ if [ "$a $b $m $r" = "FR-001 FR-002 M-1 R001" ] \
162
+ && grep -q '^reservations:' "$fx/.forge/reservations.yml" 2>/dev/null \
163
+ && grep -q 'kind: refactor' "$fx/.forge/reservations.yml" 2>/dev/null; then
164
+ report regression-existing-kinds pass
165
+ else report regression-existing-kinds fail "expected FR-001 FR-002 M-1 R001 + seeded header, got: $a $b $m $r"; fi
166
+
167
+ # case 13 regression-lock-release — back-to-back calls in one fixture must not
168
+ # time out (a leaked lock would make the second call exit 3)
169
+ fx=$(new_fixture c13)
170
+ (cd "$fx" && "$RESERVE" adr --milestone m-t >/dev/null 2>&1)
171
+ out=$(cd "$fx" && FORGE_RESERVE_WAIT=2 "$RESERVE" adr --milestone m-t 2>/dev/null)
172
+ rc=$?
173
+ if [ "$rc" -eq 0 ] && [ "$out" = "ADR-002" ]; then
174
+ report regression-lock-release pass
175
+ else report regression-lock-release fail "second call rc=$rc out=$out (leaked lock?)"; fi
176
+
177
+ # ── input hardening (review, m-41): validation before the lock, nothing written ─
178
+
179
+ # case 14 version-value-rejects-non-semver — --value must be X.Y.Z; a non-semver
180
+ # (here a YAML-injection payload) → exit 2, NOTHING written
181
+ fx=$(new_fixture c14)
182
+ err=$(cd "$fx" && "$RESERVE" version --value '9.9.9
183
+ injected: pwned' --milestone m-a 2>&1 >/dev/null)
184
+ rc=$?
185
+ if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
186
+ report version-value-rejects-non-semver pass
187
+ else report version-value-rejects-non-semver fail "rc=$rc (want 2) or files written on bad --value"; fi
188
+
189
+ # case 15 version-value-accepts-prerelease — X.Y.Z-pre is valid semver → recorded
190
+ fx=$(new_fixture c15)
191
+ out=$(cd "$fx" && "$RESERVE" version --value 1.2.0-rc.1 --milestone m-a 2>/dev/null)
192
+ if [ "$out" = "1.2.0-rc.1" ]; then report version-value-accepts-prerelease pass
193
+ else report version-value-accepts-prerelease fail "expected 1.2.0-rc.1 recorded, got: $out"; fi
194
+
195
+ # case 16 milestone-rejects-control-char — a newline in --milestone would split
196
+ # the ledger row / inject a sibling YAML key → exit 2, NOTHING written
197
+ fx=$(new_fixture c16)
198
+ err=$(cd "$fx" && "$RESERVE" fr --milestone 'm-1
199
+ injected: pwned' 2>&1 >/dev/null)
200
+ rc=$?
201
+ if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
202
+ report milestone-rejects-control-char pass
203
+ else report milestone-rejects-control-char fail "rc=$rc (want 2) or files written on control-char milestone"; fi
204
+
205
+ # case 17 symlink-target-refused — reservations.yml is a symlink → refuse to
206
+ # write through it, exit 2, and the symlink target stays untouched
207
+ fx=$(new_fixture c17)
208
+ mkdir -p "$fx/.forge"
209
+ : > "$ROOT/c17-victim.txt"
210
+ ln -s "$ROOT/c17-victim.txt" "$fx/.forge/reservations.yml"
211
+ err=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>&1 >/dev/null)
212
+ rc=$?
213
+ if [ "$rc" -eq 2 ] && [ ! -s "$ROOT/c17-victim.txt" ]; then
214
+ report symlink-target-refused pass
215
+ else report symlink-target-refused fail "rc=$rc (want 2) or victim written through symlink"; fi
216
+
217
+ # case 18 main-floor — cross-machine/cold-start floor: main:reservations.yml
218
+ # carries a merged P-020, empty ledger + roadmap → next phase is P-021
219
+ fx=$(new_fixture c18)
220
+ mkdir -p "$fx/.forge"
221
+ printf 'reservations:\n - kind: phase\n id: P-020\n milestone: m-old\n' > "$fx/.forge/reservations.yml"
222
+ git -C "$fx" add .forge/reservations.yml >/dev/null 2>&1
223
+ git -C "$fx" -c user.email=t@t -c user.name=t commit -qm seed >/dev/null 2>&1
224
+ git -C "$fx" branch -M main >/dev/null 2>&1
225
+ out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
226
+ if [ "$out" = "P-021" ]; then report main-floor pass
227
+ else report main-floor fail "main floor P-020 ignored — expected P-021, got: $out"; fi
228
+
229
+ # ── summary ──────────────────────────────────────────────────────────────────
230
+ if [ "$fails" -eq 0 ]; then
231
+ printf 'forge-reserve.test.sh: all %d cases PASS\n' "$n"
232
+ exit 0
233
+ fi
234
+ printf 'forge-reserve.test.sh: %d of %d cases FAILED\n' "$fails" "$n" >&2
235
+ exit 1