agentic-team-templates 0.4.2 → 0.6.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 +69 -5
- package/bin/cli.js +4 -1
- package/package.json +8 -3
- package/src/index.js +776 -134
- package/src/index.test.js +947 -0
- package/templates/testing/.cursorrules/advanced-techniques.md +596 -0
- package/templates/testing/.cursorrules/ci-cd-integration.md +603 -0
- package/templates/testing/.cursorrules/overview.md +163 -0
- package/templates/testing/.cursorrules/performance-testing.md +536 -0
- package/templates/testing/.cursorrules/quality-metrics.md +456 -0
- package/templates/testing/.cursorrules/reliability.md +557 -0
- package/templates/testing/.cursorrules/tdd-methodology.md +294 -0
- package/templates/testing/.cursorrules/test-data.md +565 -0
- package/templates/testing/.cursorrules/test-design.md +511 -0
- package/templates/testing/.cursorrules/test-types.md +398 -0
- package/templates/testing/CLAUDE.md +1134 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { run, _internals } from './index.js';
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
TEMPLATES,
|
|
9
|
+
SHARED_RULES,
|
|
10
|
+
SUPPORTED_IDES,
|
|
11
|
+
DEFAULT_IDES,
|
|
12
|
+
filesMatch,
|
|
13
|
+
parseMarkdownSections,
|
|
14
|
+
generateSectionSignature,
|
|
15
|
+
findMissingSections,
|
|
16
|
+
mergeClaudeContent,
|
|
17
|
+
getAlternateFilename,
|
|
18
|
+
copyFile,
|
|
19
|
+
generateClaudeMdContent,
|
|
20
|
+
generateCopilotInstructionsContent,
|
|
21
|
+
isOurFile,
|
|
22
|
+
install,
|
|
23
|
+
remove,
|
|
24
|
+
reset,
|
|
25
|
+
} = _internals;
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Constants & Configuration Tests
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
describe('Constants', () => {
|
|
32
|
+
describe('TEMPLATES', () => {
|
|
33
|
+
it('should have all expected templates', () => {
|
|
34
|
+
const expectedTemplates = [
|
|
35
|
+
'blockchain',
|
|
36
|
+
'cli-tools',
|
|
37
|
+
'documentation',
|
|
38
|
+
'fullstack',
|
|
39
|
+
'mobile',
|
|
40
|
+
'utility-agent',
|
|
41
|
+
'web-backend',
|
|
42
|
+
'web-frontend',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
expect(Object.keys(TEMPLATES).sort()).toEqual(expectedTemplates.sort());
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('each template should have description and rules array', () => {
|
|
49
|
+
for (const [name, template] of Object.entries(TEMPLATES)) {
|
|
50
|
+
expect(template).toHaveProperty('description');
|
|
51
|
+
expect(typeof template.description).toBe('string');
|
|
52
|
+
expect(template.description.length).toBeGreaterThan(0);
|
|
53
|
+
|
|
54
|
+
expect(template).toHaveProperty('rules');
|
|
55
|
+
expect(Array.isArray(template.rules)).toBe(true);
|
|
56
|
+
expect(template.rules.length).toBeGreaterThan(0);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('each template should have overview.md in rules', () => {
|
|
61
|
+
for (const [name, template] of Object.entries(TEMPLATES)) {
|
|
62
|
+
expect(template.rules).toContain('overview.md');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('SHARED_RULES', () => {
|
|
68
|
+
it('should have expected shared rules', () => {
|
|
69
|
+
expect(SHARED_RULES).toContain('code-quality.md');
|
|
70
|
+
expect(SHARED_RULES).toContain('communication.md');
|
|
71
|
+
expect(SHARED_RULES).toContain('core-principles.md');
|
|
72
|
+
expect(SHARED_RULES).toContain('git-workflow.md');
|
|
73
|
+
expect(SHARED_RULES).toContain('security-fundamentals.md');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('all rules should end with .md', () => {
|
|
77
|
+
for (const rule of SHARED_RULES) {
|
|
78
|
+
expect(rule).toMatch(/\.md$/);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('SUPPORTED_IDES', () => {
|
|
84
|
+
it('should contain cursor, claude, and codex', () => {
|
|
85
|
+
expect(SUPPORTED_IDES).toContain('cursor');
|
|
86
|
+
expect(SUPPORTED_IDES).toContain('claude');
|
|
87
|
+
expect(SUPPORTED_IDES).toContain('codex');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('DEFAULT_IDES', () => {
|
|
92
|
+
it('should default to all supported IDEs', () => {
|
|
93
|
+
expect(DEFAULT_IDES).toEqual(SUPPORTED_IDES);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Utility Functions Tests
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
describe('Utility Functions', () => {
|
|
103
|
+
describe('getAlternateFilename', () => {
|
|
104
|
+
it('should add -1 suffix before extension', () => {
|
|
105
|
+
expect(getAlternateFilename('/path/to/file.md')).toBe('/path/to/file-1.md');
|
|
106
|
+
expect(getAlternateFilename('/path/to/code-quality.md')).toBe('/path/to/code-quality-1.md');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle files without directory', () => {
|
|
110
|
+
expect(getAlternateFilename('file.md')).toBe('file-1.md');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle different extensions', () => {
|
|
114
|
+
expect(getAlternateFilename('/path/to/file.txt')).toBe('/path/to/file-1.txt');
|
|
115
|
+
expect(getAlternateFilename('/path/to/file.json')).toBe('/path/to/file-1.json');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('filesMatch', () => {
|
|
120
|
+
let tempDir;
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-templates-test-'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return true for identical files', () => {
|
|
131
|
+
const file1 = path.join(tempDir, 'file1.md');
|
|
132
|
+
const file2 = path.join(tempDir, 'file2.md');
|
|
133
|
+
const content = '# Test Content\n\nSome text here.';
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(file1, content);
|
|
136
|
+
fs.writeFileSync(file2, content);
|
|
137
|
+
|
|
138
|
+
expect(filesMatch(file1, file2)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return false for different files', () => {
|
|
142
|
+
const file1 = path.join(tempDir, 'file1.md');
|
|
143
|
+
const file2 = path.join(tempDir, 'file2.md');
|
|
144
|
+
|
|
145
|
+
fs.writeFileSync(file1, '# Content A');
|
|
146
|
+
fs.writeFileSync(file2, '# Content B');
|
|
147
|
+
|
|
148
|
+
expect(filesMatch(file1, file2)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return false if file does not exist', () => {
|
|
152
|
+
const file1 = path.join(tempDir, 'exists.md');
|
|
153
|
+
const file2 = path.join(tempDir, 'not-exists.md');
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(file1, '# Content');
|
|
156
|
+
|
|
157
|
+
expect(filesMatch(file1, file2)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('isOurFile', () => {
|
|
162
|
+
let tempDir;
|
|
163
|
+
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-templates-test-'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return false if file does not exist', () => {
|
|
173
|
+
const filePath = path.join(tempDir, 'nonexistent.md');
|
|
174
|
+
const templatePath = path.join(tempDir, 'template.md');
|
|
175
|
+
|
|
176
|
+
expect(isOurFile(filePath, templatePath)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return true if template does not exist (assume ours)', () => {
|
|
180
|
+
const filePath = path.join(tempDir, 'file.md');
|
|
181
|
+
const templatePath = path.join(tempDir, 'nonexistent-template.md');
|
|
182
|
+
|
|
183
|
+
fs.writeFileSync(filePath, '# Content');
|
|
184
|
+
|
|
185
|
+
expect(isOurFile(filePath, templatePath)).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return true if files match', () => {
|
|
189
|
+
const filePath = path.join(tempDir, 'file.md');
|
|
190
|
+
const templatePath = path.join(tempDir, 'template.md');
|
|
191
|
+
const content = '# Same Content';
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(filePath, content);
|
|
194
|
+
fs.writeFileSync(templatePath, content);
|
|
195
|
+
|
|
196
|
+
expect(isOurFile(filePath, templatePath)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return false if files differ', () => {
|
|
200
|
+
const filePath = path.join(tempDir, 'file.md');
|
|
201
|
+
const templatePath = path.join(tempDir, 'template.md');
|
|
202
|
+
|
|
203
|
+
fs.writeFileSync(filePath, '# Modified Content');
|
|
204
|
+
fs.writeFileSync(templatePath, '# Original Template');
|
|
205
|
+
|
|
206
|
+
expect(isOurFile(filePath, templatePath)).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Markdown Parsing Tests
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
describe('Markdown Parsing', () => {
|
|
216
|
+
describe('parseMarkdownSections', () => {
|
|
217
|
+
it('should parse sections with ## headings', () => {
|
|
218
|
+
const content = `# Title
|
|
219
|
+
|
|
220
|
+
Some preamble text.
|
|
221
|
+
|
|
222
|
+
## Section One
|
|
223
|
+
|
|
224
|
+
Content for section one.
|
|
225
|
+
|
|
226
|
+
## Section Two
|
|
227
|
+
|
|
228
|
+
Content for section two.
|
|
229
|
+
`;
|
|
230
|
+
const result = parseMarkdownSections(content);
|
|
231
|
+
|
|
232
|
+
expect(result.preamble).toContain('# Title');
|
|
233
|
+
expect(result.preamble).toContain('Some preamble text.');
|
|
234
|
+
expect(result.sections).toHaveLength(2);
|
|
235
|
+
expect(result.sections[0].heading).toBe('Section One');
|
|
236
|
+
expect(result.sections[1].heading).toBe('Section Two');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle content with no sections', () => {
|
|
240
|
+
const content = '# Just a title\n\nSome content without sections.';
|
|
241
|
+
const result = parseMarkdownSections(content);
|
|
242
|
+
|
|
243
|
+
expect(result.sections).toHaveLength(0);
|
|
244
|
+
expect(result.preamble).toContain('# Just a title');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should preserve section content', () => {
|
|
248
|
+
const content = `## My Section
|
|
249
|
+
|
|
250
|
+
Line 1
|
|
251
|
+
Line 2
|
|
252
|
+
Line 3
|
|
253
|
+
`;
|
|
254
|
+
const result = parseMarkdownSections(content);
|
|
255
|
+
|
|
256
|
+
expect(result.sections[0].content).toContain('Line 1');
|
|
257
|
+
expect(result.sections[0].content).toContain('Line 2');
|
|
258
|
+
expect(result.sections[0].content).toContain('Line 3');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should generate signatures for sections', () => {
|
|
262
|
+
const content = `## Test Section
|
|
263
|
+
|
|
264
|
+
Some meaningful content here.
|
|
265
|
+
`;
|
|
266
|
+
const result = parseMarkdownSections(content);
|
|
267
|
+
|
|
268
|
+
expect(result.sections[0]).toHaveProperty('signature');
|
|
269
|
+
expect(typeof result.sections[0].signature).toBe('string');
|
|
270
|
+
expect(result.sections[0].signature.length).toBeGreaterThan(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('generateSectionSignature', () => {
|
|
275
|
+
it('should normalize heading to lowercase', () => {
|
|
276
|
+
const sig1 = generateSectionSignature('My Heading', ['content']);
|
|
277
|
+
const sig2 = generateSectionSignature('my heading', ['content']);
|
|
278
|
+
|
|
279
|
+
expect(sig1).toBe(sig2);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should remove special characters from heading', () => {
|
|
283
|
+
const sig1 = generateSectionSignature('Heading: With (Special) Chars!', ['content']);
|
|
284
|
+
const sig2 = generateSectionSignature('Heading With Special Chars', ['content']);
|
|
285
|
+
|
|
286
|
+
expect(sig1).toBe(sig2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should include content in signature', () => {
|
|
290
|
+
const sig1 = generateSectionSignature('Heading', ['Line A']);
|
|
291
|
+
const sig2 = generateSectionSignature('Heading', ['Line B']);
|
|
292
|
+
|
|
293
|
+
expect(sig1).not.toBe(sig2);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should filter out empty lines and special lines', () => {
|
|
297
|
+
const sig = generateSectionSignature('Heading', [
|
|
298
|
+
'',
|
|
299
|
+
'# Subheading',
|
|
300
|
+
'| table |',
|
|
301
|
+
'- list item',
|
|
302
|
+
'Meaningful content',
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
expect(sig).toContain('meaningful content');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('findMissingSections', () => {
|
|
310
|
+
it('should find sections in template that are missing from existing', () => {
|
|
311
|
+
const existing = `## Section A
|
|
312
|
+
|
|
313
|
+
Content A
|
|
314
|
+
|
|
315
|
+
## Section B
|
|
316
|
+
|
|
317
|
+
Content B
|
|
318
|
+
`;
|
|
319
|
+
const template = `## Section A
|
|
320
|
+
|
|
321
|
+
Content A
|
|
322
|
+
|
|
323
|
+
## Section B
|
|
324
|
+
|
|
325
|
+
Content B
|
|
326
|
+
|
|
327
|
+
## Section C
|
|
328
|
+
|
|
329
|
+
Content C
|
|
330
|
+
`;
|
|
331
|
+
const result = findMissingSections(existing, template);
|
|
332
|
+
|
|
333
|
+
expect(result.missing).toHaveLength(1);
|
|
334
|
+
expect(result.missing[0].heading).toBe('Section C');
|
|
335
|
+
expect(result.matchedCount).toBe(2);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should return empty array when all sections present', () => {
|
|
339
|
+
const content = `## Section A
|
|
340
|
+
|
|
341
|
+
Content
|
|
342
|
+
|
|
343
|
+
## Section B
|
|
344
|
+
|
|
345
|
+
Content
|
|
346
|
+
`;
|
|
347
|
+
const result = findMissingSections(content, content);
|
|
348
|
+
|
|
349
|
+
expect(result.missing).toHaveLength(0);
|
|
350
|
+
expect(result.matchedCount).toBe(2);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should match by heading regardless of case', () => {
|
|
354
|
+
const existing = '## SECTION A\n\nContent';
|
|
355
|
+
const template = '## Section A\n\nContent';
|
|
356
|
+
|
|
357
|
+
const result = findMissingSections(existing, template);
|
|
358
|
+
|
|
359
|
+
expect(result.missing).toHaveLength(0);
|
|
360
|
+
expect(result.matchedCount).toBe(1);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('mergeClaudeContent', () => {
|
|
365
|
+
it('should merge missing sections into existing content', () => {
|
|
366
|
+
const existing = `# Title
|
|
367
|
+
|
|
368
|
+
## Section A
|
|
369
|
+
|
|
370
|
+
Content A
|
|
371
|
+
`;
|
|
372
|
+
const template = `# Title
|
|
373
|
+
|
|
374
|
+
## Section A
|
|
375
|
+
|
|
376
|
+
Content A
|
|
377
|
+
|
|
378
|
+
## Section B
|
|
379
|
+
|
|
380
|
+
Content B
|
|
381
|
+
`;
|
|
382
|
+
const result = mergeClaudeContent(existing, template);
|
|
383
|
+
|
|
384
|
+
expect(result.merged).toContain('## Section A');
|
|
385
|
+
expect(result.merged).toContain('## Section B');
|
|
386
|
+
expect(result.addedSections).toContain('Section B');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should return unchanged content when nothing to merge', () => {
|
|
390
|
+
const content = `## Section A
|
|
391
|
+
|
|
392
|
+
Content
|
|
393
|
+
`;
|
|
394
|
+
const result = mergeClaudeContent(content, content);
|
|
395
|
+
|
|
396
|
+
expect(result.merged).toBe(content);
|
|
397
|
+
expect(result.addedSections).toHaveLength(0);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should preserve preamble', () => {
|
|
401
|
+
const existing = `# My Title
|
|
402
|
+
|
|
403
|
+
Introduction paragraph.
|
|
404
|
+
|
|
405
|
+
## Existing Section
|
|
406
|
+
|
|
407
|
+
Content
|
|
408
|
+
`;
|
|
409
|
+
const template = `## Existing Section
|
|
410
|
+
|
|
411
|
+
Content
|
|
412
|
+
|
|
413
|
+
## New Section
|
|
414
|
+
|
|
415
|
+
New content
|
|
416
|
+
`;
|
|
417
|
+
const result = mergeClaudeContent(existing, template);
|
|
418
|
+
|
|
419
|
+
expect(result.merged).toContain('# My Title');
|
|
420
|
+
expect(result.merged).toContain('Introduction paragraph');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// ============================================================================
|
|
426
|
+
// Content Generation Tests
|
|
427
|
+
// ============================================================================
|
|
428
|
+
|
|
429
|
+
describe('Content Generation', () => {
|
|
430
|
+
describe('generateClaudeMdContent', () => {
|
|
431
|
+
it('should generate valid markdown', () => {
|
|
432
|
+
const content = generateClaudeMdContent(['web-frontend']);
|
|
433
|
+
|
|
434
|
+
expect(content).toContain('# CLAUDE.md - Development Guide');
|
|
435
|
+
expect(content).toContain('web-frontend');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should include template description', () => {
|
|
439
|
+
const content = generateClaudeMdContent(['web-frontend']);
|
|
440
|
+
|
|
441
|
+
expect(content).toContain(TEMPLATES['web-frontend'].description);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should include multiple templates', () => {
|
|
445
|
+
const content = generateClaudeMdContent(['web-frontend', 'web-backend']);
|
|
446
|
+
|
|
447
|
+
expect(content).toContain('web-frontend');
|
|
448
|
+
expect(content).toContain('web-backend');
|
|
449
|
+
expect(content).toContain(TEMPLATES['web-frontend'].description);
|
|
450
|
+
expect(content).toContain(TEMPLATES['web-backend'].description);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should include shared rules table', () => {
|
|
454
|
+
const content = generateClaudeMdContent(['web-frontend']);
|
|
455
|
+
|
|
456
|
+
expect(content).toContain('core-principles.md');
|
|
457
|
+
expect(content).toContain('code-quality.md');
|
|
458
|
+
expect(content).toContain('security-fundamentals.md');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should include development principles', () => {
|
|
462
|
+
const content = generateClaudeMdContent(['web-frontend']);
|
|
463
|
+
|
|
464
|
+
expect(content).toContain('Honesty Over Output');
|
|
465
|
+
expect(content).toContain('Security First');
|
|
466
|
+
expect(content).toContain('Tests Are Required');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should include definition of done', () => {
|
|
470
|
+
const content = generateClaudeMdContent(['web-frontend']);
|
|
471
|
+
|
|
472
|
+
expect(content).toContain('Definition of Done');
|
|
473
|
+
expect(content).toContain('Code written and reviewed');
|
|
474
|
+
expect(content).toContain('Tests written and passing');
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('generateCopilotInstructionsContent', () => {
|
|
479
|
+
it('should generate valid markdown', () => {
|
|
480
|
+
const content = generateCopilotInstructionsContent(['web-frontend']);
|
|
481
|
+
|
|
482
|
+
expect(content).toContain('# Copilot Instructions');
|
|
483
|
+
expect(content).toContain('web-frontend');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should include installed templates list', () => {
|
|
487
|
+
const content = generateCopilotInstructionsContent(['web-frontend', 'web-backend']);
|
|
488
|
+
|
|
489
|
+
expect(content).toContain('**Installed Templates:** web-frontend, web-backend');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should include core principles', () => {
|
|
493
|
+
const content = generateCopilotInstructionsContent(['web-frontend']);
|
|
494
|
+
|
|
495
|
+
expect(content).toContain('Honesty Over Output');
|
|
496
|
+
expect(content).toContain('Security First');
|
|
497
|
+
expect(content).toContain('Tests Are Required');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// File Operations Tests
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
describe('File Operations', () => {
|
|
507
|
+
let tempDir;
|
|
508
|
+
|
|
509
|
+
beforeEach(() => {
|
|
510
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-templates-test-'));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
afterEach(() => {
|
|
514
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('copyFile', () => {
|
|
518
|
+
it('should copy file to new location', () => {
|
|
519
|
+
const src = path.join(tempDir, 'source.md');
|
|
520
|
+
const dest = path.join(tempDir, 'dest.md');
|
|
521
|
+
const content = '# Test Content';
|
|
522
|
+
|
|
523
|
+
fs.writeFileSync(src, content);
|
|
524
|
+
|
|
525
|
+
const result = copyFile(src, dest);
|
|
526
|
+
|
|
527
|
+
expect(result.status).toBe('copied');
|
|
528
|
+
expect(result.destFile).toBe(dest);
|
|
529
|
+
expect(fs.readFileSync(dest, 'utf8')).toBe(content);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should create destination directory if needed', () => {
|
|
533
|
+
const src = path.join(tempDir, 'source.md');
|
|
534
|
+
const dest = path.join(tempDir, 'subdir', 'dest.md');
|
|
535
|
+
|
|
536
|
+
fs.writeFileSync(src, '# Content');
|
|
537
|
+
|
|
538
|
+
const result = copyFile(src, dest);
|
|
539
|
+
|
|
540
|
+
expect(result.status).toBe('copied');
|
|
541
|
+
expect(fs.existsSync(dest)).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should skip identical files', () => {
|
|
545
|
+
const src = path.join(tempDir, 'source.md');
|
|
546
|
+
const dest = path.join(tempDir, 'dest.md');
|
|
547
|
+
const content = '# Same Content';
|
|
548
|
+
|
|
549
|
+
fs.writeFileSync(src, content);
|
|
550
|
+
fs.writeFileSync(dest, content);
|
|
551
|
+
|
|
552
|
+
const result = copyFile(src, dest);
|
|
553
|
+
|
|
554
|
+
expect(result.status).toBe('skipped');
|
|
555
|
+
expect(result.destFile).toBe(dest);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should rename to -1 when destination differs', () => {
|
|
559
|
+
const src = path.join(tempDir, 'source.md');
|
|
560
|
+
const dest = path.join(tempDir, 'dest.md');
|
|
561
|
+
|
|
562
|
+
fs.writeFileSync(src, '# New Content');
|
|
563
|
+
fs.writeFileSync(dest, '# Existing Different Content');
|
|
564
|
+
|
|
565
|
+
const result = copyFile(src, dest, false);
|
|
566
|
+
|
|
567
|
+
expect(result.status).toBe('renamed');
|
|
568
|
+
expect(result.destFile).toBe(path.join(tempDir, 'dest-1.md'));
|
|
569
|
+
expect(fs.existsSync(path.join(tempDir, 'dest-1.md'))).toBe(true);
|
|
570
|
+
// Original should be preserved
|
|
571
|
+
expect(fs.readFileSync(dest, 'utf8')).toBe('# Existing Different Content');
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should overwrite with force flag', () => {
|
|
575
|
+
const src = path.join(tempDir, 'source.md');
|
|
576
|
+
const dest = path.join(tempDir, 'dest.md');
|
|
577
|
+
|
|
578
|
+
fs.writeFileSync(src, '# New Content');
|
|
579
|
+
fs.writeFileSync(dest, '# Old Content');
|
|
580
|
+
|
|
581
|
+
const result = copyFile(src, dest, true);
|
|
582
|
+
|
|
583
|
+
expect(result.status).toBe('updated');
|
|
584
|
+
expect(fs.readFileSync(dest, 'utf8')).toBe('# New Content');
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// ============================================================================
|
|
590
|
+
// Install/Remove/Reset Integration Tests
|
|
591
|
+
// ============================================================================
|
|
592
|
+
|
|
593
|
+
describe('Install/Remove/Reset Operations', () => {
|
|
594
|
+
let tempDir;
|
|
595
|
+
let originalCwd;
|
|
596
|
+
let consoleLogSpy;
|
|
597
|
+
|
|
598
|
+
beforeEach(() => {
|
|
599
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-templates-test-'));
|
|
600
|
+
originalCwd = process.cwd();
|
|
601
|
+
// Suppress console output during tests
|
|
602
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
603
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
afterEach(() => {
|
|
607
|
+
process.chdir(originalCwd);
|
|
608
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
609
|
+
vi.restoreAllMocks();
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('install', () => {
|
|
613
|
+
it('should create .cursorrules directory', () => {
|
|
614
|
+
install(tempDir, ['web-frontend'], false, false, ['cursor']);
|
|
615
|
+
|
|
616
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(true);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('should install shared rules', () => {
|
|
620
|
+
install(tempDir, ['web-frontend'], false, false, ['cursor']);
|
|
621
|
+
|
|
622
|
+
for (const rule of SHARED_RULES) {
|
|
623
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', rule))).toBe(true);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should install template-specific rules with prefix', () => {
|
|
628
|
+
install(tempDir, ['web-frontend'], false, false, ['cursor']);
|
|
629
|
+
|
|
630
|
+
for (const rule of TEMPLATES['web-frontend'].rules) {
|
|
631
|
+
const prefixedName = `web-frontend-${rule}`;
|
|
632
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', prefixedName))).toBe(true);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should create CLAUDE.md for claude IDE', () => {
|
|
637
|
+
install(tempDir, ['web-frontend'], false, false, ['claude']);
|
|
638
|
+
|
|
639
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
640
|
+
const content = fs.readFileSync(path.join(tempDir, 'CLAUDE.md'), 'utf8');
|
|
641
|
+
expect(content).toContain('# CLAUDE.md - Development Guide');
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('should create copilot-instructions.md for codex IDE', () => {
|
|
645
|
+
install(tempDir, ['web-frontend'], false, false, ['codex']);
|
|
646
|
+
|
|
647
|
+
expect(fs.existsSync(path.join(tempDir, '.github', 'copilot-instructions.md'))).toBe(true);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should install for all IDEs by default', () => {
|
|
651
|
+
install(tempDir, ['web-frontend'], false, false, DEFAULT_IDES);
|
|
652
|
+
|
|
653
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(true);
|
|
654
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
655
|
+
expect(fs.existsSync(path.join(tempDir, '.github', 'copilot-instructions.md'))).toBe(true);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should not write files in dry-run mode', () => {
|
|
659
|
+
install(tempDir, ['web-frontend'], true, false, ['cursor']);
|
|
660
|
+
|
|
661
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(false);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should install multiple templates', () => {
|
|
665
|
+
install(tempDir, ['web-frontend', 'web-backend'], false, false, ['cursor']);
|
|
666
|
+
|
|
667
|
+
// Check web-frontend rules
|
|
668
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'web-frontend-overview.md'))).toBe(true);
|
|
669
|
+
// Check web-backend rules
|
|
670
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'web-backend-overview.md'))).toBe(true);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
describe('remove', () => {
|
|
675
|
+
beforeEach(() => {
|
|
676
|
+
// First install a template
|
|
677
|
+
install(tempDir, ['web-frontend'], false, false, ['cursor']);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should remove template-specific files', async () => {
|
|
681
|
+
await remove(tempDir, ['web-frontend'], false, false, true, ['cursor']);
|
|
682
|
+
|
|
683
|
+
for (const rule of TEMPLATES['web-frontend'].rules) {
|
|
684
|
+
const prefixedName = `web-frontend-${rule}`;
|
|
685
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', prefixedName))).toBe(false);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should keep shared rules when removing template', async () => {
|
|
690
|
+
await remove(tempDir, ['web-frontend'], false, false, true, ['cursor']);
|
|
691
|
+
|
|
692
|
+
for (const rule of SHARED_RULES) {
|
|
693
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', rule))).toBe(true);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should not remove files in dry-run mode', async () => {
|
|
698
|
+
await remove(tempDir, ['web-frontend'], true, false, true, ['cursor']);
|
|
699
|
+
|
|
700
|
+
// Files should still exist
|
|
701
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'web-frontend-overview.md'))).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should skip modified files without force', async () => {
|
|
705
|
+
// Modify a file
|
|
706
|
+
const filePath = path.join(tempDir, '.cursorrules', 'web-frontend-overview.md');
|
|
707
|
+
fs.writeFileSync(filePath, '# Modified content');
|
|
708
|
+
|
|
709
|
+
await remove(tempDir, ['web-frontend'], false, false, true, ['cursor']);
|
|
710
|
+
|
|
711
|
+
// Modified file should still exist
|
|
712
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
713
|
+
expect(fs.readFileSync(filePath, 'utf8')).toBe('# Modified content');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('should remove modified files with force', async () => {
|
|
717
|
+
// Modify a file
|
|
718
|
+
const filePath = path.join(tempDir, '.cursorrules', 'web-frontend-overview.md');
|
|
719
|
+
fs.writeFileSync(filePath, '# Modified content');
|
|
720
|
+
|
|
721
|
+
await remove(tempDir, ['web-frontend'], false, true, true, ['cursor']);
|
|
722
|
+
|
|
723
|
+
// Modified file should be removed
|
|
724
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
describe('reset', () => {
|
|
729
|
+
beforeEach(() => {
|
|
730
|
+
// Install templates
|
|
731
|
+
install(tempDir, ['web-frontend', 'web-backend'], false, false, DEFAULT_IDES);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('should remove all template files from .cursorrules', async () => {
|
|
735
|
+
await reset(tempDir, false, false, true, ['cursor']);
|
|
736
|
+
|
|
737
|
+
// Template files should be removed
|
|
738
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'web-frontend-overview.md'))).toBe(false);
|
|
739
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'web-backend-overview.md'))).toBe(false);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should remove shared rules', async () => {
|
|
743
|
+
await reset(tempDir, false, false, true, ['cursor']);
|
|
744
|
+
|
|
745
|
+
for (const rule of SHARED_RULES) {
|
|
746
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', rule))).toBe(false);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('should remove CLAUDE.md', async () => {
|
|
751
|
+
await reset(tempDir, false, false, true, ['claude']);
|
|
752
|
+
|
|
753
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(false);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('should remove copilot-instructions.md', async () => {
|
|
757
|
+
await reset(tempDir, false, false, true, ['codex']);
|
|
758
|
+
|
|
759
|
+
expect(fs.existsSync(path.join(tempDir, '.github', 'copilot-instructions.md'))).toBe(false);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should not remove files in dry-run mode', async () => {
|
|
763
|
+
await reset(tempDir, true, false, true, DEFAULT_IDES);
|
|
764
|
+
|
|
765
|
+
// All files should still exist
|
|
766
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(true);
|
|
767
|
+
expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
it('should remove empty .cursorrules directory', async () => {
|
|
771
|
+
await reset(tempDir, false, false, true, ['cursor']);
|
|
772
|
+
|
|
773
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(false);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('should keep .cursorrules if non-template files remain', async () => {
|
|
777
|
+
// Add a custom file
|
|
778
|
+
fs.writeFileSync(path.join(tempDir, '.cursorrules', 'my-custom-rules.md'), '# Custom');
|
|
779
|
+
|
|
780
|
+
await reset(tempDir, false, false, true, ['cursor']);
|
|
781
|
+
|
|
782
|
+
// Directory should still exist with custom file
|
|
783
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules'))).toBe(true);
|
|
784
|
+
expect(fs.existsSync(path.join(tempDir, '.cursorrules', 'my-custom-rules.md'))).toBe(true);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// ============================================================================
|
|
790
|
+
// CLI Argument Parsing Tests
|
|
791
|
+
// ============================================================================
|
|
792
|
+
|
|
793
|
+
describe('CLI Argument Parsing', () => {
|
|
794
|
+
let originalCwd;
|
|
795
|
+
let tempDir;
|
|
796
|
+
let exitSpy;
|
|
797
|
+
let consoleLogSpy;
|
|
798
|
+
let consoleErrorSpy;
|
|
799
|
+
|
|
800
|
+
beforeEach(() => {
|
|
801
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cursor-templates-test-'));
|
|
802
|
+
originalCwd = process.cwd();
|
|
803
|
+
process.chdir(tempDir);
|
|
804
|
+
|
|
805
|
+
// Mock process.exit to prevent test from exiting
|
|
806
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
807
|
+
throw new Error('process.exit called');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
811
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
afterEach(() => {
|
|
815
|
+
process.chdir(originalCwd);
|
|
816
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
817
|
+
vi.restoreAllMocks();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should show help with --help', async () => {
|
|
821
|
+
await expect(run(['--help'])).rejects.toThrow('process.exit');
|
|
822
|
+
|
|
823
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
824
|
+
expect(consoleLogSpy).toHaveBeenCalled();
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('should show help with -h', async () => {
|
|
828
|
+
await expect(run(['-h'])).rejects.toThrow('process.exit');
|
|
829
|
+
|
|
830
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should list templates with --list', async () => {
|
|
834
|
+
await expect(run(['--list'])).rejects.toThrow('process.exit');
|
|
835
|
+
|
|
836
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should list templates with -l', async () => {
|
|
840
|
+
await expect(run(['-l'])).rejects.toThrow('process.exit');
|
|
841
|
+
|
|
842
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should error on unknown option', async () => {
|
|
846
|
+
await expect(run(['--unknown-option'])).rejects.toThrow('process.exit');
|
|
847
|
+
|
|
848
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
849
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('should error when no templates specified', async () => {
|
|
853
|
+
await expect(run([])).rejects.toThrow('process.exit');
|
|
854
|
+
|
|
855
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('should error on unknown template', async () => {
|
|
859
|
+
await expect(run(['nonexistent-template'])).rejects.toThrow('process.exit');
|
|
860
|
+
|
|
861
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('should error on unknown IDE', async () => {
|
|
865
|
+
await expect(run(['web-frontend', '--ide=unknown'])).rejects.toThrow('process.exit');
|
|
866
|
+
|
|
867
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should accept valid template', async () => {
|
|
871
|
+
await run(['web-frontend', '--dry-run']);
|
|
872
|
+
|
|
873
|
+
// Should not exit with error
|
|
874
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('should accept multiple templates', async () => {
|
|
878
|
+
await run(['web-frontend', 'web-backend', '--dry-run']);
|
|
879
|
+
|
|
880
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('should accept valid IDE option', async () => {
|
|
884
|
+
await run(['web-frontend', '--ide=cursor', '--dry-run']);
|
|
885
|
+
|
|
886
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should accept multiple IDE options', async () => {
|
|
890
|
+
await run(['web-frontend', '--ide=cursor', '--ide=claude', '--dry-run']);
|
|
891
|
+
|
|
892
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should error when using --remove and --reset together', async () => {
|
|
896
|
+
await expect(run(['--remove', '--reset'])).rejects.toThrow('process.exit');
|
|
897
|
+
|
|
898
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('should error when --reset has template arguments', async () => {
|
|
902
|
+
await expect(run(['--reset', 'web-frontend'])).rejects.toThrow('process.exit');
|
|
903
|
+
|
|
904
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should error when --remove has no templates', async () => {
|
|
908
|
+
await expect(run(['--remove'])).rejects.toThrow('process.exit');
|
|
909
|
+
|
|
910
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should accept --remove with valid template', async () => {
|
|
914
|
+
// First install, then remove
|
|
915
|
+
await run(['web-frontend']);
|
|
916
|
+
await run(['--remove', 'web-frontend', '--yes']);
|
|
917
|
+
|
|
918
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('should accept --reset with --yes', async () => {
|
|
922
|
+
// First install, then reset
|
|
923
|
+
await run(['web-frontend']);
|
|
924
|
+
await run(['--reset', '--yes']);
|
|
925
|
+
|
|
926
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('should accept --force flag', async () => {
|
|
930
|
+
await run(['web-frontend', '--force', '--dry-run']);
|
|
931
|
+
|
|
932
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('should accept -f shorthand for force', async () => {
|
|
936
|
+
await run(['web-frontend', '-f', '--dry-run']);
|
|
937
|
+
|
|
938
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('should accept -y shorthand for yes', async () => {
|
|
942
|
+
await run(['web-frontend']);
|
|
943
|
+
await run(['--reset', '-y']);
|
|
944
|
+
|
|
945
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
946
|
+
});
|
|
947
|
+
});
|