@uzysjung/agent-harness 26.158.0 → 26.159.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/README.ko.md +1 -1
- package/README.md +1 -1
- package/dist/{chunk-HHCNPPGS.js → chunk-FTL34NT4.js} +49 -289
- package/dist/chunk-FTL34NT4.js.map +1 -0
- package/dist/index.js +11 -40
- package/dist/index.js.map +1 -1
- package/dist/trust-tier-drift.js +1 -1
- package/package.json +1 -2
- package/templates/agents/strategist.md +10 -0
- package/dist/chunk-HHCNPPGS.js.map +0 -1
- package/scripts/prune-ecc.sh +0 -310
- package/templates/skills/e2e-testing/SKILL.md +0 -326
- package/templates/skills/e2e-testing/agents/openai.yaml +0 -7
- package/templates/skills/investor-materials/SKILL.md +0 -96
- package/templates/skills/investor-outreach/SKILL.md +0 -91
- package/templates/skills/market-research/SKILL.md +0 -75
- package/templates/skills/market-research/agents/openai.yaml +0 -7
- package/templates/skills/nextjs-turbopack/SKILL.md +0 -44
- package/templates/skills/python-patterns/SKILL.md +0 -750
- package/templates/skills/python-testing/SKILL.md +0 -816
package/scripts/prune-ecc.sh
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# ============================================================
|
|
3
|
-
# prune-ecc.sh
|
|
4
|
-
# ECC plugin을 project local로 복사하고 KEEP 외 항목을 prune.
|
|
5
|
-
# 글로벌 ~/.claude/는 read-only로만 사용 (D16 안전).
|
|
6
|
-
#
|
|
7
|
-
# 사용:
|
|
8
|
-
# 1. (선행 1회) claude plugin install everything-claude-code@everything-claude-code
|
|
9
|
-
# 2. bash prune-ecc.sh # dry-run (기본). 변경 없음.
|
|
10
|
-
# 3. bash prune-ecc.sh --apply # 실제 복사 + prune (확인 prompt)
|
|
11
|
-
# 4. bash prune-ecc.sh --apply --force # 확인 없이 즉시 진행
|
|
12
|
-
# 5. claude --plugin-dir .claude/local-plugins/ecc # 사용
|
|
13
|
-
#
|
|
14
|
-
# 옵션:
|
|
15
|
-
# --apply 실제 복사 + prune. 기본은 dry-run.
|
|
16
|
-
# --force 확인 prompt 생략.
|
|
17
|
-
# --dest <path> 복사 대상 경로 (기본: .claude/local-plugins/ecc)
|
|
18
|
-
# --keep-existing dest가 이미 있으면 덮어쓰기 안 함 (기본은 재복사)
|
|
19
|
-
# --copy-only prune 단계 skip. 전체 ECC를 project local로 복사만.
|
|
20
|
-
#
|
|
21
|
-
# 안전성:
|
|
22
|
-
# - 글로벌 ~/.claude/plugins/cache/ : read-only (cp 소스로만 사용)
|
|
23
|
-
# - dest 위치 (.claude/local-plugins/ecc) : .gitignore 자동 추가
|
|
24
|
-
# - 다른 프로젝트 영향 없음 (이 프로젝트 local만)
|
|
25
|
-
# ============================================================
|
|
26
|
-
set -u
|
|
27
|
-
|
|
28
|
-
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
|
29
|
-
|
|
30
|
-
# --- Args ---
|
|
31
|
-
APPLY=false
|
|
32
|
-
FORCE=false
|
|
33
|
-
KEEP_EXISTING=false
|
|
34
|
-
COPY_ONLY=false
|
|
35
|
-
DEST=".claude/local-plugins/ecc"
|
|
36
|
-
while [ $# -gt 0 ]; do
|
|
37
|
-
case "$1" in
|
|
38
|
-
--apply) APPLY=true; shift ;;
|
|
39
|
-
--force) FORCE=true; shift ;;
|
|
40
|
-
--keep-existing) KEEP_EXISTING=true; shift ;;
|
|
41
|
-
--copy-only) COPY_ONLY=true; shift ;;
|
|
42
|
-
--dest)
|
|
43
|
-
[ -z "${2:-}" ] || [[ "$2" == --* ]] && { echo "ERROR: --dest requires a value" >&2; exit 1; }
|
|
44
|
-
DEST="$2"; shift 2 ;;
|
|
45
|
-
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
|
|
46
|
-
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
|
47
|
-
esac
|
|
48
|
-
done
|
|
49
|
-
|
|
50
|
-
# v26.11.1 — DEST 경계 검증: 글로벌 ~/.claude/ 또는 시스템 경로 차단 (path traversal 방어)
|
|
51
|
-
# v0.6.6 — dirname 디렉토리 미존재 시 pwd 기준으로 fallback (이전엔 상대경로 그대로 잔존 → pwd 검증 실패)
|
|
52
|
-
DEST_PARENT="$(cd "$(dirname "$DEST")" 2>/dev/null && pwd)" || DEST_PARENT=""
|
|
53
|
-
if [ -n "$DEST_PARENT" ]; then
|
|
54
|
-
DEST_ABS="$DEST_PARENT/$(basename "$DEST")"
|
|
55
|
-
elif [[ "$DEST" = /* ]]; then
|
|
56
|
-
DEST_ABS="$DEST"
|
|
57
|
-
else
|
|
58
|
-
DEST_ABS="$(pwd)/$DEST"
|
|
59
|
-
fi
|
|
60
|
-
case "$DEST_ABS" in
|
|
61
|
-
"$HOME/.claude"|"$HOME/.claude/"*)
|
|
62
|
-
echo "ERROR: --dest는 글로벌 ~/.claude/ 영역 불가 (D16)" >&2
|
|
63
|
-
echo " 입력: $DEST (절대: $DEST_ABS)" >&2
|
|
64
|
-
exit 1 ;;
|
|
65
|
-
"/"|"/etc"|"/etc/"*|"/usr/bin"|"/usr/sbin"|"/usr/local/bin"|"/bin"|"/sbin"|"/System"|"/System/"*)
|
|
66
|
-
echo "ERROR: --dest는 시스템 디렉토리 불가" >&2
|
|
67
|
-
echo " 입력: $DEST (절대: $DEST_ABS)" >&2
|
|
68
|
-
exit 1 ;;
|
|
69
|
-
esac
|
|
70
|
-
# 추가 안전: 현재 pwd 하위만 허용 (project-local 보장)
|
|
71
|
-
case "$DEST_ABS" in
|
|
72
|
-
"$(pwd)"|"$(pwd)/"*) ;;
|
|
73
|
-
*)
|
|
74
|
-
echo "ERROR: --dest는 현재 프로젝트 디렉토리 하위만 허용" >&2
|
|
75
|
-
echo " 입력: $DEST (절대: $DEST_ABS)" >&2
|
|
76
|
-
echo " 현재 pwd: $(pwd)" >&2
|
|
77
|
-
exit 1 ;;
|
|
78
|
-
esac
|
|
79
|
-
|
|
80
|
-
info() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
81
|
-
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
|
82
|
-
fail() { echo -e " ${RED}✗${NC} $1" >&2; }
|
|
83
|
-
section() { echo ""; echo -e "${CYAN}${BOLD}$1${NC}"; }
|
|
84
|
-
|
|
85
|
-
# --- KEEP list (총 98건, v26.58.0 +6) ---
|
|
86
|
-
# v26.58.0 (ADR-019): cherrypicks.lock 의 C2 6개 KEEP 추가 (plugin ON 시 prune 으로 사라지는 구멍 차단):
|
|
87
|
-
# security-reviewer, silent-failure-hunter, build-error-resolver,
|
|
88
|
-
# e2e-testing, agent-introspection-debugging, nextjs-turbopack.
|
|
89
|
-
# 직전 baseline 92건 (이전 코멘트 "89건" 은 오기 — 실제 단어 count 검증 시 92).
|
|
90
|
-
KEEP_ITEMS="
|
|
91
|
-
agent-harness-construction agent-introspection-debugging agentic-engineering
|
|
92
|
-
ai-first-engineering api-design aside autonomous-loops backend-patterns
|
|
93
|
-
blueprint build-error-resolver build-fix checkpoint claude-api claw
|
|
94
|
-
code-review code-reviewer coding-standards configure-ecc content-engine
|
|
95
|
-
content-hash-cache-pattern continuous-agent-loop continuous-learning continuous-learning-v2
|
|
96
|
-
cost-aware-llm-pipeline crosspost database-migrations database-reviewer deep-research
|
|
97
|
-
deployment-patterns dmux-workflows docker-patterns e2e e2e-runner e2e-testing
|
|
98
|
-
eval eval-harness evolve exa-search frontend-patterns frontend-slides
|
|
99
|
-
harness-audit harness-optimizer instinct-export instinct-import instinct-status
|
|
100
|
-
investor-materials investor-outreach iterative-retrieval learn learn-eval
|
|
101
|
-
loop-operator loop-start loop-status market-research model-route multi-backend
|
|
102
|
-
multi-execute multi-frontend multi-plan multi-workflow nextjs-turbopack orchestrate
|
|
103
|
-
plan plankton-code-quality planner postgres-patterns projects promote
|
|
104
|
-
prompt-optimize prompt-optimizer python-patterns python-review python-reviewer
|
|
105
|
-
python-testing quality-gate refactor-clean regex-vs-llm-structured-text
|
|
106
|
-
resume-session save-session search-first security-review security-reviewer
|
|
107
|
-
security-scan sessions setup-pm silent-failure-hunter skill-create skill-stocktake
|
|
108
|
-
strategic-compact tdd tdd-guide tdd-workflow test-coverage update-codemaps
|
|
109
|
-
update-docs verification-loop verify x-api
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
is_keep() {
|
|
113
|
-
# v26.11.1 — grep -F (fixed string)로 정규식 메타문자 안전. 공백으로 단어 경계 강제.
|
|
114
|
-
echo " $KEEP_ITEMS " | tr -s ' \n' ' ' | grep -qF " $1 "
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
# --- Step 1: 글로벌 ECC cache 발견 ---
|
|
118
|
-
section "[1/5] Discover global ECC plugin cache (read-only)"
|
|
119
|
-
ECC_CACHE_BASE="$HOME/.claude/plugins/cache/everything-claude-code/everything-claude-code"
|
|
120
|
-
if [ ! -d "$ECC_CACHE_BASE" ]; then
|
|
121
|
-
fail "ECC plugin 미설치 — 다음 명령으로 먼저 설치:"
|
|
122
|
-
echo " claude plugin marketplace add affaan-m/everything-claude-code"
|
|
123
|
-
echo " claude plugin install everything-claude-code@everything-claude-code"
|
|
124
|
-
exit 1
|
|
125
|
-
fi
|
|
126
|
-
ECC_VERSION=$(ls -1 "$ECC_CACHE_BASE" 2>/dev/null | sort -V | tail -1)
|
|
127
|
-
# ECC v1.10.0 구조: 1.10.0/ 자체가 root (skills/, agents/, commands/ top-level).
|
|
128
|
-
# everything-claude-code/ 는 빈 디렉토리, ecc2/는 별개 Rust 프로젝트.
|
|
129
|
-
ECC_SOURCE="$ECC_CACHE_BASE/$ECC_VERSION"
|
|
130
|
-
if [ ! -d "$ECC_SOURCE/skills" ] || [ ! -d "$ECC_SOURCE/agents" ]; then
|
|
131
|
-
fail "ECC plugin source 디렉토리 미발견 (skills/ + agents/): $ECC_SOURCE"
|
|
132
|
-
exit 1
|
|
133
|
-
fi
|
|
134
|
-
info "Source: $ECC_SOURCE (v$ECC_VERSION)"
|
|
135
|
-
|
|
136
|
-
# --- Step 2: 복사 (또는 기존 사용) ---
|
|
137
|
-
section "[2/5] Copy to project local"
|
|
138
|
-
if [ -d "$DEST" ] && [ "$KEEP_EXISTING" = true ]; then
|
|
139
|
-
info "기존 $DEST 유지 (--keep-existing)"
|
|
140
|
-
elif [ "$APPLY" = false ]; then
|
|
141
|
-
warn "[DRY-RUN] would copy: $ECC_SOURCE → $DEST"
|
|
142
|
-
else
|
|
143
|
-
if [ -d "$DEST" ]; then
|
|
144
|
-
warn "기존 $DEST 제거 후 재복사"
|
|
145
|
-
rm -rf "$DEST"
|
|
146
|
-
fi
|
|
147
|
-
mkdir -p "$(dirname "$DEST")"
|
|
148
|
-
cp -R "$ECC_SOURCE" "$DEST"
|
|
149
|
-
info "Copied to: $DEST"
|
|
150
|
-
fi
|
|
151
|
-
|
|
152
|
-
# --- Step 3: prune 분석 ---
|
|
153
|
-
section "[3/5] Prune analysis"
|
|
154
|
-
SCAN_DIR="$DEST"
|
|
155
|
-
[ ! -d "$SCAN_DIR" ] && SCAN_DIR="$ECC_SOURCE" # dry-run 시 source로 분석
|
|
156
|
-
|
|
157
|
-
declare -a PRUNE_TARGETS=()
|
|
158
|
-
KEEP_COUNT=0
|
|
159
|
-
PRUNE_COUNT=0
|
|
160
|
-
|
|
161
|
-
analyze_dir() {
|
|
162
|
-
local kind="$1" dir="$2"
|
|
163
|
-
[ ! -d "$dir" ] && return
|
|
164
|
-
local kept=0 pruned=0
|
|
165
|
-
if [ "$kind" = "directory" ]; then
|
|
166
|
-
for d in "$dir"/*/; do
|
|
167
|
-
[ -d "$d" ] || continue
|
|
168
|
-
local name; name=$(basename "$d")
|
|
169
|
-
if is_keep "$name"; then
|
|
170
|
-
kept=$((kept+1))
|
|
171
|
-
else
|
|
172
|
-
PRUNE_TARGETS+=("$d")
|
|
173
|
-
pruned=$((pruned+1))
|
|
174
|
-
fi
|
|
175
|
-
done
|
|
176
|
-
else
|
|
177
|
-
for f in "$dir"/*.md; do
|
|
178
|
-
[ -f "$f" ] || continue
|
|
179
|
-
local name; name=$(basename "$f" .md)
|
|
180
|
-
if is_keep "$name"; then
|
|
181
|
-
kept=$((kept+1))
|
|
182
|
-
else
|
|
183
|
-
PRUNE_TARGETS+=("$f")
|
|
184
|
-
pruned=$((pruned+1))
|
|
185
|
-
fi
|
|
186
|
-
done
|
|
187
|
-
fi
|
|
188
|
-
KEEP_COUNT=$((KEEP_COUNT+kept))
|
|
189
|
-
PRUNE_COUNT=$((PRUNE_COUNT+pruned))
|
|
190
|
-
printf " %-50s keep=%3d prune=%3d\n" "${dir#$SCAN_DIR/}" "$kept" "$pruned"
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
analyze_dir "directory" "$SCAN_DIR/skills"
|
|
194
|
-
analyze_dir "file" "$SCAN_DIR/agents"
|
|
195
|
-
analyze_dir "file" "$SCAN_DIR/commands"
|
|
196
|
-
[ -d "$SCAN_DIR/.opencode/commands" ] && analyze_dir "file" "$SCAN_DIR/.opencode/commands"
|
|
197
|
-
[ -d "$SCAN_DIR/.claude/commands" ] && analyze_dir "file" "$SCAN_DIR/.claude/commands"
|
|
198
|
-
|
|
199
|
-
echo ""
|
|
200
|
-
echo -e " ${BOLD}Total keep: ${GREEN}$KEEP_COUNT${NC}${BOLD} prune: ${RED}$PRUNE_COUNT${NC}"
|
|
201
|
-
|
|
202
|
-
# KEEP 누락 검증
|
|
203
|
-
section "KEEP missing check (사용자 리스트 중 plugin에 없는 것)"
|
|
204
|
-
NOT_FOUND=()
|
|
205
|
-
for k in $KEEP_ITEMS; do
|
|
206
|
-
if ! find "$SCAN_DIR/skills" "$SCAN_DIR/agents" "$SCAN_DIR/commands" \
|
|
207
|
-
"$SCAN_DIR/.opencode/commands" "$SCAN_DIR/.claude/commands" \
|
|
208
|
-
-maxdepth 1 \( -type d -name "$k" -o -type f -name "$k.md" \) 2>/dev/null \
|
|
209
|
-
| grep -q .; then
|
|
210
|
-
NOT_FOUND+=("$k")
|
|
211
|
-
fi
|
|
212
|
-
done
|
|
213
|
-
if [ "${#NOT_FOUND[@]}" -eq 0 ]; then
|
|
214
|
-
# 카운트는 KEEP_ITEMS 에서 동적 산출 (하드코딩 시 89→92→98 처럼 drift 누적).
|
|
215
|
-
info "모든 KEEP $(echo "$KEEP_ITEMS" | wc -w | tr -d ' ')건 plugin에 존재"
|
|
216
|
-
else
|
|
217
|
-
warn "KEEP 누락 ${#NOT_FOUND[@]}건 (이름 변경/제거됐을 수 있음):"
|
|
218
|
-
printf ' - %s\n' "${NOT_FOUND[@]}"
|
|
219
|
-
fi
|
|
220
|
-
|
|
221
|
-
# --- Step 4: apply ---
|
|
222
|
-
section "[4/5] Apply prune"
|
|
223
|
-
if [ "$APPLY" = false ]; then
|
|
224
|
-
warn "DRY-RUN — 변경 없음. 실제 복사+prune은 --apply 추가."
|
|
225
|
-
echo " bash prune-ecc.sh --apply"
|
|
226
|
-
exit 0
|
|
227
|
-
fi
|
|
228
|
-
|
|
229
|
-
# v26.15.0 — --copy-only: prune 단계 skip
|
|
230
|
-
if [ "$COPY_ONLY" = true ]; then
|
|
231
|
-
info "copy-only 모드 — prune 단계 skip (전체 ECC 유지)"
|
|
232
|
-
DELETED=0
|
|
233
|
-
FAILED=0
|
|
234
|
-
else
|
|
235
|
-
if [ "$FORCE" = false ]; then
|
|
236
|
-
echo ""
|
|
237
|
-
echo -e " ${YELLOW}경고:${NC} ${PRUNE_COUNT}건을 ${DEST} 하위에서 영구 삭제합니다."
|
|
238
|
-
echo " 글로벌 cache는 영향 없음. 다른 프로젝트도 영향 없음."
|
|
239
|
-
read -rp " 진행? [y/N]: " ANSWER < /dev/tty 2>/dev/null || ANSWER=""
|
|
240
|
-
[[ ! "$ANSWER" =~ ^[Yy]$ ]] && { warn "취소됨"; exit 0; }
|
|
241
|
-
fi
|
|
242
|
-
DELETED=0
|
|
243
|
-
FAILED=0
|
|
244
|
-
for target in "${PRUNE_TARGETS[@]}"; do
|
|
245
|
-
if rm -rf "$target" 2>/dev/null; then
|
|
246
|
-
DELETED=$((DELETED+1))
|
|
247
|
-
else
|
|
248
|
-
FAILED=$((FAILED+1))
|
|
249
|
-
fail "삭제 실패: $target"
|
|
250
|
-
fi
|
|
251
|
-
done
|
|
252
|
-
fi
|
|
253
|
-
|
|
254
|
-
# --- Step 5: .gitignore + 사용 안내 ---
|
|
255
|
-
section "[5/5] Post-actions"
|
|
256
|
-
if ! grep -q "^.claude/local-plugins/" .gitignore 2>/dev/null; then
|
|
257
|
-
echo ".claude/local-plugins/" >> .gitignore
|
|
258
|
-
info ".gitignore에 .claude/local-plugins/ 추가"
|
|
259
|
-
else
|
|
260
|
-
info ".gitignore 이미 등록됨"
|
|
261
|
-
fi
|
|
262
|
-
|
|
263
|
-
# v26.15.0 — DELETED / KEPT 상세 목록 출력
|
|
264
|
-
if [ "$COPY_ONLY" = false ] && [ "$DELETED" -gt 0 ]; then
|
|
265
|
-
echo ""
|
|
266
|
-
echo -e "${BOLD}========== DELETED ($DELETED) ==========${NC}"
|
|
267
|
-
for category in skills agents commands; do
|
|
268
|
-
local_deleted=()
|
|
269
|
-
for target in "${PRUNE_TARGETS[@]}"; do
|
|
270
|
-
case "$target" in
|
|
271
|
-
*"/$category/"*)
|
|
272
|
-
local_deleted+=("$(basename "$target")") ;;
|
|
273
|
-
esac
|
|
274
|
-
done
|
|
275
|
-
if [ "${#local_deleted[@]}" -gt 0 ]; then
|
|
276
|
-
echo -e " ${RED}$category/${NC} (${#local_deleted[@]}):"
|
|
277
|
-
printf ' - %s\n' "${local_deleted[@]}" | sort
|
|
278
|
-
fi
|
|
279
|
-
done
|
|
280
|
-
fi
|
|
281
|
-
|
|
282
|
-
echo ""
|
|
283
|
-
echo -e "${BOLD}========== KEPT ($KEEP_COUNT) ==========${NC}"
|
|
284
|
-
for category in skills agents commands; do
|
|
285
|
-
dir="$DEST/$category"
|
|
286
|
-
[ ! -d "$dir" ] && continue
|
|
287
|
-
kept_items=()
|
|
288
|
-
for item in "$dir"/*; do
|
|
289
|
-
[ -e "$item" ] || continue
|
|
290
|
-
kept_items+=("$(basename "$item" | sed 's/\.md$//')")
|
|
291
|
-
done
|
|
292
|
-
if [ "${#kept_items[@]}" -gt 0 ]; then
|
|
293
|
-
echo -e " ${GREEN}$category/${NC} (${#kept_items[@]}):"
|
|
294
|
-
printf ' - %s\n' "${kept_items[@]}" | sort
|
|
295
|
-
fi
|
|
296
|
-
done
|
|
297
|
-
|
|
298
|
-
echo ""
|
|
299
|
-
echo -e "${BOLD}========== Summary ==========${NC}"
|
|
300
|
-
echo -e " ${GREEN}Deleted: $DELETED${NC}"
|
|
301
|
-
[ "$FAILED" -gt 0 ] && echo -e " ${RED}Failed: $FAILED${NC}"
|
|
302
|
-
echo -e " ${BOLD}Remaining (KEEP): $KEEP_COUNT${NC}"
|
|
303
|
-
echo -e " ${BOLD}Location: $DEST${NC}"
|
|
304
|
-
echo ""
|
|
305
|
-
echo -e "${BOLD}사용법:${NC}"
|
|
306
|
-
echo " claude --plugin-dir $DEST"
|
|
307
|
-
echo ""
|
|
308
|
-
echo -e "${BOLD}재실행 필요 시점:${NC}"
|
|
309
|
-
echo " - ECC plugin 업데이트 후 (claude plugin update)"
|
|
310
|
-
echo " - 글로벌 cache 갱신 후"
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: e2e-testing
|
|
3
|
-
description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies.
|
|
4
|
-
origin: ECC
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# E2E Testing Patterns
|
|
8
|
-
|
|
9
|
-
Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites.
|
|
10
|
-
|
|
11
|
-
## Test File Organization
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
tests/
|
|
15
|
-
├── e2e/
|
|
16
|
-
│ ├── auth/
|
|
17
|
-
│ │ ├── login.spec.ts
|
|
18
|
-
│ │ ├── logout.spec.ts
|
|
19
|
-
│ │ └── register.spec.ts
|
|
20
|
-
│ ├── features/
|
|
21
|
-
│ │ ├── browse.spec.ts
|
|
22
|
-
│ │ ├── search.spec.ts
|
|
23
|
-
│ │ └── create.spec.ts
|
|
24
|
-
│ └── api/
|
|
25
|
-
│ └── endpoints.spec.ts
|
|
26
|
-
├── fixtures/
|
|
27
|
-
│ ├── auth.ts
|
|
28
|
-
│ └── data.ts
|
|
29
|
-
└── playwright.config.ts
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Page Object Model (POM)
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { Page, Locator } from '@playwright/test'
|
|
36
|
-
|
|
37
|
-
export class ItemsPage {
|
|
38
|
-
readonly page: Page
|
|
39
|
-
readonly searchInput: Locator
|
|
40
|
-
readonly itemCards: Locator
|
|
41
|
-
readonly createButton: Locator
|
|
42
|
-
|
|
43
|
-
constructor(page: Page) {
|
|
44
|
-
this.page = page
|
|
45
|
-
this.searchInput = page.locator('[data-testid="search-input"]')
|
|
46
|
-
this.itemCards = page.locator('[data-testid="item-card"]')
|
|
47
|
-
this.createButton = page.locator('[data-testid="create-btn"]')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async goto() {
|
|
51
|
-
await this.page.goto('/items')
|
|
52
|
-
await this.page.waitForLoadState('networkidle')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async search(query: string) {
|
|
56
|
-
await this.searchInput.fill(query)
|
|
57
|
-
await this.page.waitForResponse(resp => resp.url().includes('/api/search'))
|
|
58
|
-
await this.page.waitForLoadState('networkidle')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async getItemCount() {
|
|
62
|
-
return await this.itemCards.count()
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Test Structure
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { test, expect } from '@playwright/test'
|
|
71
|
-
import { ItemsPage } from '../../pages/ItemsPage'
|
|
72
|
-
|
|
73
|
-
test.describe('Item Search', () => {
|
|
74
|
-
let itemsPage: ItemsPage
|
|
75
|
-
|
|
76
|
-
test.beforeEach(async ({ page }) => {
|
|
77
|
-
itemsPage = new ItemsPage(page)
|
|
78
|
-
await itemsPage.goto()
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('should search by keyword', async ({ page }) => {
|
|
82
|
-
await itemsPage.search('test')
|
|
83
|
-
|
|
84
|
-
const count = await itemsPage.getItemCount()
|
|
85
|
-
expect(count).toBeGreaterThan(0)
|
|
86
|
-
|
|
87
|
-
await expect(itemsPage.itemCards.first()).toContainText(/test/i)
|
|
88
|
-
await page.screenshot({ path: 'artifacts/search-results.png' })
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('should handle no results', async ({ page }) => {
|
|
92
|
-
await itemsPage.search('xyznonexistent123')
|
|
93
|
-
|
|
94
|
-
await expect(page.locator('[data-testid="no-results"]')).toBeVisible()
|
|
95
|
-
expect(await itemsPage.getItemCount()).toBe(0)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Playwright Configuration
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { defineConfig, devices } from '@playwright/test'
|
|
104
|
-
|
|
105
|
-
export default defineConfig({
|
|
106
|
-
testDir: './tests/e2e',
|
|
107
|
-
fullyParallel: true,
|
|
108
|
-
forbidOnly: !!process.env.CI,
|
|
109
|
-
retries: process.env.CI ? 2 : 0,
|
|
110
|
-
workers: process.env.CI ? 1 : undefined,
|
|
111
|
-
reporter: [
|
|
112
|
-
['html', { outputFolder: 'playwright-report' }],
|
|
113
|
-
['junit', { outputFile: 'playwright-results.xml' }],
|
|
114
|
-
['json', { outputFile: 'playwright-results.json' }]
|
|
115
|
-
],
|
|
116
|
-
use: {
|
|
117
|
-
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
118
|
-
trace: 'on-first-retry',
|
|
119
|
-
screenshot: 'only-on-failure',
|
|
120
|
-
video: 'retain-on-failure',
|
|
121
|
-
actionTimeout: 10000,
|
|
122
|
-
navigationTimeout: 30000,
|
|
123
|
-
},
|
|
124
|
-
projects: [
|
|
125
|
-
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
126
|
-
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
127
|
-
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
128
|
-
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
|
|
129
|
-
],
|
|
130
|
-
webServer: {
|
|
131
|
-
command: 'npm run dev',
|
|
132
|
-
url: 'http://localhost:3000',
|
|
133
|
-
reuseExistingServer: !process.env.CI,
|
|
134
|
-
timeout: 120000,
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
## Flaky Test Patterns
|
|
140
|
-
|
|
141
|
-
### Quarantine
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
test('flaky: complex search', async ({ page }) => {
|
|
145
|
-
test.fixme(true, 'Flaky - Issue #123')
|
|
146
|
-
// test code...
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('conditional skip', async ({ page }) => {
|
|
150
|
-
test.skip(process.env.CI, 'Flaky in CI - Issue #123')
|
|
151
|
-
// test code...
|
|
152
|
-
})
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Identify Flakiness
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
npx playwright test tests/search.spec.ts --repeat-each=10
|
|
159
|
-
npx playwright test tests/search.spec.ts --retries=3
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Common Causes & Fixes
|
|
163
|
-
|
|
164
|
-
**Race conditions:**
|
|
165
|
-
```typescript
|
|
166
|
-
// Bad: assumes element is ready
|
|
167
|
-
await page.click('[data-testid="button"]')
|
|
168
|
-
|
|
169
|
-
// Good: auto-wait locator
|
|
170
|
-
await page.locator('[data-testid="button"]').click()
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**Network timing:**
|
|
174
|
-
```typescript
|
|
175
|
-
// Bad: arbitrary timeout
|
|
176
|
-
await page.waitForTimeout(5000)
|
|
177
|
-
|
|
178
|
-
// Good: wait for specific condition
|
|
179
|
-
await page.waitForResponse(resp => resp.url().includes('/api/data'))
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
**Animation timing:**
|
|
183
|
-
```typescript
|
|
184
|
-
// Bad: click during animation
|
|
185
|
-
await page.click('[data-testid="menu-item"]')
|
|
186
|
-
|
|
187
|
-
// Good: wait for stability
|
|
188
|
-
await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' })
|
|
189
|
-
await page.waitForLoadState('networkidle')
|
|
190
|
-
await page.locator('[data-testid="menu-item"]').click()
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
## Artifact Management
|
|
194
|
-
|
|
195
|
-
### Screenshots
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
await page.screenshot({ path: 'artifacts/after-login.png' })
|
|
199
|
-
await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true })
|
|
200
|
-
await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' })
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Traces
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
await browser.startTracing(page, {
|
|
207
|
-
path: 'artifacts/trace.json',
|
|
208
|
-
screenshots: true,
|
|
209
|
-
snapshots: true,
|
|
210
|
-
})
|
|
211
|
-
// ... test actions ...
|
|
212
|
-
await browser.stopTracing()
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Video
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
// In playwright.config.ts
|
|
219
|
-
use: {
|
|
220
|
-
video: 'retain-on-failure',
|
|
221
|
-
videosPath: 'artifacts/videos/'
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## CI/CD Integration
|
|
226
|
-
|
|
227
|
-
```yaml
|
|
228
|
-
# .github/workflows/e2e.yml
|
|
229
|
-
name: E2E Tests
|
|
230
|
-
on: [push, pull_request]
|
|
231
|
-
|
|
232
|
-
jobs:
|
|
233
|
-
test:
|
|
234
|
-
runs-on: ubuntu-latest
|
|
235
|
-
steps:
|
|
236
|
-
- uses: actions/checkout@v4
|
|
237
|
-
- uses: actions/setup-node@v4
|
|
238
|
-
with:
|
|
239
|
-
node-version: 20
|
|
240
|
-
- run: npm ci
|
|
241
|
-
- run: npx playwright install --with-deps
|
|
242
|
-
- run: npx playwright test
|
|
243
|
-
env:
|
|
244
|
-
BASE_URL: ${{ vars.STAGING_URL }}
|
|
245
|
-
- uses: actions/upload-artifact@v4
|
|
246
|
-
if: always()
|
|
247
|
-
with:
|
|
248
|
-
name: playwright-report
|
|
249
|
-
path: playwright-report/
|
|
250
|
-
retention-days: 30
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## Test Report Template
|
|
254
|
-
|
|
255
|
-
```markdown
|
|
256
|
-
# E2E Test Report
|
|
257
|
-
|
|
258
|
-
**Date:** YYYY-MM-DD HH:MM
|
|
259
|
-
**Duration:** Xm Ys
|
|
260
|
-
**Status:** PASSING / FAILING
|
|
261
|
-
|
|
262
|
-
## Summary
|
|
263
|
-
- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C
|
|
264
|
-
|
|
265
|
-
## Failed Tests
|
|
266
|
-
|
|
267
|
-
### test-name
|
|
268
|
-
**File:** `tests/e2e/feature.spec.ts:45`
|
|
269
|
-
**Error:** Expected element to be visible
|
|
270
|
-
**Screenshot:** artifacts/failed.png
|
|
271
|
-
**Recommended Fix:** [description]
|
|
272
|
-
|
|
273
|
-
## Artifacts
|
|
274
|
-
- HTML Report: playwright-report/index.html
|
|
275
|
-
- Screenshots: artifacts/*.png
|
|
276
|
-
- Videos: artifacts/videos/*.webm
|
|
277
|
-
- Traces: artifacts/*.zip
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Wallet / Web3 Testing
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
test('wallet connection', async ({ page, context }) => {
|
|
284
|
-
// Mock wallet provider
|
|
285
|
-
await context.addInitScript(() => {
|
|
286
|
-
window.ethereum = {
|
|
287
|
-
isMetaMask: true,
|
|
288
|
-
request: async ({ method }) => {
|
|
289
|
-
if (method === 'eth_requestAccounts')
|
|
290
|
-
return ['0x1234567890123456789012345678901234567890']
|
|
291
|
-
if (method === 'eth_chainId') return '0x1'
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
await page.goto('/')
|
|
297
|
-
await page.locator('[data-testid="connect-wallet"]').click()
|
|
298
|
-
await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234')
|
|
299
|
-
})
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
## Financial / Critical Flow Testing
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
test('trade execution', async ({ page }) => {
|
|
306
|
-
// Skip on production — real money
|
|
307
|
-
test.skip(process.env.NODE_ENV === 'production', 'Skip on production')
|
|
308
|
-
|
|
309
|
-
await page.goto('/markets/test-market')
|
|
310
|
-
await page.locator('[data-testid="position-yes"]').click()
|
|
311
|
-
await page.locator('[data-testid="trade-amount"]').fill('1.0')
|
|
312
|
-
|
|
313
|
-
// Verify preview
|
|
314
|
-
const preview = page.locator('[data-testid="trade-preview"]')
|
|
315
|
-
await expect(preview).toContainText('1.0')
|
|
316
|
-
|
|
317
|
-
// Confirm and wait for blockchain
|
|
318
|
-
await page.locator('[data-testid="confirm-trade"]').click()
|
|
319
|
-
await page.waitForResponse(
|
|
320
|
-
resp => resp.url().includes('/api/trade') && resp.status() === 200,
|
|
321
|
-
{ timeout: 30000 }
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
await expect(page.locator('[data-testid="trade-success"]')).toBeVisible()
|
|
325
|
-
})
|
|
326
|
-
```
|