muse-crew 0.7.19 → 0.8.0

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,210 @@
1
+ #!/usr/bin/env bash
2
+ # test-publish-preflight.sh — regression tests for the npm publish
3
+ # capability preflight (bugfix 2026-09-17): publish-npm.sh assumed the
4
+ # author-machine publish helper path and failed cryptically AFTER the
5
+ # version-bump release commit when npm publish was not configured.
6
+ #
7
+ # Covers:
8
+ # - structural: the PREFLIGHT block exists in lib/publish-npm.sh
9
+ # (extracted by anchor), sits after env validation and before the
10
+ # DRY_RUN block, checks the helper with -f/-r only (never -x — the
11
+ # helper is invoked as `python3 <path>`), and reads no wall clock or
12
+ # randomness.
13
+ # - full-script runs against a scratch repo:
14
+ # * NPM_PUBLISH_PY missing -> PUBLISH_SKIPPED=no-npm-publish + the
15
+ # actionable line, exit 0, HEAD unchanged, status clean, no tarball
16
+ # * fake helper whose --check reports CREDENTIAL_MISSING=custom.npm
17
+ # (exit 1) -> same skip outcome, exit 0, repo untouched
18
+ # * fake helper whose --check reports CREDENTIAL_OK=custom.npm
19
+ # (exit 0) with DRY_RUN=1 -> preflight passes through to
20
+ # PUBLISH_DRY_RUN=1 (asserts pass-through, not the mutation)
21
+ # * old helper that does not know --check (usage to stderr, exit 1,
22
+ # no CREDENTIAL markers) with DRY_RUN=1 -> no skip (fall-through
23
+ # preserved: unknown is not absent)
24
+ # - --check probe unit test against the real helper: on the author
25
+ # machine this expects exit 0 and exactly CREDENTIAL_OK=custom.npm.
26
+ # NOTE: this case depends on the `custom.npm` credential existing in
27
+ # the secure vault — on a machine without it the probe correctly
28
+ # reports CREDENTIAL_MISSING=custom.npm (exit 1) and this case fails.
29
+ # That is expected outside the author machine, not a regression.
30
+ # - bash -n syntax check on publish-npm.sh
31
+ #
32
+ # Anchors: `# PREFLIGHT-ANCHOR` ... `# PREFLIGHT-END` in lib/publish-npm.sh.
33
+
34
+ set -uo pipefail
35
+
36
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
37
+ PUBLISH="$SCRIPT_DIR/publish-npm.sh"
38
+ REAL_HELPER="$HOME/workspace/skills/npm/bin/npm-publish.py"
39
+
40
+ pass=0
41
+ fail_count=0
42
+ ok() { echo "PASS: $1"; pass=$((pass + 1)); }
43
+ no() { echo "FAIL: $1"; fail_count=$((fail_count + 1)); }
44
+
45
+ TMPBASE="$(mktemp -d)"
46
+ trap 'rm -rf "$TMPBASE"' EXIT
47
+
48
+ # --- Scratch repo (Publish's REPO_PATH must be a git checkout) ---
49
+ SCRATCH="$TMPBASE/repo"
50
+ git init -q -b main "$SCRATCH"
51
+ git -C "$SCRATCH" config user.email "test@crew"
52
+ git -C "$SCRATCH" config user.name "crew-test"
53
+ echo x > "$SCRATCH/f.txt"
54
+ git -C "$SCRATCH" add -A && git -C "$SCRATCH" commit -qm init
55
+
56
+ # --- Fake helpers ---
57
+ cat > "$TMPBASE/helper-ok.py" <<'EOF'
58
+ #!/usr/bin/env python3
59
+ import sys
60
+ if len(sys.argv) == 2 and sys.argv[1] == "--check":
61
+ print("CREDENTIAL_OK=custom.npm")
62
+ sys.exit(0)
63
+ sys.exit(1)
64
+ EOF
65
+ cat > "$TMPBASE/helper-missing-cred.py" <<'EOF'
66
+ #!/usr/bin/env python3
67
+ import sys
68
+ if len(sys.argv) == 2 and sys.argv[1] == "--check":
69
+ print("CREDENTIAL_MISSING=custom.npm")
70
+ sys.exit(1)
71
+ sys.exit(1)
72
+ EOF
73
+ # An old helper that predates --check: usage to stderr, exit 1, no markers.
74
+ cat > "$TMPBASE/helper-old.py" <<'EOF'
75
+ #!/usr/bin/env python3
76
+ import sys
77
+ print("Usage: helper-old.py <tarball-path>", file=sys.stderr)
78
+ sys.exit(1)
79
+ EOF
80
+
81
+ ACTIONABLE="npm publish not configured on this machine — see docs/guide.md#npm-releases; code changes are merged, nothing was versioned or published"
82
+
83
+ # --- Structural: extract the shipped preflight block by anchor ---
84
+ BLOCK="$TMPBASE/preflight.sh"
85
+ sed -n '/# PREFLIGHT-ANCHOR/,/# PREFLIGHT-END/p' "$PUBLISH" > "$BLOCK"
86
+ if grep -q 'PUBLISH_SKIPPED=no-npm-publish' "$BLOCK" \
87
+ && grep -q 'docs/guide.md#npm-releases' "$BLOCK" \
88
+ && grep -q 'exit 0' "$BLOCK"; then
89
+ ok "extracted PREFLIGHT block from publish-npm.sh"
90
+ else
91
+ no "PREFLIGHT extraction failed (anchors missing)"
92
+ fi
93
+
94
+ # The helper is invoked as `python3 <path>`: -f/-r, never -x.
95
+ if grep -q '\[ -f "\$NPM_PUBLISH_PY" \]' "$BLOCK" \
96
+ && grep -q '\[ -r "\$NPM_PUBLISH_PY" \]' "$BLOCK" \
97
+ && ! grep -q '\[ -x "\$NPM_PUBLISH_PY" \]' "$BLOCK"; then
98
+ ok "preflight checks -f/-r on the helper, never -x"
99
+ else
100
+ no "preflight helper checks wrong (want -f/-r, no -x)"
101
+ fi
102
+
103
+ # No wall-clock or randomness in the preflight block.
104
+ if ! grep -Eq '\$\(date|date \+|\$RANDOM' "$BLOCK"; then
105
+ ok "preflight block reads no wall clock and no randomness"
106
+ else
107
+ no "preflight block contains clock/randomness"
108
+ fi
109
+
110
+ # Placement: after env validation, before the DRY_RUN block.
111
+ VALIDATE_LINE="$(grep -n 'rev-parse --git-dir' "$PUBLISH" | head -1 | cut -d: -f1)"
112
+ ANCHOR_LINE="$(grep -n '# PREFLIGHT-ANCHOR' "$PUBLISH" | head -1 | cut -d: -f1)"
113
+ END_LINE="$(grep -n '# PREFLIGHT-END' "$PUBLISH" | head -1 | cut -d: -f1)"
114
+ DRYRUN_LINE="$(grep -n 'DRY_RUN:-0}' "$PUBLISH" | head -1 | cut -d: -f1)"
115
+ if [ -n "$VALIDATE_LINE" ] && [ -n "$ANCHOR_LINE" ] && [ -n "$END_LINE" ] \
116
+ && [ -n "$DRYRUN_LINE" ] \
117
+ && [ "$VALIDATE_LINE" -lt "$ANCHOR_LINE" ] \
118
+ && [ "$END_LINE" -lt "$DRYRUN_LINE" ]; then
119
+ ok "PREFLIGHT block sits after env validation, before DRY_RUN"
120
+ else
121
+ no "PREFLIGHT placement wrong (validate=$VALIDATE_LINE anchor=$ANCHOR_LINE end=$END_LINE dryrun=$DRYRUN_LINE)"
122
+ fi
123
+
124
+ # Runner: full publish-npm.sh against the scratch repo. The preflight exits
125
+ # before any mutation, so the repo needs no package.json.
126
+ run_full() { # run_full <helper_path> [extra_env...]
127
+ local helper="$1"; shift
128
+ local run="$TMPBASE/run-$RANDOM"
129
+ mkdir -p "$run"
130
+ (
131
+ export TASK_ID="preflight-test" REPO_PATH="$SCRATCH" PKG="muse-crew" TARGET_VERSION="9.9.9"
132
+ export NPM_PUBLISH_PY="$helper"
133
+ # A bare `export` (empty "$@") would dump the environment to stdout and
134
+ # corrupt the captured run path — only export when extras are given.
135
+ if [ $# -gt 0 ]; then export "$@"; fi
136
+ bash "$PUBLISH" > "$run/out" 2>&1
137
+ )
138
+ local code=$?
139
+ echo "$code" > "$run/code"
140
+ echo "$run"
141
+ }
142
+
143
+ repo_state() { # prints "HEAD=<sha> STATUS=<porcelain-lines> TGZ=<count>"
144
+ local head status tgz
145
+ head="$(git -C "$SCRATCH" rev-parse HEAD)"
146
+ status="$(git -C "$SCRATCH" status --porcelain | wc -l)"
147
+ tgz="$(find "$SCRATCH" -maxdepth 1 -name '*.tgz' | wc -l)"
148
+ echo "HEAD=$head STATUS=$status TGZ=$tgz"
149
+ }
150
+
151
+ # --- Case 1: helper missing -> clean skip, exit 0, repo untouched ---
152
+ BEFORE="$(repo_state)"
153
+ RUN="$(run_full /nonexistent/path.py)"
154
+ CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state)"
155
+ if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_SKIPPED=no-npm-publish"* ]] \
156
+ && [[ "$OUT" == *"$ACTIONABLE"* ]] \
157
+ && [[ "$OUT" != *"PUBLISH_FAILED"* ]] && [[ "$OUT" != *"PUBLISH_COMPLETE"* ]] \
158
+ && [ "$BEFORE" = "$AFTER" ]; then
159
+ ok "helper missing: skips with marker + actionable line, exit 0, repo untouched"
160
+ else
161
+ no "helper missing: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
162
+ fi
163
+
164
+ # --- Case 2: helper present, credential missing -> same skip, repo untouched ---
165
+ BEFORE="$(repo_state)"
166
+ RUN="$(run_full "$TMPBASE/helper-missing-cred.py")"
167
+ CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"; AFTER="$(repo_state)"
168
+ if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_SKIPPED=no-npm-publish"* ]] \
169
+ && [[ "$OUT" == *"$ACTIONABLE"* ]] \
170
+ && [[ "$OUT" != *"PUBLISH_FAILED"* ]] && [[ "$OUT" != *"PUBLISH_COMPLETE"* ]] \
171
+ && [ "$BEFORE" = "$AFTER" ]; then
172
+ ok "credential missing: skips with marker + actionable line, exit 0, repo untouched"
173
+ else
174
+ no "credential missing: code=$CODE state=$BEFORE/$AFTER out='$OUT'"
175
+ fi
176
+
177
+ # --- Case 3: credential present + DRY_RUN=1 -> preflight passes through ---
178
+ RUN="$(run_full "$TMPBASE/helper-ok.py" DRY_RUN=1)"
179
+ CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"
180
+ if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_DRY_RUN=1"* ]] \
181
+ && [[ "$OUT" != *"PUBLISH_SKIPPED"* ]]; then
182
+ ok "credential present: preflight passes through to PUBLISH_DRY_RUN=1"
183
+ else
184
+ no "credential present: code=$CODE out='$OUT'"
185
+ fi
186
+
187
+ # --- Case 4: old helper without --check -> no skip (fall-through) ---
188
+ RUN="$(run_full "$TMPBASE/helper-old.py" DRY_RUN=1)"
189
+ CODE="$(cat "$RUN/code")"; OUT="$(cat "$RUN/out")"
190
+ if [ "$CODE" -eq 0 ] && [[ "$OUT" == *"PUBLISH_DRY_RUN=1"* ]] \
191
+ && [[ "$OUT" != *"PUBLISH_SKIPPED"* ]]; then
192
+ ok "old helper (no --check): unknown, not absent — falls through, no skip"
193
+ else
194
+ no "old helper: code=$CODE out='$OUT'"
195
+ fi
196
+
197
+ # --- Case 5: real helper --check probe (author machine: credential present) ---
198
+ CHECK_OUT="$(python3 "$REAL_HELPER" --check 2>&1)"
199
+ CHECK_CODE=$?
200
+ if [ "$CHECK_CODE" -eq 0 ] && [ "$CHECK_OUT" = "CREDENTIAL_OK=custom.npm" ]; then
201
+ ok "real helper --check: CREDENTIAL_OK=custom.npm, exit 0, nothing else printed"
202
+ else
203
+ no "real helper --check: code=$CHECK_CODE out='$CHECK_OUT' (expected CREDENTIAL_OK=custom.npm — this case is credential-dependent; see header)"
204
+ fi
205
+
206
+ bash -n "$PUBLISH" && ok "publish-npm.sh syntax OK" || no "publish-npm.sh syntax"
207
+
208
+ echo ""
209
+ echo "$pass passed, $fail_count failed"
210
+ exit $((fail_count > 0))
@@ -147,7 +147,57 @@ echo "$out" | grep -q '^MERGED: ' || fail "integrate-test: no MERGED line (no re
147
147
  echo "$out" | grep -q '^NO_REMOTE: ' || fail "integrate-test: no NO_REMOTE line: $out"
