|
2 | 2 | # shellcheck disable=SC2015,SC2329 |
3 | 3 | # ok/bad always return 0, so `cond && ok .. || bad ..` cannot mis-fire (SC2015); |
4 | 4 | # cleanup() runs via the EXIT trap, which shellcheck does not count (SC2329). |
5 | | -# Behavior tests for script/specgit-bootstrap.sh (#521). |
| 5 | +# Behavior tests for script/specgit-bootstrap.sh (#521, #530 record rollback). |
6 | 6 | # |
7 | 7 | # Zero network, zero forge: `specgit` is a stub placed first on PATH; every |
8 | 8 | # fixture is a throwaway git repo under $TMPDIR. The real repository is never |
@@ -130,6 +130,36 @@ case "$cmd" in |
130 | 130 | fi |
131 | 131 | exit "${STUB_ISSUE_EXIT:-0}" |
132 | 132 | ;; |
| 133 | + fail_after_branch_write) |
| 134 | + printf 'version: 1\nbranch: feat/probe\nissues: [530]\n' > .specgit.yaml |
| 135 | + exit "${STUB_ISSUE_EXIT:-3}" |
| 136 | + ;; |
| 137 | + fail_after_push_delete) |
| 138 | + rm -f .specgit.yaml |
| 139 | + exit "${STUB_ISSUE_EXIT:-3}" |
| 140 | + ;; |
| 141 | + fail_after_pr_write) |
| 142 | + printf 'version: 1\nbranch: feat/probe\nissues: [530, 533]\n' > .specgit.yaml |
| 143 | + exit "${STUB_ISSUE_EXIT:-3}" |
| 144 | + ;; |
| 145 | + fail_after_push_commit) |
| 146 | + printf 'version: 1\nbranch: feat/probe\nissues: [530]\n' > .specgit.yaml |
| 147 | + git add .specgit.yaml |
| 148 | + git commit -qm "stub: record delivery binding" |
| 149 | + exit "${STUB_ISSUE_EXIT:-3}" |
| 150 | + ;; |
| 151 | + succeed_after_record_write) |
| 152 | + printf 'version: 1\nbranch: feat/probe\nissues: [530]\n' > .specgit.yaml |
| 153 | + exit 0 |
| 154 | + ;; |
| 155 | + tamper_record_snapshot) |
| 156 | + d=$(ls -dt "${TMPDIR:-/tmp}"/specgit-bootstrap.* 2>/dev/null | sed -n '1p') |
| 157 | + if [ -n "$d" ] && [ -d "$d/tree" ]; then |
| 158 | + printf 'tampered-record\n' > "$d/tree/.specgit.yaml" |
| 159 | + printf '%s\n' "$d" >> "${STUB_LOG}.snapdir" |
| 160 | + fi |
| 161 | + exit "${STUB_ISSUE_EXIT:-0}" |
| 162 | + ;; |
133 | 163 | esac |
134 | 164 | ;; |
135 | 165 | *) |
@@ -267,5 +297,114 @@ assert_rc 3 "$rc" && ok "case8: wrapper exits 3 on unbound repository" || bad "c |
267 | 297 | grep -q 'spec_git/policy.yaml\|\.specgit\.yaml' "$WORK/c8.err" && ok "case8: stderr names the missing binding" || bad "case8: stderr lacks binding context" |
268 | 298 | grep -q '^specgit-bootstrap:' "$WORK/c8.err" && ok "case8: diagnostics use specgit-bootstrap: prefix" || bad "case8: missing prefix" |
269 | 299 |
|
| 300 | +# ---- case 9: validation failure — record untouched, rc passthrough --------- |
| 301 | +new_fixture c9 |
| 302 | +make_stub c9 |
| 303 | +cp "$WORK/c9/.specgit.yaml" "$WORK/c9.record-baseline" |
| 304 | +surface_digests "$WORK/c9" > "$WORK/c9.baseline" |
| 305 | +STUB_ISSUE_EXIT=2 run_wrapper c9 "fix: probe" |
| 306 | +rc=$? |
| 307 | +assert_rc 2 "$rc" && ok "case9: wrapper exits 2 (validation failure passthrough)" || bad "case9: exit $rc, want 2" |
| 308 | +diff "$WORK/c9/.specgit.yaml" "$WORK/c9.record-baseline" >/dev/null && ok "case9: record bytes untouched" || bad "case9: record bytes changed" |
| 309 | +assert_surface_restored "$WORK/c9" "$WORK/c9.baseline" && ok "case9: surface bytes restored" || bad "case9: surface bytes differ" |
| 310 | +assert_clean "$WORK/c9" && ok "case9: fixture clean" || bad "case9: fixture dirty: $(git -C "$WORK/c9" status --porcelain | tr '\n' '|')" |
| 311 | + |
| 312 | +# ---- case 10: branch-creation failure — rewritten record rolled back -------- |
| 313 | +new_fixture c10 |
| 314 | +make_stub c10 |
| 315 | +cp "$WORK/c10/.specgit.yaml" "$WORK/c10.record-baseline" |
| 316 | +surface_digests "$WORK/c10" > "$WORK/c10.baseline" |
| 317 | +STUB_ISSUE_MODE=fail_after_branch_write STUB_ISSUE_EXIT=3 run_wrapper c10 "fix: probe" |
| 318 | +rc=$? |
| 319 | +assert_rc 3 "$rc" && ok "case10: wrapper exits 3 (branch failure passthrough)" || bad "case10: exit $rc, want 3" |
| 320 | +diff "$WORK/c10/.specgit.yaml" "$WORK/c10.record-baseline" >/dev/null && ok "case10: rewritten record rolled back to pre-run bytes" || bad "case10: record not rolled back: $(tr '\n' '|' < "$WORK/c10/.specgit.yaml")" |
| 321 | +assert_surface_restored "$WORK/c10" "$WORK/c10.baseline" && ok "case10: surface bytes restored" || bad "case10: surface bytes differ" |
| 322 | +assert_clean "$WORK/c10" && ok "case10: fixture clean" || bad "case10: fixture dirty: $(git -C "$WORK/c10" status --porcelain | tr '\n' '|')" |
| 323 | + |
| 324 | +# ---- case 11: push failure deleting the record — file recreated (#519) ------ |
| 325 | +new_fixture c11 |
| 326 | +make_stub c11 |
| 327 | +cp "$WORK/c11/.specgit.yaml" "$WORK/c11.record-baseline" |
| 328 | +surface_digests "$WORK/c11" > "$WORK/c11.baseline" |
| 329 | +STUB_ISSUE_MODE=fail_after_push_delete STUB_ISSUE_EXIT=3 run_wrapper c11 "fix: probe" |
| 330 | +rc=$? |
| 331 | +assert_rc 3 "$rc" && ok "case11: wrapper exits 3 (push failure passthrough)" || bad "case11: exit $rc, want 3" |
| 332 | +[ -f "$WORK/c11/.specgit.yaml" ] && ok "case11: deleted record recreated" || bad "case11: record still missing" |
| 333 | +diff "$WORK/c11/.specgit.yaml" "$WORK/c11.record-baseline" >/dev/null 2>&1 && ok "case11: record bytes byte-identical after deletion rollback" || bad "case11: record bytes: $(tr '\n' '|' < "$WORK/c11/.specgit.yaml" 2>/dev/null)" |
| 334 | +assert_surface_restored "$WORK/c11" "$WORK/c11.baseline" && ok "case11: surface bytes restored" || bad "case11: surface bytes differ" |
| 335 | +assert_clean "$WORK/c11" && ok "case11: fixture clean" || bad "case11: fixture dirty: $(git -C "$WORK/c11" status --porcelain | tr '\n' '|')" |
| 336 | + |
| 337 | +# ---- case 12: PR-creation failure — rewritten record rolled back ------------ |
| 338 | +new_fixture c12 |
| 339 | +make_stub c12 |
| 340 | +cp "$WORK/c12/.specgit.yaml" "$WORK/c12.record-baseline" |
| 341 | +surface_digests "$WORK/c12" > "$WORK/c12.baseline" |
| 342 | +STUB_ISSUE_MODE=fail_after_pr_write STUB_ISSUE_EXIT=3 run_wrapper c12 "fix: probe" |
| 343 | +rc=$? |
| 344 | +assert_rc 3 "$rc" && ok "case12: wrapper exits 3 (PR failure passthrough)" || bad "case12: exit $rc, want 3" |
| 345 | +diff "$WORK/c12/.specgit.yaml" "$WORK/c12.record-baseline" >/dev/null && ok "case12: rewritten record rolled back to pre-run bytes" || bad "case12: record not rolled back: $(tr '\n' '|' < "$WORK/c12/.specgit.yaml")" |
| 346 | +assert_surface_restored "$WORK/c12" "$WORK/c12.baseline" && ok "case12: surface bytes restored" || bad "case12: surface bytes differ" |
| 347 | +assert_clean "$WORK/c12" && ok "case12: fixture clean" || bad "case12: fixture dirty: $(git -C "$WORK/c12" status --porcelain | tr '\n' '|')" |
| 348 | + |
| 349 | +# ---- case 13: success keeps the new binding --------------------------------- |
| 350 | +new_fixture c13 |
| 351 | +make_stub c13 |
| 352 | +surface_digests "$WORK/c13" > "$WORK/c13.baseline" |
| 353 | +STUB_ISSUE_MODE=succeed_after_record_write run_wrapper c13 "feat: probe" |
| 354 | +rc=$? |
| 355 | +assert_rc 0 "$rc" && ok "case13: wrapper exits 0" || bad "case13: exit $rc, want 0" |
| 356 | +grep -q 'issues: \[530\]' "$WORK/c13/.specgit.yaml" && ok "case13: successful bootstrap keeps new binding" || bad "case13: new binding lost: $(tr '\n' '|' < "$WORK/c13/.specgit.yaml")" |
| 357 | +assert_surface_restored "$WORK/c13" "$WORK/c13.baseline" && ok "case13: surface bytes restored" || bad "case13: surface bytes differ" |
| 358 | +[ "$(git -C "$WORK/c13" status --porcelain | grep -cv 'specgit.yaml')" = "0" ] && ok "case13: only the record binding differs post-success" || bad "case13: unexpected residual changes: $(git -C "$WORK/c13" status --porcelain | tr '\n' '|')" |
| 359 | + |
| 360 | +# ---- case 14: pre-run dirty record — dirty bytes restored, not committed ---- |
| 361 | +new_fixture c14 |
| 362 | +make_stub c14 |
| 363 | +printf 'version: 1\nbranch: feat/dirty-pre\nissues: [519]\n' > "$WORK/c14/.specgit.yaml" |
| 364 | +cp "$WORK/c14/.specgit.yaml" "$WORK/c14.record-baseline" |
| 365 | +surface_digests "$WORK/c14" > "$WORK/c14.baseline" |
| 366 | +STUB_ISSUE_MODE=fail_after_branch_write STUB_ISSUE_EXIT=3 run_wrapper c14 "fix: probe" |
| 367 | +rc=$? |
| 368 | +assert_rc 3 "$rc" && ok "case14: wrapper exits 3" || bad "case14: exit $rc, want 3" |
| 369 | +diff "$WORK/c14/.specgit.yaml" "$WORK/c14.record-baseline" >/dev/null && ok "case14: pre-run dirty bytes restored (not committed bytes)" || bad "case14: record bytes: $(tr '\n' '|' < "$WORK/c14/.specgit.yaml")" |
| 370 | +assert_surface_restored "$WORK/c14" "$WORK/c14.baseline" && ok "case14: surface bytes restored" || bad "case14: surface bytes differ" |
| 371 | + |
| 372 | +# ---- case 15: untracked record (git rm --cached) — restored byte-for-byte --- |
| 373 | +new_fixture c15 |
| 374 | +make_stub c15 |
| 375 | +git -C "$WORK/c15" rm -q --cached .specgit.yaml |
| 376 | +cp "$WORK/c15/.specgit.yaml" "$WORK/c15.record-baseline" |
| 377 | +STUB_ISSUE_MODE=fail_after_pr_write STUB_ISSUE_EXIT=3 run_wrapper c15 "fix: probe" |
| 378 | +rc=$? |
| 379 | +assert_rc 3 "$rc" && ok "case15: wrapper exits 3" || bad "case15: exit $rc, want 3" |
| 380 | +[ -f "$WORK/c15/.specgit.yaml" ] && ok "case15: untracked record file exists after restore" || bad "case15: record missing" |
| 381 | +diff "$WORK/c15/.specgit.yaml" "$WORK/c15.record-baseline" >/dev/null && ok "case15: untracked record bytes restored" || bad "case15: record bytes: $(tr '\n' '|' < "$WORK/c15/.specgit.yaml")" |
| 382 | + |
| 383 | +# ---- case 16: record snapshot tampering — loud mismatch, exit 3, snap kept -- |
| 384 | +new_fixture c16 |
| 385 | +make_stub c16 |
| 386 | +STUB_ISSUE_MODE=tamper_record_snapshot STUB_ISSUE_EXIT=1 run_wrapper c16 "fix: probe" |
| 387 | +rc=$? |
| 388 | +assert_rc 3 "$rc" && ok "case16: wrapper exits 3 on record restore mismatch" || bad "case16: exit $rc, want 3" |
| 389 | +grep -q 'RESTORE MISMATCH for .specgit.yaml' "$WORK/c16.err" && ok "case16: record mismatch reported loudly on stderr" || bad "case16: no record RESTORE MISMATCH diagnostic" |
| 390 | +snapdir=$(sed -n '1p' "$WORK/stubs/c16/log.snapdir" 2>/dev/null) |
| 391 | +[ -n "$snapdir" ] && [ -d "$snapdir" ] && ok "case16: forensic snapshot kept" || bad "case16: snapshot removed: ${snapdir:-<none>}" |
| 392 | +grep -q 'tampered-record' "$WORK/c16/.specgit.yaml" && ok "case16: corrupted record surfaced (deliberate artifact)" || bad "case16: record bytes unexpected" |
| 393 | + |
| 394 | +# ---- case 17: failed push that committed — commit kept, worktree restored --- |
| 395 | +new_fixture c17 |
| 396 | +make_stub c17 |
| 397 | +cp "$WORK/c17/.specgit.yaml" "$WORK/c17.record-baseline" |
| 398 | +heads0=$(git -C "$WORK/c17" rev-list --count HEAD) |
| 399 | +STUB_ISSUE_MODE=fail_after_push_commit STUB_ISSUE_EXIT=3 run_wrapper c17 "fix: probe" |
| 400 | +rc=$? |
| 401 | +assert_rc 3 "$rc" && ok "case17: wrapper exits 3" || bad "case17: exit $rc, want 3" |
| 402 | +heads1=$(git -C "$WORK/c17" rev-list --count HEAD) |
| 403 | +assert_rc $((heads0 + 1)) "$heads1" && ok "case17: commit made during failed bootstrap is NOT undone" || bad "case17: HEAD count $heads1, want $((heads0 + 1))" |
| 404 | +git -C "$WORK/c17" show HEAD:.specgit.yaml > "$WORK/c17.head-record" 2>/dev/null && \ |
| 405 | + grep -q 'issues: \[530\]' "$WORK/c17.head-record" && ok "case17: committed record content intact in HEAD" || bad "case17: HEAD record missing stub content" |
| 406 | +diff "$WORK/c17/.specgit.yaml" "$WORK/c17.record-baseline" >/dev/null && ok "case17: only the record's worktree diff restored" || bad "case17: worktree record not restored: $(tr '\n' '|' < "$WORK/c17/.specgit.yaml")" |
| 407 | +[ "$(git -C "$WORK/c17" status --porcelain | grep -cv 'specgit.yaml')" = "0" ] && ok "case17: non-record paths clean (worktree == committed surface)" || bad "case17: unexpected residual changes: $(git -C "$WORK/c17" status --porcelain | tr '\n' '|')" |
| 408 | + |
270 | 409 | report |
271 | 410 | exit $? |
0 commit comments