@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.2.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": "file:../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=$(grep -E '\|[[:space:]]+IN PROGRESS' "$RTM_PATH" 2>/dev/null \
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 `deferred`
408
- - **Deferrals** — explicit one-line rationale per deferred layer (`e2e N/A — schema-only, no UI yet` rather than silent absence)
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.