oh-my-customcode 0.49.0 → 0.50.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/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 +1 -1
- package/templates/manifest.json +2 -2
|
@@ -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
|
+
*/
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Condition-Based Waiting
|
|
2
|
+
|
|
3
|
+
<!-- Source: https://github.com/tmdgusya/engineering-disciplines (MIT License) -->
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Arbitrary delays (`sleep`, `setTimeout`, `await new Promise(r => setTimeout(r, 1000))`) are a debugging anti-pattern. They hide timing issues, make tests slow, and still fail intermittently.
|
|
8
|
+
|
|
9
|
+
**Core principle:** Replace arbitrary delays with condition-based polling that waits for the actual state you need.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
**Use when:**
|
|
14
|
+
- Tests use `await sleep(1000)` or similar arbitrary waits
|
|
15
|
+
- Code has `setTimeout` for "giving time" to async operations
|
|
16
|
+
- Tests are flaky because timing depends on system speed
|
|
17
|
+
- You need to wait for: file to exist, process to start, server to be ready, database record to appear
|
|
18
|
+
|
|
19
|
+
## The Problem with Arbitrary Delays
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// ❌ Arbitrary delay - fragile and slow
|
|
23
|
+
await fs.writeFile(path, content);
|
|
24
|
+
await sleep(500); // "Give it time to write"
|
|
25
|
+
const result = await fs.readFile(path);
|
|
26
|
+
|
|
27
|
+
// Problems:
|
|
28
|
+
// 1. 500ms may not be enough on slow systems
|
|
29
|
+
// 2. 500ms is always wasted on fast systems
|
|
30
|
+
// 3. The actual condition (file readable) is never verified
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The Solution: Condition-Based Waiting
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// ✓ Condition-based - fast and reliable
|
|
37
|
+
await fs.writeFile(path, content);
|
|
38
|
+
await waitUntil(() => fs.access(path).then(() => true).catch(() => false));
|
|
39
|
+
const result = await fs.readFile(path);
|
|
40
|
+
|
|
41
|
+
// Benefits:
|
|
42
|
+
// 1. Proceeds as soon as condition is met (fast on fast systems)
|
|
43
|
+
// 2. Has a timeout for safety (catches real failures)
|
|
44
|
+
// 3. The actual condition is explicit and verified
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Core Implementation
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
interface WaitOptions {
|
|
51
|
+
timeout?: number; // Max wait time in ms (default: 5000)
|
|
52
|
+
interval?: number; // Poll interval in ms (default: 100)
|
|
53
|
+
message?: string; // Error message on timeout
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function waitUntil(
|
|
57
|
+
condition: () => boolean | Promise<boolean>,
|
|
58
|
+
options: WaitOptions = {}
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const { timeout = 5000, interval = 100, message = 'Condition not met' } = options;
|
|
61
|
+
const deadline = Date.now() + timeout;
|
|
62
|
+
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
const result = await condition();
|
|
65
|
+
if (result) return;
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, interval));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`waitUntil timeout after ${timeout}ms: ${message}`);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See `condition-based-waiting-example.ts` for the complete implementation with all utilities.
|
|
74
|
+
|
|
75
|
+
## Common Patterns
|
|
76
|
+
|
|
77
|
+
### Wait for File to Exist
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ❌ Arbitrary delay
|
|
81
|
+
await triggerFileCreation();
|
|
82
|
+
await sleep(1000);
|
|
83
|
+
const content = await fs.readFile(outputPath, 'utf-8');
|
|
84
|
+
|
|
85
|
+
// ✓ Condition-based
|
|
86
|
+
await triggerFileCreation();
|
|
87
|
+
await waitUntil(
|
|
88
|
+
() => fs.access(outputPath).then(() => true).catch(() => false),
|
|
89
|
+
{ timeout: 5000, message: `File not created: ${outputPath}` }
|
|
90
|
+
);
|
|
91
|
+
const content = await fs.readFile(outputPath, 'utf-8');
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Wait for Process to Start
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// ❌ Arbitrary delay
|
|
98
|
+
startServer();
|
|
99
|
+
await sleep(2000); // "Give server time to start"
|
|
100
|
+
const response = await fetch('http://localhost:3000/health');
|
|
101
|
+
|
|
102
|
+
// ✓ Condition-based
|
|
103
|
+
startServer();
|
|
104
|
+
await waitUntil(
|
|
105
|
+
async () => {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch('http://localhost:3000/health');
|
|
108
|
+
return res.ok;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{ timeout: 10000, message: 'Server did not start within 10s' }
|
|
114
|
+
);
|
|
115
|
+
const response = await fetch('http://localhost:3000/health');
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Wait for Database Record
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// ❌ Arbitrary delay
|
|
122
|
+
await triggerAsyncOperation();
|
|
123
|
+
await sleep(500);
|
|
124
|
+
const record = await db.find({ id: expectedId });
|
|
125
|
+
|
|
126
|
+
// ✓ Condition-based
|
|
127
|
+
await triggerAsyncOperation();
|
|
128
|
+
await waitUntil(
|
|
129
|
+
async () => {
|
|
130
|
+
const record = await db.find({ id: expectedId });
|
|
131
|
+
return record !== null;
|
|
132
|
+
},
|
|
133
|
+
{ timeout: 3000, message: `Record ${expectedId} not created` }
|
|
134
|
+
);
|
|
135
|
+
const record = await db.find({ id: expectedId });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Wait for Log Output
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ Arbitrary delay
|
|
142
|
+
process.spawn('my-command');
|
|
143
|
+
await sleep(1000);
|
|
144
|
+
expect(logOutput).toContain('Server started');
|
|
145
|
+
|
|
146
|
+
// ✓ Condition-based
|
|
147
|
+
const logOutput: string[] = [];
|
|
148
|
+
const proc = process.spawn('my-command');
|
|
149
|
+
proc.stdout.on('data', (chunk) => logOutput.push(chunk.toString()));
|
|
150
|
+
|
|
151
|
+
await waitUntil(
|
|
152
|
+
() => logOutput.some(line => line.includes('Server started')),
|
|
153
|
+
{ timeout: 10000, message: 'Server start message not seen in logs' }
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Wait for Count to Reach Expected Value
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// ❌ Arbitrary delay
|
|
161
|
+
await triggerBatchOperation();
|
|
162
|
+
await sleep(2000);
|
|
163
|
+
const items = await db.findAll();
|
|
164
|
+
expect(items.length).toBe(10);
|
|
165
|
+
|
|
166
|
+
// ✓ Condition-based
|
|
167
|
+
await triggerBatchOperation();
|
|
168
|
+
await waitUntil(
|
|
169
|
+
async () => {
|
|
170
|
+
const items = await db.findAll();
|
|
171
|
+
return items.length >= 10;
|
|
172
|
+
},
|
|
173
|
+
{ timeout: 5000, message: 'Batch did not complete: expected 10 items' }
|
|
174
|
+
);
|
|
175
|
+
const items = await db.findAll();
|
|
176
|
+
expect(items.length).toBe(10);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Choosing Timeout and Interval Values
|
|
180
|
+
|
|
181
|
+
| Scenario | Timeout | Interval | Rationale |
|
|
182
|
+
|----------|---------|----------|-----------|
|
|
183
|
+
| File write | 2s | 50ms | Fast local I/O |
|
|
184
|
+
| Process start | 10s | 200ms | Process startup varies |
|
|
185
|
+
| HTTP server ready | 15s | 300ms | Server may need to compile |
|
|
186
|
+
| Database record | 3s | 100ms | DB writes are fast |
|
|
187
|
+
| CI environment | 2-3x local | same | CI is often slower |
|
|
188
|
+
|
|
189
|
+
**Rule of thumb:**
|
|
190
|
+
- Interval: 10-20% of expected wait time, minimum 50ms
|
|
191
|
+
- Timeout: 3-5x the typical wait time
|
|
192
|
+
- Add a buffer for CI: multiply timeout by 2-3x
|
|
193
|
+
|
|
194
|
+
## Testing the Waiters Themselves
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Test that waitUntil resolves when condition becomes true
|
|
198
|
+
it('resolves when condition becomes true', async () => {
|
|
199
|
+
let ready = false;
|
|
200
|
+
setTimeout(() => { ready = true; }, 100);
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
waitUntil(() => ready, { timeout: 1000 })
|
|
204
|
+
).resolves.toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Test that waitUntil rejects on timeout
|
|
208
|
+
it('rejects with timeout error when condition never met', async () => {
|
|
209
|
+
await expect(
|
|
210
|
+
waitUntil(() => false, { timeout: 100, message: 'test condition' })
|
|
211
|
+
).rejects.toThrow('waitUntil timeout after 100ms: test condition');
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Migration Guide
|
|
216
|
+
|
|
217
|
+
When refactoring existing `sleep` calls:
|
|
218
|
+
|
|
219
|
+
1. **Identify what the sleep is "waiting for"** — read surrounding code
|
|
220
|
+
2. **Find the observable state change** — what becomes true when the operation completes?
|
|
221
|
+
3. **Replace with `waitUntil(condition)`** — poll for that state
|
|
222
|
+
4. **Set appropriate timeout** — 3-5x the typical wait time
|
|
223
|
+
5. **Add descriptive message** — what should have happened?
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Before
|
|
227
|
+
await startProcess();
|
|
228
|
+
await sleep(2000);
|
|
229
|
+
checkResult();
|
|
230
|
+
|
|
231
|
+
// After - step 1: what is sleep waiting for? Process to be ready.
|
|
232
|
+
// After - step 2: observable state? Process responds to health check.
|
|
233
|
+
// After - step 3-5:
|
|
234
|
+
await startProcess();
|
|
235
|
+
await waitUntil(
|
|
236
|
+
() => isProcessReady(),
|
|
237
|
+
{ timeout: 10000, message: 'Process did not become ready' }
|
|
238
|
+
);
|
|
239
|
+
checkResult();
|
|
240
|
+
```
|