148
148
  "$CREW_LIB/merge-lock.sh" release "int2" >/dev/null || fail "integrate-test: lock release (no remote) failed"
149
149
 
150
- rm -rf "$T2" "$T3"
150
+ # Prefix resolution (task 4e1a1bba, 2026-09-17): a Build agent created the
151
+ # branch with raw git as task/<8-char id>, bypassing prepare, so the
152
+ # registry is empty. resolve-branch must find it by id-prefix enumeration;
153
+ # inspect/integrate/cleanup must operate on the real branch, never on a
154
+ # reconstructed task/<full-id>.
155
+ T4=/tmp/crew-git-prefix-test
156
+ rm -rf "$T4"
157
+ mkdir -p "$T4"
158
+ git init -q -b main "$T4/repo"
159
+ git -C "$T4/repo" config user.email test@test.t
160
+ git -C "$T4/repo" config user.name test
161
+ echo base > "$T4/repo/f.txt"
162
+ echo ".worktrees/" > "$T4/repo/.gitignore"
163
+ git -C "$T4/repo" add -A
164
+ git -C "$T4/repo" commit -qm base
165
+ export CREW_REPO="$T4/repo"
166
+
167
+ fullid="51aaa9fe-9cb5-4a4f-acfc-72092fcf0899"
168
+ shortid="51aaa9fe"
169
+ # Raw-git branch creation, like the Build agent: no registry entry.
170
+ git -C "$T4/repo" branch "task/$shortid"
171
+ [ -f "$T4/repo/.worktrees/.registry/$fullid" ] && fail "prefix-test: registry should be empty for this fixture"
172
+ out=$(bash "$LIFECYCLE" resolve-branch "$fullid") || fail "prefix-test: resolve-branch failed: $out"
173
+ [ "$out" = "task/$shortid" ] || fail "prefix-test: resolve-branch returned '$out', expected task/$shortid"
174
+
175
+ # inspect operates on the real branch (no "branch ... does not exist")
176
+ out=$(bash "$LIFECYCLE" inspect "$fullid") || fail "prefix-test: inspect failed on abbreviated branch: $out"
177
+ echo "$out" | grep -q "=== Branch: task/$shortid ===" || fail "prefix-test: inspect used the wrong branch"
178
+
179
+ # verify-merge vacuous-pass on the merged-prefix branch tip == main
180
+ out=$(bash "$LIFECYCLE" verify-merge "$fullid") || fail "prefix-test: verify-merge failed on abbreviated branch: $out"
181
+
182
+ # cleanup deletes the real abbreviated branch, not a reconstructed one
183
+ out=$(bash "$LIFECYCLE" cleanup "$fullid") || fail "prefix-test: cleanup failed: $out"
184
+ git -C "$T4/repo" rev-parse --verify "task/$shortid" >/dev/null 2>&1 && fail "prefix-test: cleanup left the abbreviated branch behind"
185
+
186
+ # Ambiguous prefixes fail closed (exit non-zero), never guess
187
+ fullid2="deadbeef-1111-2222-3333-444455556666"
188
+ git -C "$T4/repo" branch "task/deadbeef"
189
+ git -C "$T4/repo" branch "task/deadbeef-1111"
190
+ if bash "$LIFECYCLE" resolve-branch "$fullid2" >/dev/null 2>&1; then
191
+ fail "prefix-test: resolve-branch guessed on ambiguous prefix"
192
+ fi
193
+ git -C "$T4/repo" branch -D "task/deadbeef" "task/deadbeef-1111" >/dev/null
194
+
195
+ # No branch at all: resolve-branch falls back to the canonical name so
196
+ # callers keep their "branch X does not exist" failure
197
+ out=$(bash "$LIFECYCLE" resolve-branch "$fullid") || fail "prefix-test: resolve-branch failed with no branch present"
198
+ [ "$out" = "task/$fullid" ] || fail "prefix-test: resolve-branch returned '$out', expected canonical fallback task/$fullid"
199
+
200
+ rm -rf "$T2" "$T3" "$T4"
151
201
  export CREW_REPO="$T"
152
202
  rm -rf "$T"
153
203
  echo "worktree-backend: OK"