@vibe-agent-toolkit/vat-development-agents 0.1.29 → 0.1.30-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.claude/plugins/marketplaces/vat-skills/CHANGELOG.md +20 -0
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/.claude-plugin/plugin.json +1 -1
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/audit/SKILL.md +21 -16
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/authoring/SKILL.md +30 -12
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/authoring/resources/skill-quality-checklist.md +42 -0
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/debugging/SKILL.md +4 -4
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/SKILL.md +6 -6
- package/dist/generated/resources/skills/vat-agent-authoring.js +3 -3
- package/dist/generated/resources/skills/vat-audit.js +5 -5
- package/dist/skills/audit/SKILL.md +21 -16
- package/dist/skills/authoring/SKILL.md +30 -12
- package/dist/skills/authoring/resources/skill-quality-checklist.md +42 -0
- package/dist/skills/debugging/SKILL.md +4 -4
- package/dist/skills/vibe-agent-toolkit/SKILL.md +6 -6
- package/package.json +4 -4
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/debugging/resources/CLAUDE.md +0 -539
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/debugging/resources/debug-and-test-vat-fixes.md +0 -111
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/debugging/resources/writing-tests.md +0 -577
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/adding-runtime-adapters.md +0 -628
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/agent-authoring.md +0 -905
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/compiling-markdown-to-typescript.md +0 -501
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/getting-started.md +0 -360
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/orchestration.md +0 -859
- package/dist/.claude/plugins/marketplaces/vat-skills/plugins/vibe-agent-toolkit/skills/vibe-agent-toolkit/resources/rag-usage-guide.md +0 -770
- package/dist/skills/debugging/resources/CLAUDE.md +0 -539
- package/dist/skills/debugging/resources/debug-and-test-vat-fixes.md +0 -111
- package/dist/skills/debugging/resources/writing-tests.md +0 -577
- package/dist/skills/vibe-agent-toolkit/resources/adding-runtime-adapters.md +0 -628
- package/dist/skills/vibe-agent-toolkit/resources/agent-authoring.md +0 -905
- package/dist/skills/vibe-agent-toolkit/resources/compiling-markdown-to-typescript.md +0 -501
- package/dist/skills/vibe-agent-toolkit/resources/getting-started.md +0 -360
- package/dist/skills/vibe-agent-toolkit/resources/orchestration.md +0 -859
- package/dist/skills/vibe-agent-toolkit/resources/rag-usage-guide.md +0 -770
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
# Writing Tests Guide
|
|
2
|
-
|
|
3
|
-
**CRITICAL**: Code duplication in tests will block commits and PR merges. Follow these patterns to avoid duplication from the start.
|
|
4
|
-
|
|
5
|
-
## Quick Reference
|
|
6
|
-
|
|
7
|
-
**When writing ANY new test file:**
|
|
8
|
-
1. Create `test/test-helpers.ts` if it doesn't exist
|
|
9
|
-
2. After writing 2-3 similar tests, extract a `setupXTestSuite()` helper
|
|
10
|
-
3. Use `toForwardSlash()` from `@vibe-agent-toolkit/utils` for cross-platform path comparisons
|
|
11
|
-
4. Run `bun run duplication-check` before committing
|
|
12
|
-
|
|
13
|
-
## Test File Organization
|
|
14
|
-
|
|
15
|
-
### Directory Structure
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
packages/my-package/
|
|
19
|
-
├── src/
|
|
20
|
-
│ └── my-module.ts # Source code
|
|
21
|
-
├── test/
|
|
22
|
-
│ ├── test-helpers.ts # Shared test utilities
|
|
23
|
-
│ ├── my-module.test.ts # Unit tests
|
|
24
|
-
│ ├── integration/
|
|
25
|
-
│ │ └── workflow.integration.test.ts
|
|
26
|
-
│ └── system/
|
|
27
|
-
│ └── e2e.system.test.ts
|
|
28
|
-
└── package.json
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Test Types
|
|
32
|
-
|
|
33
|
-
| Type | Location | Purpose | Speed | Dependencies |
|
|
34
|
-
|------|----------|---------|-------|--------------|
|
|
35
|
-
| **Unit** | `test/*.test.ts` | Test functions/classes in isolation | < 100ms | Mock external deps |
|
|
36
|
-
| **Integration** | `test/integration/*.integration.test.ts` | Test multiple modules together | < 5s | Real file system, DBs |
|
|
37
|
-
| **System** | `test/system/*.system.test.ts` | End-to-end workflows | < 30s | Real external services |
|
|
38
|
-
|
|
39
|
-
### Test Classification Rules
|
|
40
|
-
|
|
41
|
-
Misclassified tests are the #1 cause of flaky CI and slow unit test suites. If your test does any of the following, it is **NOT a unit test**:
|
|
42
|
-
|
|
43
|
-
| If your test... | It belongs in... | Why |
|
|
44
|
-
|----------------|-----------------|-----|
|
|
45
|
-
| Makes real HTTP requests | **Integration** | Network flakiness breaks CI; 2-15s per request |
|
|
46
|
-
| Loads ML models (Transformers.js, ONNX) | **Integration** | Model loading costs 2-5s; native bindings crash threads pool |
|
|
47
|
-
| Spawns child processes (`spawnSync`, `exec`) | **System** | Node startup overhead ~1-2s per spawn |
|
|
48
|
-
| Connects to a real database | **Integration** | Requires external service |
|
|
49
|
-
| Reads/writes real files (not mocked) | **Integration** | I/O-dependent, slower |
|
|
50
|
-
|
|
51
|
-
**Unit tests must be deterministic, fast, and isolated.** Mock all I/O, network, and heavy dependencies. If you're unsure, ask: "Would this test fail on an airplane?" If yes, it's not a unit test.
|
|
52
|
-
|
|
53
|
-
**Network-dependent integration tests** should use `describe.skipIf(!!process.env.CI)` if they hit external services that may be unreachable in CI:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// Tests that make real HTTP requests — skip in CI where egress may be restricted
|
|
57
|
-
describe.skipIf(!!process.env.CI)('ExternalLinkValidator (integration)', () => {
|
|
58
|
-
// ...
|
|
59
|
-
});
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## Vitest Pool Compatibility
|
|
63
|
-
|
|
64
|
-
Unit tests run with **threads pool on Mac/Unix** (shared module cache, ~20% faster) and **forks pool on Windows** (required for native module isolation). This affects what you can do in tests.
|
|
65
|
-
|
|
66
|
-
### `process.chdir()` — Forbidden in unit tests
|
|
67
|
-
|
|
68
|
-
`process.chdir()` throws in worker threads (all workers share one process). Use `vi.spyOn(process, 'cwd')` instead:
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
// ❌ WRONG — throws in threads pool (Mac/Unix unit tests)
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
originalCwd = process.cwd();
|
|
74
|
-
process.chdir(tempDir);
|
|
75
|
-
});
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
process.chdir(originalCwd);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ✅ CORRECT — works in both threads and forks
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
83
|
-
});
|
|
84
|
-
afterEach(() => {
|
|
85
|
-
vi.restoreAllMocks();
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
**Caveat**: `vi.spyOn(process, 'cwd')` only affects code that calls `process.cwd()`. It does NOT affect `fs.existsSync('relative/path')` — Node's `fs` module resolves relative paths against the real OS-level CWD, not the mocked one. If your code under test uses `fs` with relative paths, you need one of:
|
|
90
|
-
- Absolute paths in your test setup
|
|
91
|
-
- `process.chdir()` in integration tests (forks pool)
|
|
92
|
-
- Mocking the `fs` functions
|
|
93
|
-
|
|
94
|
-
### When `process.chdir()` is acceptable
|
|
95
|
-
|
|
96
|
-
In **integration** or **system** tests (which use forks pool), `process.chdir()` is safe. If a unit test absolutely requires it, add a comment explaining why and ensure the test restores CWD in `afterEach`.
|
|
97
|
-
|
|
98
|
-
## The Test Suite Helper Pattern
|
|
99
|
-
|
|
100
|
-
**CRITICAL**: The #1 source of test duplication is repeated `beforeEach`/`afterEach` setup across describe blocks.
|
|
101
|
-
|
|
102
|
-
### When to Create a Suite Helper
|
|
103
|
-
|
|
104
|
-
Create a `setupXTestSuite()` helper when:
|
|
105
|
-
- ✅ Starting a new test file (proactive)
|
|
106
|
-
- ✅ After writing 2-3 similar describe blocks (reactive)
|
|
107
|
-
- ✅ You notice repeated setup/teardown code
|
|
108
|
-
|
|
109
|
-
**Don't wait for 10+ duplicates to accumulate!**
|
|
110
|
-
|
|
111
|
-
### Basic Pattern
|
|
112
|
-
|
|
113
|
-
**Step 1: Create the helper** (in `test/test-helpers.ts`):
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
117
|
-
import { tmpdir } from 'node:os';
|
|
118
|
-
import { join } from 'node:path';
|
|
119
|
-
|
|
120
|
-
import { MyRegistry } from '../src/my-registry.js';
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Setup test suite with standard lifecycle hooks
|
|
124
|
-
* Eliminates duplication of beforeEach/afterEach setup
|
|
125
|
-
*/
|
|
126
|
-
export function setupMyTestSuite(testPrefix: string): {
|
|
127
|
-
tempDir: string;
|
|
128
|
-
registry: MyRegistry;
|
|
129
|
-
beforeEach: () => Promise<void>;
|
|
130
|
-
afterEach: () => Promise<void>;
|
|
131
|
-
} {
|
|
132
|
-
const suite = {
|
|
133
|
-
tempDir: '',
|
|
134
|
-
registry: null as unknown as MyRegistry,
|
|
135
|
-
beforeEach: async () => {
|
|
136
|
-
suite.tempDir = await mkdtemp(join(tmpdir(), testPrefix));
|
|
137
|
-
suite.registry = new MyRegistry();
|
|
138
|
-
},
|
|
139
|
-
afterEach: async () => {
|
|
140
|
-
await rm(suite.tempDir, { recursive: true, force: true });
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return suite;
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Step 2: Use in test files**:
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
152
|
-
import { setupMyTestSuite } from './test-helpers.js';
|
|
153
|
-
|
|
154
|
-
const suite = setupMyTestSuite('my-test-');
|
|
155
|
-
|
|
156
|
-
describe('MyModule basic usage', () => {
|
|
157
|
-
beforeEach(suite.beforeEach);
|
|
158
|
-
afterEach(suite.afterEach);
|
|
159
|
-
|
|
160
|
-
it('should work', async () => {
|
|
161
|
-
// Use suite.tempDir, suite.registry
|
|
162
|
-
const result = await doSomething(suite.tempDir);
|
|
163
|
-
expect(result).toBeDefined();
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('MyModule advanced usage', () => {
|
|
168
|
-
beforeEach(suite.beforeEach);
|
|
169
|
-
afterEach(suite.afterEach);
|
|
170
|
-
|
|
171
|
-
it('should also work', async () => {
|
|
172
|
-
// Same suite, different tests
|
|
173
|
-
const result = await doSomethingElse(suite.registry);
|
|
174
|
-
expect(result).toBeDefined();
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Real-World Examples
|
|
180
|
-
|
|
181
|
-
#### Example 1: Resource Tests
|
|
182
|
-
|
|
183
|
-
From `packages/resources/test/test-helpers.ts`:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
export function setupResourceTestSuite(testPrefix: string): {
|
|
187
|
-
tempDir: string;
|
|
188
|
-
registry: ResourceRegistry;
|
|
189
|
-
beforeEach: () => Promise<void>;
|
|
190
|
-
afterEach: () => Promise<void>;
|
|
191
|
-
} {
|
|
192
|
-
const suite = {
|
|
193
|
-
tempDir: '',
|
|
194
|
-
registry: null as unknown as ResourceRegistry,
|
|
195
|
-
beforeEach: async () => {
|
|
196
|
-
suite.tempDir = await mkdtemp(join(tmpdir(), testPrefix));
|
|
197
|
-
suite.registry = new ResourceRegistry();
|
|
198
|
-
},
|
|
199
|
-
afterEach: async () => {
|
|
200
|
-
await rm(suite.tempDir, { recursive: true, force: true });
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
return suite;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Impact**: Eliminated 8-10 lines per describe block across 6 describe blocks = ~50 lines removed
|
|
209
|
-
|
|
210
|
-
#### Example 2: RAG System Tests
|
|
211
|
-
|
|
212
|
-
From `packages/cli/test/system/test-helpers.ts`:
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
export function setupRagTestSuite(
|
|
216
|
-
testName: string,
|
|
217
|
-
binPath: string,
|
|
218
|
-
getTestOutputDir: (pkg: string, ...segments: string[]) => string
|
|
219
|
-
): {
|
|
220
|
-
tempDir: string;
|
|
221
|
-
projectDir: string;
|
|
222
|
-
dbPath: string;
|
|
223
|
-
beforeAll: () => void;
|
|
224
|
-
afterAll: () => void;
|
|
225
|
-
} {
|
|
226
|
-
const suite = {
|
|
227
|
-
tempDir: '',
|
|
228
|
-
projectDir: '',
|
|
229
|
-
dbPath: '',
|
|
230
|
-
beforeAll: () => {
|
|
231
|
-
suite.dbPath = getTestOutputDir('cli', 'system', `rag-${testName}-db`);
|
|
232
|
-
const result = setupIndexedRagTest(
|
|
233
|
-
`vat-rag-${testName}-test-`,
|
|
234
|
-
'test-project',
|
|
235
|
-
binPath,
|
|
236
|
-
suite.dbPath
|
|
237
|
-
);
|
|
238
|
-
suite.tempDir = result.tempDir;
|
|
239
|
-
suite.projectDir = result.projectDir;
|
|
240
|
-
},
|
|
241
|
-
afterAll: () => {
|
|
242
|
-
fs.rmSync(suite.tempDir, { recursive: true, force: true });
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
return suite;
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Impact**: Eliminated 41-62% duplication across 4 system test files
|
|
251
|
-
|
|
252
|
-
## Other Common Helper Patterns
|
|
253
|
-
|
|
254
|
-
### Factory Functions
|
|
255
|
-
|
|
256
|
-
Create test entities with sensible defaults:
|
|
257
|
-
|
|
258
|
-
```typescript
|
|
259
|
-
export function createTestResource(overrides?: Partial<Resource>): Resource {
|
|
260
|
-
return {
|
|
261
|
-
id: 'test-id',
|
|
262
|
-
name: 'Test Resource',
|
|
263
|
-
path: '/tmp/test.md',
|
|
264
|
-
...overrides,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Usage:
|
|
269
|
-
const resource = createTestResource({ name: 'Custom Name' });
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
### Assertion Helpers
|
|
273
|
-
|
|
274
|
-
Extract repeated assertion patterns:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
export async function assertValidation(
|
|
278
|
-
options: {
|
|
279
|
-
link: ResourceLink;
|
|
280
|
-
sourceFile: string;
|
|
281
|
-
expected: ValidationIssue | null;
|
|
282
|
-
},
|
|
283
|
-
expectFn: (actual: unknown) => Assertion<unknown>
|
|
284
|
-
): Promise<void> {
|
|
285
|
-
const result = await validateLink(options.link, options.sourceFile);
|
|
286
|
-
|
|
287
|
-
if (options.expected === null) {
|
|
288
|
-
expectFn(result).toBeNull();
|
|
289
|
-
} else {
|
|
290
|
-
expectFn(result).not.toBeNull();
|
|
291
|
-
expectFn(result?.severity).toBe(options.expected.severity);
|
|
292
|
-
expectFn(result?.type).toBe(options.expected.type);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Workflow Helpers
|
|
298
|
-
|
|
299
|
-
Combine setup → action → partial assert:
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
export async function createAndAddResource(
|
|
303
|
-
tempDir: string,
|
|
304
|
-
filename: string,
|
|
305
|
-
content: string,
|
|
306
|
-
registry: ResourceRegistry
|
|
307
|
-
): Promise<ResourceMetadata> {
|
|
308
|
-
const filePath = join(tempDir, filename);
|
|
309
|
-
await writeFile(filePath, content, 'utf-8');
|
|
310
|
-
const resource = await parseMarkdown(filePath);
|
|
311
|
-
registry.add(resource);
|
|
312
|
-
return resource;
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
## Cross-Platform Testing
|
|
317
|
-
|
|
318
|
-
### Path Comparisons
|
|
319
|
-
|
|
320
|
-
**CRITICAL**: Path comparisons must work on Windows (`\`) and Unix (`/`).
|
|
321
|
-
|
|
322
|
-
**Always use `toForwardSlash()` from utils when comparing paths**:
|
|
323
|
-
|
|
324
|
-
```typescript
|
|
325
|
-
// ❌ WRONG - fails on Windows
|
|
326
|
-
expect(resource.filePath.includes('/docs/')).toBe(true);
|
|
327
|
-
|
|
328
|
-
// ✅ CORRECT - works everywhere
|
|
329
|
-
import { toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
330
|
-
expect(toForwardSlash(resource.filePath).includes('/docs/')).toBe(true);
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
**Why this works**: Windows accepts both forward slashes and backslashes as path separators.
|
|
334
|
-
`toForwardSlash()` normalizes all paths to use forward slashes for consistent string comparisons.
|
|
335
|
-
|
|
336
|
-
**Example**:
|
|
337
|
-
```typescript
|
|
338
|
-
// Windows path: "docs\\api\\guide.md"
|
|
339
|
-
// Unix path: "docs/api/guide.md"
|
|
340
|
-
// Both normalize to: "docs/api/guide.md"
|
|
341
|
-
import { toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
342
|
-
expect(toForwardSlash(resource.filePath)).toContain('/api/')
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Path Construction
|
|
346
|
-
|
|
347
|
-
Use `path.join()` for constructing paths, never string concatenation:
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
// ❌ WRONG
|
|
351
|
-
const filePath = tempDir + '/' + 'test.md';
|
|
352
|
-
|
|
353
|
-
// ✅ CORRECT
|
|
354
|
-
const filePath = join(tempDir, 'test.md');
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Hardcoded Path Constants
|
|
358
|
-
|
|
359
|
-
When tests use hardcoded fake paths (not real filesystem paths), always use `path.resolve()` so they include the drive letter on Windows. Functions like `path.resolve()`, `path.dirname()`, and `path.join()` prepend the current drive on Windows — if your constants don't match, lookups and assertions fail.
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
// ❌ WRONG — '/project/docs/guide.md' becomes 'D:\project\docs\guide.md' after path.resolve()
|
|
363
|
-
const PROJECT_ROOT = '/project';
|
|
364
|
-
const GUIDE_PATH = '/project/docs/guide.md';
|
|
365
|
-
|
|
366
|
-
// ✅ CORRECT — path.resolve() makes constants platform-appropriate
|
|
367
|
-
import { resolve } from 'node:path';
|
|
368
|
-
const PROJECT_ROOT = resolve('/project'); // '/project' on Unix, 'D:\project' on Windows
|
|
369
|
-
const GUIDE_PATH = resolve('/project/docs/guide.md');
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### `path.join()` with Two Absolute Paths on Windows
|
|
373
|
-
|
|
374
|
-
On POSIX, `path.join('/a/b', '/c/d')` produces `/a/b/c/d` (valid). On Windows, `path.join('C:\\a', 'C:\\b')` produces `C:\\a\\C:\\b` — the colon from the second drive letter creates an **invalid path** that crashes `fs` operations.
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
// ❌ DANGEROUS — breaks on Windows when both args are absolute
|
|
378
|
-
const targetDir = join(distDir, generatedDir); // C:\dist\C:\gen — invalid!
|
|
379
|
-
|
|
380
|
-
// ✅ SAFE — use relative paths or basename for the second argument
|
|
381
|
-
const targetDir = join(distDir, 'generated');
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
If your function joins two user-supplied paths and both could be absolute, you have a Windows compatibility bug. Either:
|
|
385
|
-
1. Ensure one argument is always relative
|
|
386
|
-
2. Use platform-conditional logic (chdir on Windows, absolute paths on Unix)
|
|
387
|
-
|
|
388
|
-
## When to Extract Helpers
|
|
389
|
-
|
|
390
|
-
### The 2-3 Rule
|
|
391
|
-
|
|
392
|
-
Extract helpers after seeing a pattern **2-3 times**, not 10+:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
// ❌ BAD - Wait until 10+ duplicates accumulate
|
|
396
|
-
describe('Test 1', () => { /* 10 lines of setup */ });
|
|
397
|
-
describe('Test 2', () => { /* 10 lines of setup */ });
|
|
398
|
-
describe('Test 3', () => { /* 10 lines of setup */ });
|
|
399
|
-
// ... 7 more times
|
|
400
|
-
// Finally: "Oh, maybe I should extract this?"
|
|
401
|
-
|
|
402
|
-
// ✅ GOOD - Extract early
|
|
403
|
-
describe('Test 1', () => { /* 10 lines of setup */ });
|
|
404
|
-
describe('Test 2', () => { /* 10 lines of setup */ });
|
|
405
|
-
// "I see a pattern" → Extract setupXTestSuite()
|
|
406
|
-
describe('Test 3', () => {
|
|
407
|
-
beforeEach(suite.beforeEach);
|
|
408
|
-
afterEach(suite.afterEach);
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Questions to Ask
|
|
413
|
-
|
|
414
|
-
While writing tests:
|
|
415
|
-
- ❓ "Have I written similar setup code before?" → Extract factory function
|
|
416
|
-
- ❓ "Am I repeating the same assertions?" → Extract assertion helper
|
|
417
|
-
- ❓ "Is this a common workflow?" → Extract workflow helper
|
|
418
|
-
- ❓ "Do I have 2+ describe blocks with identical beforeEach/afterEach?" → Extract suite helper
|
|
419
|
-
|
|
420
|
-
## Code Duplication Detection
|
|
421
|
-
|
|
422
|
-
### Running the Check
|
|
423
|
-
|
|
424
|
-
```bash
|
|
425
|
-
# Before every commit
|
|
426
|
-
bun run duplication-check
|
|
427
|
-
|
|
428
|
-
# If it fails
|
|
429
|
-
bun run duplication-check # See what's duplicated
|
|
430
|
-
# → Refactor to eliminate duplication
|
|
431
|
-
# → Re-run until it passes
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
### Policy: Zero Tolerance
|
|
435
|
-
|
|
436
|
-
**Code duplication will block your PR.** The CI system runs `duplication-check` and fails if any duplication is detected.
|
|
437
|
-
|
|
438
|
-
**When duplication is detected:**
|
|
439
|
-
1. ❌ **Don't** update the baseline
|
|
440
|
-
2. ❌ **Don't** add eslint-disable comments
|
|
441
|
-
3. ✅ **Do** extract helpers to eliminate duplication
|
|
442
|
-
4. ✅ **Do** refactor until the check passes
|
|
443
|
-
|
|
444
|
-
**The baseline exists to track progress towards zero, not to accept new duplication.**
|
|
445
|
-
|
|
446
|
-
## Time-Dependent Tests
|
|
447
|
-
|
|
448
|
-
Never use real `setTimeout` or `Date.now()` waits in tests. They're flaky on loaded CI machines and waste wall-clock time.
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// ❌ WRONG — flaky on slow CI, wastes 10ms per test
|
|
452
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
453
|
-
expect(cache.isExpired(key)).toBe(true);
|
|
454
|
-
|
|
455
|
-
// ✅ CORRECT — deterministic, instant
|
|
456
|
-
vi.useFakeTimers();
|
|
457
|
-
cache.set(key, value, { ttl: 100 });
|
|
458
|
-
vi.advanceTimersByTime(101);
|
|
459
|
-
expect(cache.isExpired(key)).toBe(true);
|
|
460
|
-
vi.useRealTimers();
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
## Testing Anti-Patterns
|
|
464
|
-
|
|
465
|
-
### ❌ Don't: Copy-paste test setup
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
// BAD - Duplicated in every describe block
|
|
469
|
-
describe('Feature A', () => {
|
|
470
|
-
let tempDir: string;
|
|
471
|
-
let registry: Registry;
|
|
472
|
-
|
|
473
|
-
beforeEach(async () => {
|
|
474
|
-
tempDir = await mkdtemp(join(tmpdir(), 'test-'));
|
|
475
|
-
registry = new Registry();
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
afterEach(async () => {
|
|
479
|
-
await rm(tempDir, { recursive: true });
|
|
480
|
-
});
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
describe('Feature B', () => {
|
|
484
|
-
// ... same 10 lines repeated
|
|
485
|
-
});
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### ✅ Do: Extract suite helper
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
// GOOD - Shared via helper
|
|
492
|
-
const suite = setupTestSuite('my-test-');
|
|
493
|
-
|
|
494
|
-
describe('Feature A', () => {
|
|
495
|
-
beforeEach(suite.beforeEach);
|
|
496
|
-
afterEach(suite.afterEach);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
describe('Feature B', () => {
|
|
500
|
-
beforeEach(suite.beforeEach);
|
|
501
|
-
afterEach(suite.afterEach);
|
|
502
|
-
});
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### ❌ Don't: Inline path operations without normalization
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
// BAD - Fails on Windows
|
|
509
|
-
expect(resource.filePath.includes('/docs/')).toBe(true);
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### ✅ Do: Normalize before comparison
|
|
513
|
-
|
|
514
|
-
```typescript
|
|
515
|
-
// GOOD - Works everywhere
|
|
516
|
-
expect(toForwardSlash(resource.filePath)).toContain('/docs/');
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### ❌ Don't: Write 10 tests before extracting
|
|
520
|
-
|
|
521
|
-
```typescript
|
|
522
|
-
// BAD - Wait until massive duplication accumulates
|
|
523
|
-
it('test 1', () => { /* repeated setup */ });
|
|
524
|
-
// ... 9 more times with identical setup
|
|
525
|
-
// Finally extract helper after SonarQube complains
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### ✅ Do: Extract after 2-3 similar patterns
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
// GOOD - Extract early, prevent accumulation
|
|
532
|
-
it('test 1', () => { /* setup */ });
|
|
533
|
-
it('test 2', () => { /* same setup */ });
|
|
534
|
-
// "I see a pattern" → Extract helper now
|
|
535
|
-
it('test 3', () => { /* uses helper */ });
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
## Summary Checklist
|
|
539
|
-
|
|
540
|
-
When writing tests:
|
|
541
|
-
|
|
542
|
-
**Classification:**
|
|
543
|
-
- [ ] Test is in the right tier (no network/ML/process spawning in unit tests)
|
|
544
|
-
- [ ] Network-dependent integration tests use `describe.skipIf(!!process.env.CI)` if needed
|
|
545
|
-
|
|
546
|
-
**Cross-platform:**
|
|
547
|
-
- [ ] Used `toForwardSlash()` from utils for path comparisons
|
|
548
|
-
- [ ] Used `path.resolve()` for hardcoded fake path constants
|
|
549
|
-
- [ ] No `path.join()` with two absolute paths (breaks on Windows)
|
|
550
|
-
- [ ] No `process.chdir()` in unit tests (use `vi.spyOn(process, 'cwd')`)
|
|
551
|
-
- [ ] All tests pass on both Windows and Unix systems
|
|
552
|
-
|
|
553
|
-
**Patterns:**
|
|
554
|
-
- [ ] Created `test/test-helpers.ts` for the package
|
|
555
|
-
- [ ] Extracted `setupXTestSuite()` helper after 2-3 similar describe blocks
|
|
556
|
-
- [ ] Created factory functions for common test entities
|
|
557
|
-
- [ ] Extracted assertion helpers for repeated validation patterns
|
|
558
|
-
- [ ] Used `vi.useFakeTimers()` instead of real `setTimeout` for time-dependent tests
|
|
559
|
-
|
|
560
|
-
**Quality:**
|
|
561
|
-
- [ ] Ran `bun run duplication-check` before committing
|
|
562
|
-
- [ ] No code duplication detected
|
|
563
|
-
|
|
564
|
-
## Real-World Impact
|
|
565
|
-
|
|
566
|
-
**Before applying these patterns:**
|
|
567
|
-
- resources package: 25.6% duplication in tests
|
|
568
|
-
- RAG system tests: 41-62% duplication per file
|
|
569
|
-
- Frequent Windows CI failures from path issues
|
|
570
|
-
|
|
571
|
-
**After applying these patterns:**
|
|
572
|
-
- resources package: 0% duplication
|
|
573
|
-
- RAG system tests: 0% duplication
|
|
574
|
-
- All tests pass on Windows and Ubuntu
|
|
575
|
-
- Test files 30-50% shorter and more maintainable
|
|
576
|
-
|
|
577
|
-
**The key**: Extract helpers EARLY (after 2-3 patterns), not LATE (after 10+ duplicates).
|