@metasession.co/devaudit-cli 0.2.0 → 0.3.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.
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/sdlc/files/_common/scripts/close-out-release.sh +8 -1
- package/sdlc/files/_common/scripts/close-out-release.test.sh +59 -0
- package/sdlc/files/_common/scripts/derive-release-version.sh +2 -1
- package/sdlc/files/_common/scripts/derive-release-version.test.sh +29 -0
- package/sdlc/files/_common/scripts/validate-test-summary.sh +146 -0
- package/sdlc/files/_common/scripts/validate-test-summary.test.sh +268 -0
- package/sdlc/files/_common/skills/e2e-test-engineer/SKILL.md +28 -2
- package/sdlc/files/_common/skills/sdlc-implementer/SKILL.md +20 -1
- package/sdlc/files/ci/ci.yml.template +26 -1
- package/sdlc/files/ci/compliance-validation.yml.template +3 -0
- package/sdlc/files/stacks/node/hooks/pre-push +19 -0
- package/sdlc/package.json +1 -1
- package/sdlc/src/blueprints/2-implement-and-test.raw.md +12 -0
- package/sdlc/.claude/settings.local.json +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metasession.co/devaudit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.8.2",
|
|
36
|
-
"@metasession.co/devaudit-plugin-sdk": "
|
|
36
|
+
"@metasession.co/devaudit-plugin-sdk": "^0.3.0",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
38
|
"commander": "^12.1.0",
|
|
39
39
|
"consola": "^3.2.3",
|
|
@@ -131,6 +131,12 @@ if [ -f "$RTM" ] && grep -qE "^\| ${REQ_ID} " "$RTM"; then
|
|
|
131
131
|
# (`|---|---|…`) are left intact and don't affect `statuscol`.
|
|
132
132
|
awk -v req="$REQ_ID" '
|
|
133
133
|
BEGIN { FS="|"; OFS="|"; statuscol=0 }
|
|
134
|
+
# Protect escaped pipes (\|) from being treated as column delimiters.
|
|
135
|
+
# Replacing \| with \001 in $0 forces awk to re-split on only unescaped
|
|
136
|
+
# pipes, so literal pipes inside cell content (e.g. regex patterns like
|
|
137
|
+
# stop\|unsubscribe, enum values open\|in_progress\|closed) do not create
|
|
138
|
+
# phantom extra columns. Placeholders are restored to \| before output.
|
|
139
|
+
/\\\|/ { gsub(/\\\|/, "\001", $0) }
|
|
134
140
|
# Header detection: scan every row; require both a "Status" header cell
|
|
135
141
|
# AND an ID-like header cell in the same row before locking statuscol to
|
|
136
142
|
# this row''s column index.
|
|
@@ -149,9 +155,10 @@ if [ -f "$RTM" ] && grep -qE "^\| ${REQ_ID} " "$RTM"; then
|
|
|
149
155
|
if (match(cell, /\(/)) note=substr(cell, RSTART) # preserve any " (requirement note)"
|
|
150
156
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", note)
|
|
151
157
|
$statuscol = (note != "" ? " RELEASED " note " " : " RELEASED ")
|
|
158
|
+
gsub(/\001/, "\\|", $0)
|
|
152
159
|
print; next
|
|
153
160
|
}
|
|
154
|
-
{ print }
|
|
161
|
+
{ gsub(/\001/, "\\|", $0); print }
|
|
155
162
|
' "$RTM" > "$TMP" && mv "$TMP" "$RTM"
|
|
156
163
|
git add "$RTM" 2>/dev/null || true
|
|
157
164
|
echo "RTM row ${REQ_ID} -> RELEASED."
|
|
@@ -136,6 +136,65 @@ EOF
|
|
|
136
136
|
rm -rf "$(dirname "$dir")"
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
# ── Case 3: RTM row with escaped pipes (\|) in Status column ─────────────────
|
|
140
|
+
# The Status cell contains literal pipe characters escaped as \| (markdown
|
|
141
|
+
# table convention). Without the fix, awk's FS="|" splits on these escaped
|
|
142
|
+
# pipes too, creating phantom extra columns and mangling the row on close-out.
|
|
143
|
+
{
|
|
144
|
+
dir="$(mktemp -d)/cli-close-out-fixture-3"
|
|
145
|
+
mkdir -p "$dir/compliance/pending-releases" "$dir/compliance/approved-releases"
|
|
146
|
+
cd "$dir"
|
|
147
|
+
git init -q --initial-branch=main >/dev/null
|
|
148
|
+
git config user.email "test@example.com"
|
|
149
|
+
git config user.name "test"
|
|
150
|
+
cat > compliance/RTM.md <<'EOF'
|
|
151
|
+
# Requirements Traceability Matrix
|
|
152
|
+
|
|
153
|
+
| REQ-ID | Issue | Risk | Evidence | Status | Approver | Date |
|
|
154
|
+
| ------- | ----- | ----------- | ---------------------------- | --------------------------------------------------------------------- | ---------- | ---------- |
|
|
155
|
+
| REQ-056 | #117 | MEDIUM-HIGH | compliance/evidence/REQ-056/ | IN PROGRESS (regex: /^\s*(stop\|unsubscribe\|opt[-\s]?out)\s*$/i) | ostendo-io | 2026-06-01 |
|
|
156
|
+
| REQ-064 | #121 | MEDIUM | compliance/evidence/REQ-064/ | IN PROGRESS (enum: open\|in_progress\|awaiting_customer\|resolved\|closed) | dev | 2026-06-02 |
|
|
157
|
+
| REQ-075 | #135 | LOW | compliance/evidence/REQ-075/ | TESTED - PENDING SIGN-OFF (union: 'food'\|'drinks') | dev | 2026-06-03 |
|
|
158
|
+
EOF
|
|
159
|
+
cat > compliance/pending-releases/RELEASE-TICKET-REQ-056.md <<'EOF'
|
|
160
|
+
# Release Ticket: REQ-056
|
|
161
|
+
|
|
162
|
+
**Status:** TESTED - PENDING SIGN-OFF
|
|
163
|
+
**DevAudit Release:** REQ-056
|
|
164
|
+
EOF
|
|
165
|
+
git add -A
|
|
166
|
+
git commit -q -m "fixture: escaped pipes in status"
|
|
167
|
+
unset DEVAUDIT_API_KEY DEVAUDIT_BASE_URL || true
|
|
168
|
+
bash "$HELPER" REQ-056 >/dev/null 2>&1 || true
|
|
169
|
+
# Assert: REQ-056 col-1 stays REQ-056, col-5 flips to RELEASED with note preserved
|
|
170
|
+
# Use sed to neutralize \| before awk field splitting (same technique as the
|
|
171
|
+
# production fix in derive-release-version.sh).
|
|
172
|
+
row=$(grep -m1 -E "^\| REQ-056 " compliance/RTM.md || true)
|
|
173
|
+
col1=$(echo "$row" | sed 's/\\|/ /g' | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2}')
|
|
174
|
+
col5=$(echo "$row" | sed 's/\\|/ /g' | awk -F '|' '{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$6); print $6}')
|
|
175
|
+
assert_eq "escaped-pipe: REQ-056 col-1 unchanged" "REQ-056" "$col1"
|
|
176
|
+
# col5 starts with RELEASED (the parenthetical note is preserved by close-out)
|
|
177
|
+
case "$col5" in RELEASED*) assert_eq "escaped-pipe: REQ-056 col-5 flipped" "RELEASED" "RELEASED" ;; *) assert_eq "escaped-pipe: REQ-056 col-5 flipped" "RELEASED" "$col5" ;; esac
|
|
178
|
+
# Assert: the parenthetical note with escaped pipes is preserved
|
|
179
|
+
echo "$row" | grep -qF 'stop\|unsubscribe\|opt' \
|
|
180
|
+
&& assert_eq "escaped-pipe: note preserved" "yes" "yes" \
|
|
181
|
+
|| assert_eq "escaped-pipe: note preserved" "yes" "no"
|
|
182
|
+
# Assert: REQ-064 row untouched (still has escaped pipes in status)
|
|
183
|
+
row64=$(grep -m1 -E "^\| REQ-064 " compliance/RTM.md || true)
|
|
184
|
+
echo "$row64" | grep -qF 'open\|in_progress' \
|
|
185
|
+
&& assert_eq "escaped-pipe: REQ-064 untouched" "yes" "yes" \
|
|
186
|
+
|| assert_eq "escaped-pipe: REQ-064 untouched" "yes" "no"
|
|
187
|
+
# Assert: REQ-075 row untouched (text has 'food'\|'drinks' with quotes)
|
|
188
|
+
row75=$(grep -m1 -E "^\| REQ-075 " compliance/RTM.md || true)
|
|
189
|
+
echo "$row75" | grep -qF "food" && echo "$row75" | grep -qF "drinks" \
|
|
190
|
+
&& assert_eq "escaped-pipe: REQ-075 untouched" "yes" "yes" \
|
|
191
|
+
|| assert_eq "escaped-pipe: REQ-075 untouched" "yes" "no"
|
|
192
|
+
# Assert: row still has exactly 7 columns (9 fields with leading/trailing empty)
|
|
193
|
+
nfields=$(echo "$row" | sed 's/\\|/ /g' | awk -F '|' '{print NF}')
|
|
194
|
+
assert_eq "escaped-pipe: REQ-056 still 7 columns" "9" "$nfields"
|
|
195
|
+
rm -rf "$(dirname "$dir")"
|
|
196
|
+
}
|
|
197
|
+
|
|
139
198
|
echo
|
|
140
199
|
echo "Result: $PASS passed, $FAIL failed"
|
|
141
200
|
[ "$FAIL" = "0" ]
|
|
@@ -114,7 +114,8 @@ if [ -f "$RTM_PATH" ]; then
|
|
|
114
114
|
# Variable padding between REQ-ID and Status (Issue/Risk/Evidence
|
|
115
115
|
# columns) is fine — only the leading REQ-XXX and the status-cell
|
|
116
116
|
# marker matter.
|
|
117
|
-
IN_PROGRESS_REQS=$(
|
|
117
|
+
IN_PROGRESS_REQS=$(sed 's/\\|/ /g' "$RTM_PATH" 2>/dev/null \
|
|
118
|
+
| grep -E '\|[[:space:]]+IN PROGRESS' \
|
|
118
119
|
| grep -oE '^\|[[:space:]]*REQ-[0-9]+' \
|
|
119
120
|
| grep -oE 'REQ-[0-9]+' | sort -u || true)
|
|
120
121
|
if [ -n "$IN_PROGRESS_REQS" ]; then
|
|
@@ -237,6 +237,35 @@ RTM
|
|
|
237
237
|
GOT=$(RTM_PATH=docs/custom-RTM.md run_helper)
|
|
238
238
|
assert_eq "RTM_PATH=docs/custom-RTM.md -> REQ-400" "REQ-400" "$GOT"
|
|
239
239
|
|
|
240
|
+
# Case 19: step-4-bis with escaped pipes (\|) in the Status column.
|
|
241
|
+
# The Status cell contains literal pipe characters escaped as \| (markdown
|
|
242
|
+
# table convention). Without the sed pre-processing, the grep could match
|
|
243
|
+
# a pipe inside the cell content followed by " IN PROGRESS", producing
|
|
244
|
+
# false positives. With the fix, only unescaped pipe delimiters are matched.
|
|
245
|
+
make_fixture "$WORK/c19" "chore: devaudit update to 0.1.29"
|
|
246
|
+
mkdir -p compliance
|
|
247
|
+
cat > compliance/RTM.md <<'RTM'
|
|
248
|
+
# RTM
|
|
249
|
+
| REQ-ID | Issue | Risk | Evidence | Status | Approver | Date |
|
|
250
|
+
| ------- | ----- | ----------- | ---------------------------- | --------------------------------------------------------------------- | ---------- | ---------- |
|
|
251
|
+
| REQ-056 | #117 | MEDIUM-HIGH | compliance/evidence/REQ-056/ | IN PROGRESS (regex: /^\s*(stop\|unsubscribe\|opt[-\s]?out)\s*$/i) | ostendo-io | 2026-06-01 |
|
|
252
|
+
| REQ-064 | #121 | MEDIUM | compliance/evidence/REQ-064/ | RELEASED (enum: open\|in_progress\|awaiting_customer\|resolved\|closed) | dev | 2026-06-02 |
|
|
253
|
+
RTM
|
|
254
|
+
assert_eq "RTM escaped pipes -> REQ-056 (single IN PROGRESS)" "REQ-056" "$(run_helper)"
|
|
255
|
+
|
|
256
|
+
# Case 20: step-4-bis with escaped pipes — ambiguity guard still works.
|
|
257
|
+
# Two IN PROGRESS rows, both with escaped pipes in status → falls through
|
|
258
|
+
# to bare date rather than guessing.
|
|
259
|
+
make_fixture "$WORK/c20" "chore: devaudit update to 0.1.29"
|
|
260
|
+
mkdir -p compliance
|
|
261
|
+
cat > compliance/RTM.md <<'RTM'
|
|
262
|
+
| REQ-ID | Status |
|
|
263
|
+
| ------- | -------------------------------------------------------------------- |
|
|
264
|
+
| REQ-056 | IN PROGRESS (regex: stop\|unsubscribe\|opt[-\s]?out) |
|
|
265
|
+
| REQ-064 | IN PROGRESS (enum: open\|in_progress\|awaiting_customer\|resolved) |
|
|
266
|
+
RTM
|
|
267
|
+
assert_eq "RTM two IN PROGRESS w/ escaped pipes -> bare date $TODAY" "$TODAY" "$(run_helper)"
|
|
268
|
+
|
|
240
269
|
echo ""
|
|
241
270
|
echo "=== Summary: $PASS pass / $FAIL fail ==="
|
|
242
271
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# validate-test-summary.sh — Validates test-execution-summary.md content.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./scripts/validate-test-summary.sh [base-branch]
|
|
6
|
+
#
|
|
7
|
+
# Checks that test-execution-summary.md files for in-scope REQs do not
|
|
8
|
+
# contain invalid gate states like "deferred" or "Deferred to CI".
|
|
9
|
+
# E2E gate results must be one of: PASS, FAIL, NOT_NEEDED (with reason),
|
|
10
|
+
# or SKIPPED (with operator-approved rationale). "Deferred to CI" is not
|
|
11
|
+
# a valid SDLC state (devaudit-installer#240).
|
|
12
|
+
#
|
|
13
|
+
# Designed to run as a CI job on PRs to main, alongside
|
|
14
|
+
# validate-compliance-artifacts.sh.
|
|
15
|
+
#
|
|
16
|
+
# Install: cp this file to your project's scripts/ directory && chmod +x scripts/validate-test-summary.sh
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
BASE_BRANCH="${1:-origin/main}"
|
|
21
|
+
EXIT_CODE=0
|
|
22
|
+
|
|
23
|
+
echo "=== Test Execution Summary Validation ==="
|
|
24
|
+
echo "Comparing: $BASE_BRANCH...HEAD"
|
|
25
|
+
echo ""
|
|
26
|
+
|
|
27
|
+
# Extract the requirement(s) THIS PR implements — same logic as
|
|
28
|
+
# validate-compliance-artifacts.sh
|
|
29
|
+
PR_MSGS=$(git log "$BASE_BRANCH"..HEAD --format='%B' || true)
|
|
30
|
+
REQUIREMENTS=$(
|
|
31
|
+
{
|
|
32
|
+
printf '%s\n' "$PR_MSGS" | grep -oP '\[\KREQ-\d{3,}(?=\])' || true
|
|
33
|
+
printf '%s\n' "$PR_MSGS" | grep -iP '^\s*Ref:' | grep -oP 'REQ-\d{3,}' || true
|
|
34
|
+
} | sort -u
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if [ -z "$REQUIREMENTS" ]; then
|
|
38
|
+
echo "No REQ-XXX references found in PR commits — skipping test summary validation."
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "Requirements found in PR commits: $REQUIREMENTS"
|
|
43
|
+
echo ""
|
|
44
|
+
|
|
45
|
+
for REQ in $REQUIREMENTS; do
|
|
46
|
+
echo "--- Checking $REQ ---"
|
|
47
|
+
|
|
48
|
+
SUMMARY="compliance/evidence/$REQ/test-execution-summary.md"
|
|
49
|
+
|
|
50
|
+
if [ ! -f "$SUMMARY" ]; then
|
|
51
|
+
echo " INFO: $SUMMARY not found — skipping (validate-compliance-artifacts.sh checks existence)"
|
|
52
|
+
echo ""
|
|
53
|
+
continue
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo " OK: test-execution-summary.md exists"
|
|
57
|
+
|
|
58
|
+
# Check 1: Reject "deferred" (case-insensitive) anywhere in the file
|
|
59
|
+
if grep -qi 'deferred' "$SUMMARY"; then
|
|
60
|
+
echo " ERROR: 'deferred' found in $SUMMARY — 'Deferred to CI' is not a valid gate state (devaudit-installer#238, #240)."
|
|
61
|
+
echo " E2E gate results must be: PASS, FAIL, NOT_NEEDED (with reason), or SKIPPED (with operator-approved rationale)."
|
|
62
|
+
echo " Offending lines:"
|
|
63
|
+
grep -in 'deferred' "$SUMMARY" | sed 's/^/ /'
|
|
64
|
+
EXIT_CODE=1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Check 2: Reject "browsers not installed" as a gate outcome
|
|
68
|
+
if grep -qi 'browsers not installed' "$SUMMARY"; then
|
|
69
|
+
echo " ERROR: 'browsers not installed' found in $SUMMARY — this is an environment setup issue, not a gate state (devaudit-installer#238, #240)."
|
|
70
|
+
echo " Install browsers with 'npx playwright install' and re-run the E2E suite."
|
|
71
|
+
echo " Offending lines:"
|
|
72
|
+
grep -in 'browsers not installed' "$SUMMARY" | sed 's/^/ /'
|
|
73
|
+
EXIT_CODE=1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Check 3: Reject "Deferred to CI" explicitly (catches the exact pattern from #238)
|
|
77
|
+
if grep -qi 'deferred to ci' "$SUMMARY"; then
|
|
78
|
+
echo " ERROR: 'Deferred to CI' found in $SUMMARY — CI is a safety net, not a replacement for local E2E execution (devaudit-installer#238, #240)."
|
|
79
|
+
echo " Offending lines:"
|
|
80
|
+
grep -in 'deferred to ci' "$SUMMARY" | sed 's/^/ /'
|
|
81
|
+
EXIT_CODE=1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Check 4: Validate E2E gate result in the gate results table
|
|
85
|
+
# Look for lines matching "| E2E ... | <result> |" pattern
|
|
86
|
+
E2E_LINES=$(grep -i '| *e2e' "$SUMMARY" 2>/dev/null || true)
|
|
87
|
+
if [ -n "$E2E_LINES" ]; then
|
|
88
|
+
while IFS= read -r line; do
|
|
89
|
+
# Extract the result column (2nd | field after "E2E")
|
|
90
|
+
RESULT=$(echo "$line" | grep -oP '\|\s*\K[^|]+(?=\s*\|)' | head -2 | tail -1 | xargs || true)
|
|
91
|
+
if [ -n "$RESULT" ]; then
|
|
92
|
+
RESULT_LOWER=$(echo "$RESULT" | tr '[:upper:]' '[:lower:]')
|
|
93
|
+
# Skip empty or separator lines
|
|
94
|
+
if [ -z "$RESULT_LOWER" ] || echo "$RESULT_LOWER" | grep -q '^[|-]*$'; then
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
# Check for invalid "deferred" state
|
|
98
|
+
if echo "$RESULT_LOWER" | grep -qi 'defer'; then
|
|
99
|
+
echo " ERROR: E2E gate result '$RESULT' is not valid — 'deferred' is not a gate state (devaudit-installer#240)"
|
|
100
|
+
EXIT_CODE=1
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
done <<< "$E2E_LINES"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Check 5: If E2E result is SKIPPED or NOT_NEEDED, require a rationale on the same line
|
|
107
|
+
if grep -qi '| *e2e.*| *skipped' "$SUMMARY" 2>/dev/null; then
|
|
108
|
+
SKIPPED_LINE=$(grep -i '| *e2e.*| *skipped' "$SUMMARY" | head -1)
|
|
109
|
+
# Rationale can be in the details column (after the result column) or inline with the state
|
|
110
|
+
if echo "$SKIPPED_LINE" | grep -qi 'skipped.*[—–-].*\|| *skipped.*|.*[a-z]'; then
|
|
111
|
+
echo " OK: E2E gate result is SKIPPED with rationale"
|
|
112
|
+
else
|
|
113
|
+
echo " ERROR: E2E gate result is SKIPPED but no rationale provided. Add a reason in the Details column (e.g. 'SKIPPED — API-only change, no UI surface (operator-approved)') (devaudit-installer#240)"
|
|
114
|
+
EXIT_CODE=1
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if grep -qi '| *e2e.*| *not_needed' "$SUMMARY" 2>/dev/null; then
|
|
119
|
+
NN_LINE=$(grep -i '| *e2e.*| *not_needed' "$SUMMARY" | head -1)
|
|
120
|
+
if echo "$NN_LINE" | grep -qi 'not_needed.*[—–-].*\|| *not_needed.*|.*[a-z]'; then
|
|
121
|
+
echo " OK: E2E gate result is NOT_NEEDED with rationale"
|
|
122
|
+
else
|
|
123
|
+
echo " ERROR: E2E gate result is NOT_NEEDED but no rationale provided. Add a reason in the Details column (e.g. 'NOT_NEEDED — schema-only change, no UI surface') (devaudit-installer#240)"
|
|
124
|
+
EXIT_CODE=1
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Check 6: Reject "E2E deferred to CI" in final assessment lines
|
|
129
|
+
if grep -qi 'e2e deferred\|e2e.*deferred to ci' "$SUMMARY" 2>/dev/null; then
|
|
130
|
+
echo " ERROR: 'E2E deferred' language found in final assessment — 'deferred' is not a valid gate state (devaudit-installer#240)"
|
|
131
|
+
echo " Offending lines:"
|
|
132
|
+
grep -in 'e2e deferred\|e2e.*deferred to ci' "$SUMMARY" | sed 's/^/ /'
|
|
133
|
+
EXIT_CODE=1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
echo ""
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
echo ""
|
|
140
|
+
if [ $EXIT_CODE -eq 0 ]; then
|
|
141
|
+
echo "=== All test execution summary checks passed ==="
|
|
142
|
+
else
|
|
143
|
+
echo "=== FAILED: Invalid gate states found in test execution summary ==="
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
exit $EXIT_CODE
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-test-summary.test.sh — Tests for validate-test-summary.sh.
|
|
3
|
+
#
|
|
4
|
+
# Builds a throwaway git repo per case, creates compliance evidence with
|
|
5
|
+
# crafted test-execution-summary.md files, runs the validator, and asserts
|
|
6
|
+
# on the exit code. Hermetic: runs inside mktemp'd directories that are
|
|
7
|
+
# torn down at the end.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ./scripts/validate-test-summary.test.sh
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
HELPER="$SCRIPT_DIR/validate-test-summary.sh"
|
|
16
|
+
[ -x "$HELPER" ] || chmod +x "$HELPER"
|
|
17
|
+
|
|
18
|
+
PASS=0
|
|
19
|
+
FAIL=0
|
|
20
|
+
|
|
21
|
+
WORK=$(mktemp -d)
|
|
22
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
23
|
+
|
|
24
|
+
# Build a fresh git fixture with a commit referencing REQ-XXX.
|
|
25
|
+
# Sets up compliance/evidence/REQ-XXX/ directory.
|
|
26
|
+
# $1 = dir, $2 = REQ ID, $3 = summary file content (via stdin)
|
|
27
|
+
make_fixture() {
|
|
28
|
+
local dir="$1" req="$2"
|
|
29
|
+
rm -rf "$dir"
|
|
30
|
+
mkdir -p "$dir"
|
|
31
|
+
cd "$dir"
|
|
32
|
+
git init -q --initial-branch=main
|
|
33
|
+
git config user.email "test@example.com"
|
|
34
|
+
git config user.name "test"
|
|
35
|
+
|
|
36
|
+
# Create a base branch and a feature branch with a REQ commit
|
|
37
|
+
echo "x" > f.txt
|
|
38
|
+
git add f.txt
|
|
39
|
+
git commit -q -m "initial"
|
|
40
|
+
|
|
41
|
+
git checkout -q -b feature
|
|
42
|
+
echo "y" > g.txt
|
|
43
|
+
git add g.txt
|
|
44
|
+
git commit -q -m "feat: [${req}] test change
|
|
45
|
+
|
|
46
|
+
Ref: ${req}"
|
|
47
|
+
|
|
48
|
+
# Create evidence directory and RTM row
|
|
49
|
+
mkdir -p "compliance/evidence/${req}"
|
|
50
|
+
echo "| ${req} | #999 | HIGH | compliance/evidence/${req}/ | TESTED - PENDING SIGN-OFF | TBD | -- |" > compliance/RTM.md
|
|
51
|
+
git add compliance/RTM.md
|
|
52
|
+
git commit -q -m "docs: add RTM row for ${req}"
|
|
53
|
+
|
|
54
|
+
# Write the test-execution-summary.md from stdin
|
|
55
|
+
cat > "compliance/evidence/${req}/test-execution-summary.md"
|
|
56
|
+
|
|
57
|
+
# Stay on feature branch so the validator sees the diff from main
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Run the validator against the fixture, capture exit code.
|
|
61
|
+
# $1 = fixture dir
|
|
62
|
+
run_validator() {
|
|
63
|
+
local dir="$1"
|
|
64
|
+
cd "$dir"
|
|
65
|
+
local exit_code=0
|
|
66
|
+
bash "$HELPER" main > /tmp/validate-test-summary-output.txt 2>&1 || exit_code=$?
|
|
67
|
+
echo "$exit_code"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
assert_exit() {
|
|
71
|
+
local desc="$1" want="$2" got="$3"
|
|
72
|
+
if [ "$got" = "$want" ]; then
|
|
73
|
+
echo " PASS: $desc"
|
|
74
|
+
PASS=$((PASS + 1))
|
|
75
|
+
else
|
|
76
|
+
echo " FAIL: $desc"
|
|
77
|
+
echo " want exit: $want"
|
|
78
|
+
echo " got exit: $got"
|
|
79
|
+
cat /tmp/validate-test-summary-output.txt | sed 's/^/ /'
|
|
80
|
+
FAIL=$((FAIL + 1))
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
echo "=== validate-test-summary.sh tests ==="
|
|
85
|
+
|
|
86
|
+
# --- Case 1: Clean summary with PASS — should pass ---
|
|
87
|
+
D1="$WORK/case1"
|
|
88
|
+
make_fixture "$D1" "REQ-001" << 'EOF'
|
|
89
|
+
# Test Execution Summary — REQ-001
|
|
90
|
+
|
|
91
|
+
## Test design
|
|
92
|
+
|
|
93
|
+
**Layers planned:** unit, e2e
|
|
94
|
+
**Layers covered:** unit ✓, e2e ✓
|
|
95
|
+
|
|
96
|
+
## Gate Results
|
|
97
|
+
|
|
98
|
+
| Gate | Result | Details |
|
|
99
|
+
| ---------------- | -------- | -------------------- |
|
|
100
|
+
| TypeScript | PASS | 0 errors |
|
|
101
|
+
| E2E Tests | PASS | All specs green |
|
|
102
|
+
| Unit Tests | PASS | 42 passed |
|
|
103
|
+
|
|
104
|
+
**Final assessment:** All gates passed.
|
|
105
|
+
EOF
|
|
106
|
+
EXIT1=$(run_validator "$D1")
|
|
107
|
+
assert_exit "clean summary with PASS — should pass" 0 "$EXIT1"
|
|
108
|
+
|
|
109
|
+
# --- Case 2: Summary with 'deferred' — should fail ---
|
|
110
|
+
D2="$WORK/case2"
|
|
111
|
+
make_fixture "$D2" "REQ-002" << 'EOF'
|
|
112
|
+
# Test Execution Summary — REQ-002
|
|
113
|
+
|
|
114
|
+
## Test design
|
|
115
|
+
|
|
116
|
+
**Layers covered:** unit ✓, e2e deferred
|
|
117
|
+
|
|
118
|
+
## Gate Results
|
|
119
|
+
|
|
120
|
+
| Gate | Result | Details |
|
|
121
|
+
| ---------------- | -------- | ---------------------------------------- |
|
|
122
|
+
| E2E Tests | DEFERRED | Playwright browsers not installed locally |
|
|
123
|
+
|
|
124
|
+
**Final assessment:** All local gates passed. E2E deferred to CI.
|
|
125
|
+
EOF
|
|
126
|
+
EXIT2=$(run_validator "$D2")
|
|
127
|
+
assert_exit "summary with 'deferred' — should fail" 1 "$EXIT2"
|
|
128
|
+
|
|
129
|
+
# --- Case 3: Summary with 'Deferred to CI' — should fail ---
|
|
130
|
+
D3="$WORK/case3"
|
|
131
|
+
make_fixture "$D3" "REQ-003" << 'EOF'
|
|
132
|
+
# Test Execution Summary — REQ-003
|
|
133
|
+
|
|
134
|
+
**Final assessment:** All gates passed. E2E Deferred to CI.
|
|
135
|
+
EOF
|
|
136
|
+
EXIT3=$(run_validator "$D3")
|
|
137
|
+
assert_exit "summary with 'Deferred to CI' — should fail" 1 "$EXIT3"
|
|
138
|
+
|
|
139
|
+
# --- Case 4: Summary with 'browsers not installed' — should fail ---
|
|
140
|
+
D4="$WORK/case4"
|
|
141
|
+
make_fixture "$D4" "REQ-004" << 'EOF'
|
|
142
|
+
# Test Execution Summary — REQ-004
|
|
143
|
+
|
|
144
|
+
## Gate Results
|
|
145
|
+
|
|
146
|
+
| Gate | Result | Details |
|
|
147
|
+
| ---------------- | -------- | ---------------------------------------- |
|
|
148
|
+
| E2E Tests | SKIPPED | Playwright browsers not installed locally |
|
|
149
|
+
|
|
150
|
+
**Final assessment:** E2E skipped — browsers not installed.
|
|
151
|
+
EOF
|
|
152
|
+
EXIT4=$(run_validator "$D4")
|
|
153
|
+
assert_exit "summary with 'browsers not installed' — should fail" 1 "$EXIT4"
|
|
154
|
+
|
|
155
|
+
# --- Case 5: Summary with NOT_NEEDED and rationale — should pass ---
|
|
156
|
+
D5="$WORK/case5"
|
|
157
|
+
make_fixture "$D5" "REQ-005" << 'EOF'
|
|
158
|
+
# Test Execution Summary — REQ-005
|
|
159
|
+
|
|
160
|
+
## Gate Results
|
|
161
|
+
|
|
162
|
+
| Gate | Result | Details |
|
|
163
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
164
|
+
| E2E Tests | NOT_NEEDED | Schema-only change, no UI surface |
|
|
165
|
+
|
|
166
|
+
**Final assessment:** All gates passed. E2E not needed for this REQ.
|
|
167
|
+
EOF
|
|
168
|
+
EXIT5=$(run_validator "$D5")
|
|
169
|
+
assert_exit "summary with NOT_NEEDED and rationale — should pass" 0 "$EXIT5"
|
|
170
|
+
|
|
171
|
+
# --- Case 6: Summary with SKIPPED and rationale — should pass ---
|
|
172
|
+
D6="$WORK/case6"
|
|
173
|
+
make_fixture "$D6" "REQ-006" << 'EOF'
|
|
174
|
+
# Test Execution Summary — REQ-006
|
|
175
|
+
|
|
176
|
+
## Gate Results
|
|
177
|
+
|
|
178
|
+
| Gate | Result | Details |
|
|
179
|
+
| ---------------- | ------------ | ------------------------------------------ |
|
|
180
|
+
| E2E Tests | SKIPPED | API-only change, no UI surface (operator-approved) |
|
|
181
|
+
|
|
182
|
+
**Final assessment:** All gates passed. E2E skipped — API-only change.
|
|
183
|
+
EOF
|
|
184
|
+
EXIT6=$(run_validator "$D6")
|
|
185
|
+
assert_exit "summary with SKIPPED and rationale — should pass" 0 "$EXIT6"
|
|
186
|
+
|
|
187
|
+
# --- Case 7: Summary with SKIPPED but no rationale — should fail ---
|
|
188
|
+
D7="$WORK/case7"
|
|
189
|
+
make_fixture "$D7" "REQ-007" << 'EOF'
|
|
190
|
+
# Test Execution Summary — REQ-007
|
|
191
|
+
|
|
192
|
+
## Gate Results
|
|
193
|
+
|
|
194
|
+
| Gate | Result | Details |
|
|
195
|
+
| ---------------- | ------------ | ---------- |
|
|
196
|
+
| E2E Tests | SKIPPED | |
|
|
197
|
+
|
|
198
|
+
**Final assessment:** All gates passed.
|
|
199
|
+
EOF
|
|
200
|
+
EXIT7=$(run_validator "$D7")
|
|
201
|
+
assert_exit "summary with SKIPPED but no rationale — should fail" 1 "$EXIT7"
|
|
202
|
+
|
|
203
|
+
# --- Case 8: No REQ references — should pass (skip) ---
|
|
204
|
+
D8="$WORK/case8"
|
|
205
|
+
rm -rf "$D8"
|
|
206
|
+
mkdir -p "$D8"
|
|
207
|
+
cd "$D8"
|
|
208
|
+
git init -q --initial-branch=main
|
|
209
|
+
git config user.email "test@example.com"
|
|
210
|
+
git config user.name "test"
|
|
211
|
+
echo "x" > f.txt
|
|
212
|
+
git add f.txt
|
|
213
|
+
git commit -q -m "initial"
|
|
214
|
+
git checkout -q -b feature
|
|
215
|
+
echo "y" > g.txt
|
|
216
|
+
git add g.txt
|
|
217
|
+
git commit -q -m "docs: update README"
|
|
218
|
+
EXIT8=$(run_validator "$D8")
|
|
219
|
+
assert_exit "no REQ references — should pass (skip)" 0 "$EXIT8"
|
|
220
|
+
|
|
221
|
+
# --- Case 9: No test-execution-summary.md — should pass (skip) ---
|
|
222
|
+
D9="$WORK/case9"
|
|
223
|
+
make_fixture "$D9" "REQ-009" << 'EOF'
|
|
224
|
+
# placeholder
|
|
225
|
+
EOF
|
|
226
|
+
rm "$D9/compliance/evidence/REQ-009/test-execution-summary.md"
|
|
227
|
+
EXIT9=$(run_validator "$D9")
|
|
228
|
+
assert_exit "no test-execution-summary.md — should pass (skip)" 0 "$EXIT9"
|
|
229
|
+
|
|
230
|
+
# --- Case 10: Summary with 'e2e deferred' in final assessment — should fail ---
|
|
231
|
+
D10="$WORK/case10"
|
|
232
|
+
make_fixture "$D10" "REQ-010" << 'EOF'
|
|
233
|
+
# Test Execution Summary — REQ-010
|
|
234
|
+
|
|
235
|
+
## Gate Results
|
|
236
|
+
|
|
237
|
+
| Gate | Result | Details |
|
|
238
|
+
| ---------------- | -------- | --------------- |
|
|
239
|
+
| E2E Tests | PASS | All specs green |
|
|
240
|
+
|
|
241
|
+
**Final assessment:** All gates passed. E2E deferred to CI for critical tier.
|
|
242
|
+
EOF
|
|
243
|
+
EXIT10=$(run_validator "$D10")
|
|
244
|
+
assert_exit "summary with 'e2e deferred' in final assessment — should fail" 1 "$EXIT10"
|
|
245
|
+
|
|
246
|
+
# --- Case 11: NOT_NEEDED without rationale — should fail ---
|
|
247
|
+
D11="$WORK/case11"
|
|
248
|
+
make_fixture "$D11" "REQ-011" << 'EOF'
|
|
249
|
+
# Test Execution Summary — REQ-011
|
|
250
|
+
|
|
251
|
+
## Gate Results
|
|
252
|
+
|
|
253
|
+
| Gate | Result | Details |
|
|
254
|
+
| ---------------- | ------------ | ---------- |
|
|
255
|
+
| E2E Tests | NOT_NEEDED | |
|
|
256
|
+
|
|
257
|
+
**Final assessment:** All gates passed.
|
|
258
|
+
EOF
|
|
259
|
+
EXIT11=$(run_validator "$D11")
|
|
260
|
+
assert_exit "summary with NOT_NEEDED but no rationale — should fail" 1 "$EXIT11"
|
|
261
|
+
|
|
262
|
+
echo ""
|
|
263
|
+
echo "=== Summary: $PASS pass / $FAIL fail ==="
|
|
264
|
+
|
|
265
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
exit 0
|
|
@@ -241,8 +241,34 @@ Halt and report the gap to the user:
|
|
|
241
241
|
|
|
242
242
|
Do **not** proceed to Phase 6 until all gaps are resolved. The user may choose to skip an AC (e.g. API-only, transport-only) — that's valid, but it must be an explicit decision recorded in the test-execution-summary, not an omission.
|
|
243
243
|
|
|
244
|
+
**Write the evidence-wiring sentinel (devaudit-installer#226).** After all Phase 5½ checks pass (all `@requirement`, `evidenceShot()`, and `tagTest()` calls verified), write a `.e2e-evidence-wired` sentinel file in the repo root so the pre-push hook and `sdlc-implementer` Phase 2 step 5b can verify evidence wiring was validated:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
echo "WIRED $(date -u +%Y-%m-%dT%H:%M:%SZ) REQ-XXX" > .e2e-evidence-wired
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If the user explicitly skipped an AC (e.g. API-only), note it in the sentinel:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
echo "WIRED $(date -u +%Y-%m-%dT%H:%M:%SZ) REQ-XXX (AC3 skipped — API-only)" > .e2e-evidence-wired
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The file is gitignored and never committed — it's a local-only signal that evidence wiring was validated in this working directory.
|
|
257
|
+
|
|
244
258
|
### Phase 6 — Execute and report
|
|
245
259
|
|
|
260
|
+
**Pre-flight: browser availability (devaudit-installer#238).** Before running the suite, verify Playwright browsers are installed:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npx playwright install --dry-run 2>&1 | grep -q "is already installed" || npx playwright install
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
If the install fails (e.g. missing system dependencies on Linux), run `npx playwright install --with-deps` (requires sudo on some systems — ask the operator). Do not defer E2E execution to CI because browsers are not installed. Installing browsers takes ~30 seconds; deferring breaks the evidence trail.
|
|
267
|
+
|
|
268
|
+
**Do not defer E2E to CI (devaudit-installer#238).** If browsers are not installed, install them. If the dev server will not start, debug it. If the database is not running, start it. CI is a safety net, not a replacement for local E2E execution. The `.e2e-gate-passed` sentinel must be written by a local run — skipping it by deferring to CI breaks the evidence-completeness chain and causes the evidence-completeness gate (#237) to fire with false negatives.
|
|
269
|
+
|
|
270
|
+
**Gate state vocabulary (devaudit-installer#240).** In `test-execution-summary.md`, E2E gate results must be one of: `PASS`, `FAIL`, `NOT_NEEDED` (with reason), or `SKIPPED` (with operator-approved rationale). The word "deferred" must never appear in `test-execution-summary.md` — not as a gate state, not in prose, not in final assessment. The CI validator (`validate-test-summary.sh`) rejects any file containing "deferred" anywhere. If E2E was not run locally, record it as `SKIPPED` with the reason and flag it as a gate failure for the reviewer. Do not write "E2E deferred to CI" or "Playwright browsers not installed locally" — these are environment issues, not gate states.
|
|
271
|
+
|
|
246
272
|
Run the suite. Strategy:
|
|
247
273
|
|
|
248
274
|
1. **Iterate focused.** During fix-and-verify, run only the failing specs (`--grep`, spec-path args, or a CI input that scopes to a subset). Cycle time is what makes the loop tractable — full regression for every iteration burns CI budget and operator patience. Expect to loop: fix → focused run → fix → focused run, many times.
|
|
@@ -404,8 +430,8 @@ Wrap up with a summary the user can drop into the PR or ticket:
|
|
|
404
430
|
**Then feed the test-design record (devaudit#50).** The Stage 3 `test-execution-summary.md` (generated per `3-compile-evidence.md` Step 1a) carries a `## Test design` section at the top. Before Stage 3 finalises the file, populate that section with the design-time decisions this skill made, so the SDLC has a recorded trace that scope was *decided*, not implicit:
|
|
405
431
|
|
|
406
432
|
- **Layers planned** — which of `unit | integration | e2e | visual | manual` applied to this REQ
|
|
407
|
-
- **Layers covered** — same list with ✓ or `
|
|
408
|
-
- **
|
|
433
|
+
- **Layers covered** — same list with ✓ or `NOT_NEEDED` (with reason)
|
|
434
|
+
- **Exemptions** — explicit one-line rationale per exempt layer (`e2e NOT_NEEDED — schema-only, no UI yet` rather than silent absence). Do not use "deferred" as a gate state — it is not a valid SDLC state (devaudit-installer#240). The CI validator (`validate-test-summary.sh`) will reject any summary containing "deferred".
|
|
409
435
|
- **Skill invocation** — _"`e2e-test-engineer` invoked on turn N during Phase 2"_, with a turn pointer the reviewer can verify against the chat transcript
|
|
410
436
|
|
|
411
437
|
If you authored or modified `e2e/**/*.spec.ts` directly without invoking this skill, that's a delegation gap — the `sdlc-implementer` Phase 2 audit (devaudit#132) will catch it before Phase 3. The honest record is: the skill ran (or didn't), the layers were chosen for stated reasons, and the test-execution-summary attribution points back at the chat turn where the decision happened.
|