oh-my-customcode 0.49.0 → 0.51.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.md +3 -3
- package/dist/cli/index.js +59 -14
- package/dist/index.js +83 -14
- package/package.json +1 -1
- package/templates/.claude/skills/scout/SKILL.md +248 -0
- package/templates/.claude/skills/systematic-debugging/SKILL.md +288 -0
- package/templates/.claude/skills/systematic-debugging/condition-based-waiting-example.ts +278 -0
- package/templates/.claude/skills/systematic-debugging/condition-based-waiting.md +240 -0
- package/templates/.claude/skills/systematic-debugging/defense-in-depth.md +252 -0
- package/templates/.claude/skills/systematic-debugging/find-polluter.sh +147 -0
- package/templates/.claude/skills/systematic-debugging/root-cause-tracing.md +87 -0
- package/templates/CLAUDE.md +2 -1
- package/templates/manifest.json +2 -2
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systematic-debugging
|
|
3
|
+
description: Use when encountering any bug, test failure, or unexpected behavior. Enforces a strict reproduce-first, root-cause-first, failing-test-first debugging workflow before fixing.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
user-invocable: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- Source: https://github.com/tmdgusya/engineering-disciplines (MIT License) -->
|
|
9
|
+
|
|
10
|
+
# Systematic Debugging
|
|
11
|
+
|
|
12
|
+
엄격한 디버깅 워크플로우다. 버그, 테스트 실패, 예기치 않은 동작을 다룰 때 사용한다.
|
|
13
|
+
|
|
14
|
+
핵심 목적은 세 가지다.
|
|
15
|
+
|
|
16
|
+
1. 증상이 아니라 원인을 고친다.
|
|
17
|
+
2. 추측 기반 수정을 막는다.
|
|
18
|
+
3. 실패를 테스트로 고정한 뒤 수정한다.
|
|
19
|
+
|
|
20
|
+
## Hard Gates
|
|
21
|
+
|
|
22
|
+
다음 규칙은 예외 없이 따른다.
|
|
23
|
+
|
|
24
|
+
1. **재현 또는 관측 가능 상태를 만들기 전에는 수정하지 않는다.**
|
|
25
|
+
2. **원인 가설을 명시하기 전에는 수정하지 않는다.**
|
|
26
|
+
3. **실패 테스트 또는 동등한 재현 장치를 만들기 전에는 수정하지 않는다.**
|
|
27
|
+
4. **한 번에 하나의 가설만 검증한다.**
|
|
28
|
+
5. **수정 시 "while I'm here" 리팩터링을 금지한다.**
|
|
29
|
+
6. **수정 시도가 3번 실패하면 추가 패치 전에 구조적 문제를 의심한다.**
|
|
30
|
+
|
|
31
|
+
이 과정을 어기는 것은 디버깅 실패로 본다.
|
|
32
|
+
|
|
33
|
+
## When To Use
|
|
34
|
+
|
|
35
|
+
다음 상황이면 이 스킬을 사용한다.
|
|
36
|
+
|
|
37
|
+
- 테스트가 실패할 때
|
|
38
|
+
- 운영 또는 로컬에서 버그가 발생할 때
|
|
39
|
+
- 예상과 다른 응답, 상태, 렌더링, 쿼리 결과가 나올 때
|
|
40
|
+
- 성능 저하, 타임아웃, 레이스 컨디션, 간헐 실패를 조사할 때
|
|
41
|
+
- 이미 한 번 이상 고쳤는데 다시 깨졌을 때
|
|
42
|
+
|
|
43
|
+
다음 핑계는 허용하지 않는다.
|
|
44
|
+
|
|
45
|
+
- "간단해 보여서 바로 고치면 된다"
|
|
46
|
+
- "시간이 없으니 일단 패치하고 보자"
|
|
47
|
+
- "이거 같으니까 그냥 바꿔보자"
|
|
48
|
+
|
|
49
|
+
## Required Output Contract
|
|
50
|
+
|
|
51
|
+
이 스킬을 사용할 때는 내부적으로 아래 항목을 반드시 고정한다.
|
|
52
|
+
|
|
53
|
+
1. **Problem statement**: 무엇이 잘못되었는지 한 문장으로 정의
|
|
54
|
+
2. **Reproduction path**: 어떻게 실패를 재현하거나 관측할지
|
|
55
|
+
3. **Evidence**: 실제 관측 결과
|
|
56
|
+
4. **Root-cause hypothesis**: 왜 이 문제가 발생한다고 보는지
|
|
57
|
+
5. **Failing guard**: 실패 테스트, 재현 스크립트, 로그 검증 중 하나
|
|
58
|
+
6. **Fix**: 원인에 대한 단일 수정
|
|
59
|
+
7. **Verification**: 수정 후 재현 경로와 관련 테스트 결과
|
|
60
|
+
|
|
61
|
+
이 7개 중 빠진 항목이 있으면 아직 끝난 일이 아니다.
|
|
62
|
+
|
|
63
|
+
## Workflow
|
|
64
|
+
|
|
65
|
+
반드시 아래 순서로 진행한다.
|
|
66
|
+
|
|
67
|
+
### Phase 1. Define The Problem
|
|
68
|
+
|
|
69
|
+
먼저 문제를 축약한다.
|
|
70
|
+
|
|
71
|
+
- 실제 기대 동작은 무엇인가
|
|
72
|
+
- 실제 관측 동작은 무엇인가
|
|
73
|
+
- 영향 범위는 어디까지인가
|
|
74
|
+
- 항상 재현되는가, 간헐적인가
|
|
75
|
+
|
|
76
|
+
출력 형식:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Problem: <expected> but got <actual> under <condition>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
증상과 추측을 섞지 않는다.
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Good: Product detail API returns 500 when brand is null.
|
|
86
|
+
Bad: Serializer is broken because brand mapping seems wrong.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Phase 2. Reproduce Or Instrument
|
|
90
|
+
|
|
91
|
+
수정 전에 실패를 다시 볼 수 있어야 한다.
|
|
92
|
+
|
|
93
|
+
우선순위:
|
|
94
|
+
|
|
95
|
+
1. 기존 테스트로 재현
|
|
96
|
+
2. 최소 통합 테스트로 재현
|
|
97
|
+
3. 단위 테스트로 재현
|
|
98
|
+
4. 재현 스크립트 또는 명령으로 관측
|
|
99
|
+
5. 로그/계측 추가 후 관측
|
|
100
|
+
|
|
101
|
+
규칙:
|
|
102
|
+
|
|
103
|
+
- 재현 경로는 가능한 한 가장 작게 만든다.
|
|
104
|
+
- UI에서만 보이는 버그라도 더 아래 계층에서 재현 가능하면 그쪽을 선호한다.
|
|
105
|
+
- 간헐 실패면 로그, 입력, 시간, 동시성 조건을 추가해 관측성을 높인다.
|
|
106
|
+
- 재현되지 않으면 수정으로 넘어가지 말고 관측 수단을 늘린다.
|
|
107
|
+
|
|
108
|
+
재현 불가 상태에서 해야 할 일:
|
|
109
|
+
|
|
110
|
+
1. 입력값 기록
|
|
111
|
+
2. 환경 차이 확인
|
|
112
|
+
3. 최근 변경점 확인
|
|
113
|
+
4. 경계 지점별 로그 추가
|
|
114
|
+
5. 동일 증상을 만드는 더 작은 조건 탐색
|
|
115
|
+
|
|
116
|
+
### Phase 3. Gather Evidence
|
|
117
|
+
|
|
118
|
+
관측 가능한 사실만 모은다.
|
|
119
|
+
|
|
120
|
+
항상 확인할 것:
|
|
121
|
+
|
|
122
|
+
- 에러 메시지와 스택트레이스 전문
|
|
123
|
+
- 실패 입력값
|
|
124
|
+
- 최근 변경 파일 또는 커밋
|
|
125
|
+
- 환경/설정 차이
|
|
126
|
+
- 호출 경로와 데이터 흐름
|
|
127
|
+
|
|
128
|
+
멀티 컴포넌트 문제에서는 경계마다 확인한다.
|
|
129
|
+
|
|
130
|
+
예시:
|
|
131
|
+
|
|
132
|
+
- controller -> application -> service -> repository
|
|
133
|
+
- client -> API -> external service
|
|
134
|
+
- scheduler -> batch service -> database
|
|
135
|
+
|
|
136
|
+
각 경계에서 확인할 것:
|
|
137
|
+
|
|
138
|
+
- 무엇이 들어왔는가
|
|
139
|
+
- 무엇이 나갔는가
|
|
140
|
+
- 어떤 값이 변형되었는가
|
|
141
|
+
- 어떤 조건에서만 깨지는가
|
|
142
|
+
|
|
143
|
+
문제 위치를 특정하기 전에는 고치지 않는다.
|
|
144
|
+
|
|
145
|
+
### Phase 4. Isolate Root Cause
|
|
146
|
+
|
|
147
|
+
원인 후보를 하나만 세운다.
|
|
148
|
+
|
|
149
|
+
형식:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
Hypothesis: <root cause> because <evidence>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
좋은 가설의 조건:
|
|
156
|
+
|
|
157
|
+
- 단일 원인을 가리킨다
|
|
158
|
+
- 관측 증거와 연결된다
|
|
159
|
+
- 작은 실험으로 반증 가능하다
|
|
160
|
+
|
|
161
|
+
나쁜 가설의 예:
|
|
162
|
+
|
|
163
|
+
- "어딘가 비동기 문제가 있는 것 같다"
|
|
164
|
+
- "직렬화 쪽 전체가 불안정한 듯하다"
|
|
165
|
+
|
|
166
|
+
원인을 소스까지 거슬러 올라간다. 오류가 깊은 스택에서 보이면 증상이 아니라 입력의 출처를 추적한다.
|
|
167
|
+
|
|
168
|
+
### Phase 5. Lock The Failure
|
|
169
|
+
|
|
170
|
+
수정 전에 실패를 고정한다.
|
|
171
|
+
|
|
172
|
+
우선순위:
|
|
173
|
+
|
|
174
|
+
1. 자동화된 failing test
|
|
175
|
+
2. 기존 테스트에 회귀 케이스 추가
|
|
176
|
+
3. 최소 재현 스크립트
|
|
177
|
+
4. 로그/어설션 기반 임시 검증 장치
|
|
178
|
+
|
|
179
|
+
규칙:
|
|
180
|
+
|
|
181
|
+
- 가능하면 자동화 테스트를 만든다.
|
|
182
|
+
- 수정 전에는 실패해야 한다.
|
|
183
|
+
- 수정 후에는 같은 경로에서 통과해야 한다.
|
|
184
|
+
- 테스트 이름은 무엇이 깨졌는지 드러내야 한다.
|
|
185
|
+
|
|
186
|
+
자동화 테스트를 쓸 수 있으면 `test-driven-development` 스킬을 함께 사용한다.
|
|
187
|
+
|
|
188
|
+
### Phase 6. Implement A Single Fix
|
|
189
|
+
|
|
190
|
+
수정은 하나의 가설만 다룬다.
|
|
191
|
+
|
|
192
|
+
허용:
|
|
193
|
+
|
|
194
|
+
- 원인에 직접 대응하는 최소 코드 변경
|
|
195
|
+
- 검증에 필요한 최소한의 보조 수정
|
|
196
|
+
|
|
197
|
+
금지:
|
|
198
|
+
|
|
199
|
+
- 관련 있어 보이는 여러 수정 묶기
|
|
200
|
+
- 리팩터링 겸 수정
|
|
201
|
+
- 포맷/정리/이름 변경 끼워넣기
|
|
202
|
+
- 근거 없는 null-guard 추가
|
|
203
|
+
- 예외 삼키기
|
|
204
|
+
|
|
205
|
+
실패하면 즉시 다시 Phase 1 또는 Phase 3으로 돌아간다. 이전 가설이 틀렸다는 뜻이다.
|
|
206
|
+
|
|
207
|
+
### Phase 7. Verify And Close
|
|
208
|
+
|
|
209
|
+
아래를 모두 만족해야 종료한다.
|
|
210
|
+
|
|
211
|
+
1. 원래 재현 경로가 더 이상 실패하지 않는다.
|
|
212
|
+
2. 새 failing guard가 통과한다.
|
|
213
|
+
3. 관련 테스트가 깨지지 않는다.
|
|
214
|
+
4. 수정이 증상이 아니라 원인을 막는다는 설명이 가능하다.
|
|
215
|
+
|
|
216
|
+
간헐 버그라면 한 번 통과로 끝내지 않는다. 반복 실행 또는 조건 변화 하 검증이 필요하다.
|
|
217
|
+
|
|
218
|
+
## Stop Conditions
|
|
219
|
+
|
|
220
|
+
다음 상황이면 멈추고 프레임을 다시 잡는다.
|
|
221
|
+
|
|
222
|
+
### 1. Reproduction Failed
|
|
223
|
+
|
|
224
|
+
여러 번 시도해도 재현이 안 되면:
|
|
225
|
+
|
|
226
|
+
- 관측 수단이 부족한지 본다.
|
|
227
|
+
- 환경 차이가 있는지 본다.
|
|
228
|
+
- 문제 정의가 잘못되었는지 본다.
|
|
229
|
+
|
|
230
|
+
재현이 안 되는데 코드를 바꾸는 것은 금지다.
|
|
231
|
+
|
|
232
|
+
### 2. Three Failed Fixes
|
|
233
|
+
|
|
234
|
+
세 번 연속으로 수정이 빗나가면 이렇게 판단한다.
|
|
235
|
+
|
|
236
|
+
- 현재 이해가 틀렸거나
|
|
237
|
+
- 문제가 공유 상태, 경계 설계, 책임 분리 같은 구조 문제일 가능성이 크다
|
|
238
|
+
|
|
239
|
+
이 시점부터는 "네 번째 땜질"이 아니라 구조 논의가 필요하다.
|
|
240
|
+
|
|
241
|
+
### 3. No Failing Guard
|
|
242
|
+
|
|
243
|
+
실패 테스트나 동등한 재현 장치를 만들 수 없으면, 완료로 선언하지 않는다. 최소한 재현 명령과 관측 결과를 남긴다.
|
|
244
|
+
|
|
245
|
+
## Red Flags
|
|
246
|
+
|
|
247
|
+
아래 생각이 들면 즉시 멈추고 앞 단계로 돌아간다.
|
|
248
|
+
|
|
249
|
+
- "이 줄만 바꿔보면 될 것 같다"
|
|
250
|
+
- "로그는 나중에 보고 일단 수정해보자"
|
|
251
|
+
- "테스트는 나중에 추가하지 뭐"
|
|
252
|
+
- "한 번에 이것도 저것도 같이 고치자"
|
|
253
|
+
- "에러는 사라졌으니 원인은 몰라도 됐다"
|
|
254
|
+
|
|
255
|
+
## Minimal Checklist
|
|
256
|
+
|
|
257
|
+
실행 중에는 아래 체크리스트를 기준으로 스스로 검증한다.
|
|
258
|
+
|
|
259
|
+
- [ ] 문제를 한 문장으로 정의했다
|
|
260
|
+
- [ ] 실패를 재현하거나 관측 가능하게 만들었다
|
|
261
|
+
- [ ] 증거를 수집했다
|
|
262
|
+
- [ ] 단일 원인 가설을 만들었다
|
|
263
|
+
- [ ] 수정 전 실패 guard를 만들었다
|
|
264
|
+
- [ ] 단일 수정만 적용했다
|
|
265
|
+
- [ ] 같은 경로로 수정 후 검증했다
|
|
266
|
+
|
|
267
|
+
## Completion Standard
|
|
268
|
+
|
|
269
|
+
이 스킬의 완료 기준은 "코드가 바뀌었다"가 아니다.
|
|
270
|
+
|
|
271
|
+
완료 기준:
|
|
272
|
+
|
|
273
|
+
- 문제 정의가 명확하다
|
|
274
|
+
- 실패가 수정 전에 고정되었다
|
|
275
|
+
- 수정이 원인과 연결된다
|
|
276
|
+
- 검증 결과가 남아 있다
|
|
277
|
+
|
|
278
|
+
이 네 가지가 없으면 디버깅은 끝난 것이 아니다.
|
|
279
|
+
|
|
280
|
+
## Reference Materials
|
|
281
|
+
|
|
282
|
+
This skill includes reference documents for specific debugging techniques:
|
|
283
|
+
|
|
284
|
+
- `root-cause-tracing.md` — Tracing bugs back through call chains to the original trigger
|
|
285
|
+
- `defense-in-depth.md` — Adding validation at every layer to make bugs structurally impossible
|
|
286
|
+
- `condition-based-waiting.md` — Replacing arbitrary delays with condition-based polling
|
|
287
|
+
- `find-polluter.sh` — Bisection script for finding test pollution sources
|
|
288
|
+
- `condition-based-waiting-example.ts` — Complete implementation of condition-based waiting utilities
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// Source: https://github.com/tmdgusya/engineering-disciplines (MIT License)
|
|
2
|
+
// Complete implementation of condition-based waiting utilities
|
|
3
|
+
|
|
4
|
+
import { access, readFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Core waitUntil implementation
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
export interface WaitOptions {
|
|
12
|
+
/** Maximum wait time in milliseconds. Default: 5000 */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
/** Polling interval in milliseconds. Default: 100 */
|
|
15
|
+
interval?: number;
|
|
16
|
+
/** Error message to show on timeout */
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Waits until the condition returns true, polling at the specified interval.
|
|
22
|
+
* Throws a timeout error if the condition is not met within the timeout period.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Wait for a file to exist
|
|
26
|
+
* await waitUntil(
|
|
27
|
+
* () => fileExists('/path/to/file'),
|
|
28
|
+
* { timeout: 5000, message: 'File was not created' }
|
|
29
|
+
* );
|
|
30
|
+
*/
|
|
31
|
+
export async function waitUntil(
|
|
32
|
+
condition: () => boolean | Promise<boolean>,
|
|
33
|
+
options: WaitOptions = {}
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const { timeout = 5000, interval = 100, message = 'Condition was not met' } = options;
|
|
36
|
+
const deadline = Date.now() + timeout;
|
|
37
|
+
|
|
38
|
+
while (Date.now() < deadline) {
|
|
39
|
+
const result = await condition();
|
|
40
|
+
if (result) return;
|
|
41
|
+
await sleep(interval);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error(`waitUntil timeout after ${timeout}ms: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Waits until the condition returns a truthy value (not undefined/null/false),
|
|
49
|
+
* then returns that value.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const record = await waitFor(
|
|
53
|
+
* () => db.find({ id: 'expected-id' }),
|
|
54
|
+
* { timeout: 3000, message: 'Record was not created' }
|
|
55
|
+
* );
|
|
56
|
+
*/
|
|
57
|
+
export async function waitFor<T>(
|
|
58
|
+
condition: () => T | Promise<T>,
|
|
59
|
+
options: WaitOptions = {}
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
const { timeout = 5000, interval = 100, message = 'Value was not available' } = options;
|
|
62
|
+
const deadline = Date.now() + timeout;
|
|
63
|
+
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
const result = await condition();
|
|
66
|
+
if (result) return result;
|
|
67
|
+
await sleep(interval);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(`waitFor timeout after ${timeout}ms: ${message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================
|
|
74
|
+
// Common condition helpers
|
|
75
|
+
// ============================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns true if the file exists and is accessible.
|
|
79
|
+
*/
|
|
80
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
await access(filePath);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns true if the file exists and has content (size > 0).
|
|
91
|
+
*/
|
|
92
|
+
export async function fileHasContent(filePath: string): Promise<boolean> {
|
|
93
|
+
try {
|
|
94
|
+
const stats = await stat(filePath);
|
|
95
|
+
return stats.size > 0;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns true if the file exists and contains the expected text.
|
|
103
|
+
*/
|
|
104
|
+
export async function fileContains(filePath: string, text: string): Promise<boolean> {
|
|
105
|
+
try {
|
|
106
|
+
const content = await readFile(filePath, 'utf-8');
|
|
107
|
+
return content.includes(text);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns true if the HTTP endpoint responds with a successful status code.
|
|
115
|
+
*/
|
|
116
|
+
export async function httpEndpointReady(url: string): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1000) });
|
|
119
|
+
return response.ok;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns true if all files in the list exist.
|
|
127
|
+
*/
|
|
128
|
+
export async function allFilesExist(filePaths: string[]): Promise<boolean> {
|
|
129
|
+
const results = await Promise.all(filePaths.map(fileExists));
|
|
130
|
+
return results.every(Boolean);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// Convenience waiters
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Waits for a file to exist.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* await waitForFile('/path/to/output.json', { timeout: 5000 });
|
|
142
|
+
*/
|
|
143
|
+
export async function waitForFile(filePath: string, options: WaitOptions = {}): Promise<void> {
|
|
144
|
+
await waitUntil(() => fileExists(filePath), {
|
|
145
|
+
timeout: 5000,
|
|
146
|
+
message: `File not created: ${filePath}`,
|
|
147
|
+
...options,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Waits for a file to contain specific text.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* await waitForFileContent('/path/to/log.txt', 'Server started', { timeout: 10000 });
|
|
156
|
+
*/
|
|
157
|
+
export async function waitForFileContent(
|
|
158
|
+
filePath: string,
|
|
159
|
+
text: string,
|
|
160
|
+
options: WaitOptions = {}
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
await waitUntil(() => fileContains(filePath, text), {
|
|
163
|
+
timeout: 5000,
|
|
164
|
+
message: `File ${filePath} did not contain: ${text}`,
|
|
165
|
+
...options,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Waits for an HTTP endpoint to respond with a successful status.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* await waitForServer('http://localhost:3000/health', { timeout: 15000 });
|
|
174
|
+
*/
|
|
175
|
+
export async function waitForServer(url: string, options: WaitOptions = {}): Promise<void> {
|
|
176
|
+
await waitUntil(() => httpEndpointReady(url), {
|
|
177
|
+
timeout: 15000,
|
|
178
|
+
interval: 300,
|
|
179
|
+
message: `Server at ${url} did not become ready`,
|
|
180
|
+
...options,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Waits for a directory to contain at least minCount files.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* await waitForDirectoryCount('/output/dir', 3, { timeout: 5000 });
|
|
189
|
+
*/
|
|
190
|
+
export async function waitForDirectoryCount(
|
|
191
|
+
dirPath: string,
|
|
192
|
+
minCount: number,
|
|
193
|
+
options: WaitOptions = {}
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
const { readdir } = await import('node:fs/promises');
|
|
196
|
+
await waitUntil(
|
|
197
|
+
async () => {
|
|
198
|
+
try {
|
|
199
|
+
const entries = await readdir(dirPath);
|
|
200
|
+
return entries.length >= minCount;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
timeout: 5000,
|
|
207
|
+
message: `Directory ${dirPath} did not reach ${minCount} files`,
|
|
208
|
+
...options,
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ============================================================
|
|
214
|
+
// Utilities
|
|
215
|
+
// ============================================================
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Sleep for the specified number of milliseconds.
|
|
219
|
+
*/
|
|
220
|
+
export function sleep(ms: number): Promise<void> {
|
|
221
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// Usage examples (not for import, documentation only)
|
|
226
|
+
// ============================================================
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
// Example 1: Wait for build output file
|
|
230
|
+
await waitForFile(join(outputDir, 'bundle.js'), { timeout: 30000 });
|
|
231
|
+
|
|
232
|
+
// Example 2: Wait for server to start
|
|
233
|
+
await waitForServer('http://localhost:8080/health', { timeout: 20000 });
|
|
234
|
+
|
|
235
|
+
// Example 3: Wait for database record with custom condition
|
|
236
|
+
const user = await waitFor(
|
|
237
|
+
async () => {
|
|
238
|
+
const u = await db.users.findUnique({ where: { email: 'test@example.com' } });
|
|
239
|
+
return u?.emailVerified ? u : null;
|
|
240
|
+
},
|
|
241
|
+
{ timeout: 5000, message: 'User email was not verified' }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Example 4: Wait for process output
|
|
245
|
+
const logs: string[] = [];
|
|
246
|
+
const proc = spawn('npm', ['start']);
|
|
247
|
+
proc.stdout.on('data', (chunk) => logs.push(chunk.toString()));
|
|
248
|
+
|
|
249
|
+
await waitUntil(
|
|
250
|
+
() => logs.some((line) => line.includes('Listening on port')),
|
|
251
|
+
{ timeout: 15000, interval: 200, message: 'Server did not log startup message' }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Example 5: Wait for all output files to be generated
|
|
255
|
+
const expectedFiles = ['report.json', 'summary.txt', 'data.csv'].map((f) =>
|
|
256
|
+
join(outputDir, f)
|
|
257
|
+
);
|
|
258
|
+
await waitUntil(() => allFilesExist(expectedFiles), {
|
|
259
|
+
timeout: 10000,
|
|
260
|
+
message: `Not all output files were generated in ${outputDir}`,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Example 6: Test that verifies count reaches expected value
|
|
264
|
+
it('should process all items', async () => {
|
|
265
|
+
await triggerBatchProcessing(items);
|
|
266
|
+
|
|
267
|
+
await waitUntil(
|
|
268
|
+
async () => {
|
|
269
|
+
const processed = await db.items.count({ where: { status: 'done' } });
|
|
270
|
+
return processed >= items.length;
|
|
271
|
+
},
|
|
272
|
+
{ timeout: 5000, message: `Expected ${items.length} items to be processed` }
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const processed = await db.items.count({ where: { status: 'done' } });
|
|
276
|
+
expect(processed).toBe(items.length);
|
|
277
|
+
});
|
|
278
|
+
*/
|