@codebakers/cli 3.0.0 → 3.1.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/dist/commands/doctor.js +10 -3
- package/dist/commands/go.d.ts +5 -1
- package/dist/commands/go.js +189 -8
- package/dist/index.js +3 -2
- package/dist/mcp/server.js +1270 -0
- package/package.json +8 -3
- package/src/commands/doctor.ts +9 -3
- package/src/commands/go.ts +231 -9
- package/src/index.ts +3 -2
- package/src/mcp/server.ts +1464 -0
- package/tests/api.test.ts +216 -0
- package/tests/doctor.test.ts +168 -0
- package/tests/go.test.ts +142 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock fetch globally
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
global.fetch = mockFetch;
|
|
6
|
+
|
|
7
|
+
describe('API communication', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockFetch.mockClear();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('content API', () => {
|
|
13
|
+
it('should fetch content with API key', async () => {
|
|
14
|
+
mockFetch.mockResolvedValueOnce({
|
|
15
|
+
ok: true,
|
|
16
|
+
json: () => Promise.resolve({
|
|
17
|
+
version: '5.1',
|
|
18
|
+
router: '# Router content',
|
|
19
|
+
modules: {
|
|
20
|
+
'00-core.md': '# Core',
|
|
21
|
+
'02-auth.md': '# Auth',
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const response = await fetch('https://codebakers.ai/api/content', {
|
|
27
|
+
headers: { Authorization: 'Bearer test-key' },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(response.ok).toBe(true);
|
|
31
|
+
const data = await response.json();
|
|
32
|
+
expect(data.version).toBe('5.1');
|
|
33
|
+
expect(Object.keys(data.modules).length).toBe(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should fetch content with trial ID', async () => {
|
|
37
|
+
mockFetch.mockResolvedValueOnce({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: () => Promise.resolve({
|
|
40
|
+
version: '5.1',
|
|
41
|
+
router: '# Router content',
|
|
42
|
+
modules: { '00-core.md': '# Core' },
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const response = await fetch('https://codebakers.ai/api/content', {
|
|
47
|
+
headers: { 'X-Trial-ID': 'trial-123' },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(response.ok).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle unauthorized response', async () => {
|
|
54
|
+
mockFetch.mockResolvedValueOnce({
|
|
55
|
+
ok: false,
|
|
56
|
+
status: 401,
|
|
57
|
+
json: () => Promise.resolve({ error: 'Unauthorized' }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const response = await fetch('https://codebakers.ai/api/content', {
|
|
61
|
+
headers: { Authorization: 'Bearer invalid-key' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(response.ok).toBe(false);
|
|
65
|
+
expect(response.status).toBe(401);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle network errors', async () => {
|
|
69
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
70
|
+
|
|
71
|
+
await expect(
|
|
72
|
+
fetch('https://codebakers.ai/api/content')
|
|
73
|
+
).rejects.toThrow('Network error');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('trial API', () => {
|
|
78
|
+
it('should start a new trial', async () => {
|
|
79
|
+
mockFetch.mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
json: () => Promise.resolve({
|
|
82
|
+
trialId: 'trial-123',
|
|
83
|
+
stage: 'anonymous',
|
|
84
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
85
|
+
daysRemaining: 7,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const response = await fetch('https://codebakers.ai/api/trial/start', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
deviceHash: 'test-hash',
|
|
94
|
+
machineId: 'test-machine',
|
|
95
|
+
platform: 'test',
|
|
96
|
+
hostname: 'test-host',
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(response.ok).toBe(true);
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
expect(data.trialId).toBe('trial-123');
|
|
103
|
+
expect(data.daysRemaining).toBe(7);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle trial not available', async () => {
|
|
107
|
+
mockFetch.mockResolvedValueOnce({
|
|
108
|
+
ok: false,
|
|
109
|
+
status: 400,
|
|
110
|
+
json: () => Promise.resolve({ error: 'trial_not_available' }),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const response = await fetch('https://codebakers.ai/api/trial/start', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ deviceHash: 'used-hash' }),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(response.ok).toBe(false);
|
|
120
|
+
const data = await response.json();
|
|
121
|
+
expect(data.error).toBe('trial_not_available');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle expired trial', async () => {
|
|
125
|
+
mockFetch.mockResolvedValueOnce({
|
|
126
|
+
ok: true,
|
|
127
|
+
json: () => Promise.resolve({
|
|
128
|
+
stage: 'expired',
|
|
129
|
+
canExtend: true,
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const response = await fetch('https://codebakers.ai/api/trial/start', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ deviceHash: 'expired-hash' }),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const data = await response.json();
|
|
140
|
+
expect(data.stage).toBe('expired');
|
|
141
|
+
expect(data.canExtend).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('API key validation', () => {
|
|
146
|
+
it('should validate a correct API key', async () => {
|
|
147
|
+
mockFetch.mockResolvedValueOnce({
|
|
148
|
+
ok: true,
|
|
149
|
+
json: () => Promise.resolve({ valid: true }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const response = await fetch('https://codebakers.ai/api/verify', {
|
|
153
|
+
headers: { Authorization: 'Bearer valid-key' },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(response.ok).toBe(true);
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
expect(data.valid).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should reject an invalid API key', async () => {
|
|
162
|
+
mockFetch.mockResolvedValueOnce({
|
|
163
|
+
ok: false,
|
|
164
|
+
status: 401,
|
|
165
|
+
json: () => Promise.resolve({ valid: false, error: 'Invalid API key' }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const response = await fetch('https://codebakers.ai/api/verify', {
|
|
169
|
+
headers: { Authorization: 'Bearer invalid-key' },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(response.ok).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('error handling', () => {
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
mockFetch.mockClear();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should provide helpful error messages for timeout', async () => {
|
|
183
|
+
mockFetch.mockRejectedValueOnce(new Error('timeout'));
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await fetch('https://codebakers.ai/api/content');
|
|
187
|
+
} catch (error) {
|
|
188
|
+
expect(error).toBeInstanceOf(Error);
|
|
189
|
+
if (error instanceof Error) {
|
|
190
|
+
expect(error.message).toContain('timeout');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle rate limiting', async () => {
|
|
196
|
+
mockFetch.mockResolvedValueOnce({
|
|
197
|
+
ok: false,
|
|
198
|
+
status: 429,
|
|
199
|
+
json: () => Promise.resolve({ error: 'Too many requests' }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const response = await fetch('https://codebakers.ai/api/content');
|
|
203
|
+
expect(response.status).toBe(429);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle server errors', async () => {
|
|
207
|
+
mockFetch.mockResolvedValueOnce({
|
|
208
|
+
ok: false,
|
|
209
|
+
status: 500,
|
|
210
|
+
json: () => Promise.resolve({ error: 'Internal server error' }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const response = await fetch('https://codebakers.ai/api/content');
|
|
214
|
+
expect(response.status).toBe(500);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
describe('doctor command checks', () => {
|
|
7
|
+
let testDir: string;
|
|
8
|
+
let originalCwd: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalCwd = process.cwd();
|
|
12
|
+
testDir = join(tmpdir(), `codebakers-doctor-test-${Date.now()}`);
|
|
13
|
+
mkdirSync(testDir, { recursive: true });
|
|
14
|
+
process.chdir(testDir);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
process.chdir(originalCwd);
|
|
19
|
+
try {
|
|
20
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
} catch {
|
|
22
|
+
// Ignore cleanup errors
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('CLAUDE.md checks', () => {
|
|
27
|
+
it('should detect missing CLAUDE.md', () => {
|
|
28
|
+
const claudeMdPath = join(testDir, 'CLAUDE.md');
|
|
29
|
+
expect(existsSync(claudeMdPath)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should detect valid CodeBakers CLAUDE.md', () => {
|
|
33
|
+
const claudeMdPath = join(testDir, 'CLAUDE.md');
|
|
34
|
+
writeFileSync(claudeMdPath, '# CODEBAKERS SMART ROUTER\nVersion: 5.1');
|
|
35
|
+
|
|
36
|
+
const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
|
|
37
|
+
const isCodeBakers = content.includes('CODEBAKERS') || content.includes('CodeBakers');
|
|
38
|
+
expect(isCodeBakers).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should detect non-CodeBakers CLAUDE.md', () => {
|
|
42
|
+
const claudeMdPath = join(testDir, 'CLAUDE.md');
|
|
43
|
+
writeFileSync(claudeMdPath, '# My Custom Instructions');
|
|
44
|
+
|
|
45
|
+
const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
|
|
46
|
+
const isCodeBakers = content.includes('CODEBAKERS') || content.includes('CodeBakers');
|
|
47
|
+
expect(isCodeBakers).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('.claude folder checks', () => {
|
|
52
|
+
it('should detect missing .claude folder', () => {
|
|
53
|
+
const claudeDir = join(testDir, '.claude');
|
|
54
|
+
expect(existsSync(claudeDir)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should count modules in .claude folder', () => {
|
|
58
|
+
const claudeDir = join(testDir, '.claude');
|
|
59
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
// Create test modules
|
|
62
|
+
const modules = [
|
|
63
|
+
'00-core.md',
|
|
64
|
+
'01-database.md',
|
|
65
|
+
'02-auth.md',
|
|
66
|
+
'03-api.md',
|
|
67
|
+
'04-frontend.md',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const mod of modules) {
|
|
71
|
+
writeFileSync(join(claudeDir, mod), `# ${mod}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
|
|
75
|
+
expect(files.length).toBe(5);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should detect insufficient modules (less than 10)', () => {
|
|
79
|
+
const claudeDir = join(testDir, '.claude');
|
|
80
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
81
|
+
|
|
82
|
+
// Create only 3 modules
|
|
83
|
+
writeFileSync(join(claudeDir, '00-core.md'), '# Core');
|
|
84
|
+
writeFileSync(join(claudeDir, '01-database.md'), '# Database');
|
|
85
|
+
writeFileSync(join(claudeDir, '02-auth.md'), '# Auth');
|
|
86
|
+
|
|
87
|
+
const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
|
|
88
|
+
expect(files.length).toBeLessThan(10);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should detect full module set (47+ modules)', () => {
|
|
92
|
+
const claudeDir = join(testDir, '.claude');
|
|
93
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// Create 47 modules
|
|
96
|
+
for (let i = 0; i < 47; i++) {
|
|
97
|
+
const name = i.toString().padStart(2, '0');
|
|
98
|
+
writeFileSync(join(claudeDir, `${name}-module.md`), `# Module ${i}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
|
|
102
|
+
expect(files.length).toBeGreaterThanOrEqual(47);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should check for required 00-core.md', () => {
|
|
106
|
+
const claudeDir = join(testDir, '.claude');
|
|
107
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
const corePath = join(claudeDir, '00-core.md');
|
|
110
|
+
expect(existsSync(corePath)).toBe(false);
|
|
111
|
+
|
|
112
|
+
writeFileSync(corePath, '# Core patterns');
|
|
113
|
+
expect(existsSync(corePath)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('project state checks', () => {
|
|
118
|
+
it('should handle missing PROJECT-STATE.md gracefully', () => {
|
|
119
|
+
const statePath = join(testDir, 'PROJECT-STATE.md');
|
|
120
|
+
// This is optional, should not fail
|
|
121
|
+
expect(existsSync(statePath)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should detect existing PROJECT-STATE.md', () => {
|
|
125
|
+
const statePath = join(testDir, 'PROJECT-STATE.md');
|
|
126
|
+
writeFileSync(statePath, '# Project State\n');
|
|
127
|
+
expect(existsSync(statePath)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('doctor summary', () => {
|
|
133
|
+
it('should calculate correct pass/fail counts', () => {
|
|
134
|
+
const checks = [
|
|
135
|
+
{ ok: true, message: 'Check 1' },
|
|
136
|
+
{ ok: true, message: 'Check 2' },
|
|
137
|
+
{ ok: false, message: 'Check 3' },
|
|
138
|
+
{ ok: true, message: 'Check 4' },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const passed = checks.filter(c => c.ok).length;
|
|
142
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
143
|
+
const total = checks.length;
|
|
144
|
+
|
|
145
|
+
expect(passed).toBe(3);
|
|
146
|
+
expect(failed).toBe(1);
|
|
147
|
+
expect(total).toBe(4);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should provide fix suggestions for common issues', () => {
|
|
151
|
+
const suggestions: string[] = [];
|
|
152
|
+
|
|
153
|
+
// Simulate missing CLAUDE.md
|
|
154
|
+
const hasClaudeMd = false;
|
|
155
|
+
if (!hasClaudeMd) {
|
|
156
|
+
suggestions.push('Run: codebakers install');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Simulate missing hook
|
|
160
|
+
const hasHook = false;
|
|
161
|
+
if (!hasHook) {
|
|
162
|
+
suggestions.push('Run: codebakers install-hook');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
expect(suggestions).toContain('Run: codebakers install');
|
|
166
|
+
expect(suggestions).toContain('Run: codebakers install-hook');
|
|
167
|
+
});
|
|
168
|
+
});
|
package/tests/go.test.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
// Mock modules before importing
|
|
7
|
+
vi.mock('../src/config.js', () => ({
|
|
8
|
+
getApiKey: vi.fn(() => null),
|
|
9
|
+
getTrialState: vi.fn(() => null),
|
|
10
|
+
setTrialState: vi.fn(),
|
|
11
|
+
getApiUrl: vi.fn(() => 'https://codebakers.ai'),
|
|
12
|
+
isTrialExpired: vi.fn(() => false),
|
|
13
|
+
getTrialDaysRemaining: vi.fn(() => 7),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../src/lib/fingerprint.js', () => ({
|
|
17
|
+
getDeviceFingerprint: vi.fn(() => ({
|
|
18
|
+
deviceHash: 'test-hash',
|
|
19
|
+
machineId: 'test-machine',
|
|
20
|
+
platform: 'test',
|
|
21
|
+
hostname: 'test-host',
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('go command', () => {
|
|
26
|
+
let testDir: string;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// Create a temp directory for each test
|
|
30
|
+
testDir = join(tmpdir(), `codebakers-test-${Date.now()}`);
|
|
31
|
+
mkdirSync(testDir, { recursive: true });
|
|
32
|
+
process.chdir(testDir);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
// Clean up
|
|
37
|
+
try {
|
|
38
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore cleanup errors
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should create CLAUDE.md when patterns are installed', async () => {
|
|
45
|
+
const claudeMdPath = join(testDir, 'CLAUDE.md');
|
|
46
|
+
|
|
47
|
+
// Simulate pattern installation
|
|
48
|
+
writeFileSync(claudeMdPath, '# CodeBakers Router');
|
|
49
|
+
|
|
50
|
+
expect(existsSync(claudeMdPath)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should create .claude directory with modules', async () => {
|
|
54
|
+
const claudeDir = join(testDir, '.claude');
|
|
55
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
// Simulate module installation
|
|
58
|
+
writeFileSync(join(claudeDir, '00-core.md'), '# Core patterns');
|
|
59
|
+
writeFileSync(join(claudeDir, '02-auth.md'), '# Auth patterns');
|
|
60
|
+
|
|
61
|
+
expect(existsSync(claudeDir)).toBe(true);
|
|
62
|
+
expect(existsSync(join(claudeDir, '00-core.md'))).toBe(true);
|
|
63
|
+
expect(existsSync(join(claudeDir, '02-auth.md'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle network errors gracefully', async () => {
|
|
67
|
+
// Mock fetch to fail
|
|
68
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
69
|
+
|
|
70
|
+
// The go command should not throw, just warn
|
|
71
|
+
expect(() => {
|
|
72
|
+
// Simulating the error handling logic
|
|
73
|
+
try {
|
|
74
|
+
throw new Error('Network error');
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof Error && error.message.includes('Network')) {
|
|
77
|
+
// Expected behavior - handle gracefully
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('pattern file writing', () => {
|
|
85
|
+
let testDir: string;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
testDir = join(tmpdir(), `codebakers-test-${Date.now()}`);
|
|
89
|
+
mkdirSync(testDir, { recursive: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
try {
|
|
94
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore cleanup errors
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not overwrite existing CLAUDE.md', () => {
|
|
101
|
+
const claudeMdPath = join(testDir, 'CLAUDE.md');
|
|
102
|
+
const originalContent = '# My existing CLAUDE.md';
|
|
103
|
+
|
|
104
|
+
writeFileSync(claudeMdPath, originalContent);
|
|
105
|
+
|
|
106
|
+
// Simulate check before writing
|
|
107
|
+
if (existsSync(claudeMdPath)) {
|
|
108
|
+
// Should skip writing
|
|
109
|
+
const content = require('fs').readFileSync(claudeMdPath, 'utf-8');
|
|
110
|
+
expect(content).toBe(originalContent);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should add .claude/ to .gitignore if exists', () => {
|
|
115
|
+
const gitignorePath = join(testDir, '.gitignore');
|
|
116
|
+
writeFileSync(gitignorePath, 'node_modules/\n.env\n');
|
|
117
|
+
|
|
118
|
+
// Simulate gitignore update
|
|
119
|
+
let gitignore = require('fs').readFileSync(gitignorePath, 'utf-8');
|
|
120
|
+
if (!gitignore.includes('.claude/')) {
|
|
121
|
+
gitignore += '\n# CodeBakers patterns\n.claude/\n';
|
|
122
|
+
writeFileSync(gitignorePath, gitignore);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const updatedContent = require('fs').readFileSync(gitignorePath, 'utf-8');
|
|
126
|
+
expect(updatedContent).toContain('.claude/');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle missing .gitignore gracefully', () => {
|
|
130
|
+
const gitignorePath = join(testDir, '.gitignore');
|
|
131
|
+
|
|
132
|
+
// Don't create .gitignore
|
|
133
|
+
expect(existsSync(gitignorePath)).toBe(false);
|
|
134
|
+
|
|
135
|
+
// Should not throw when trying to update non-existent .gitignore
|
|
136
|
+
expect(() => {
|
|
137
|
+
if (existsSync(gitignorePath)) {
|
|
138
|
+
// Would update gitignore
|
|
139
|
+
}
|
|
140
|
+
}).not.toThrow();
|
|
141
|
+
});
|
|
142
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['tests/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'html'],
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
|
+
exclude: ['src/mcp/**', 'src/templates/**'],
|
|
13
|
+
},
|
|
14
|
+
testTimeout: 30000,
|
|
15
|
+
},
|
|
16
|
+
});
|