@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.
@@ -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
+ });
@@ -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
+ });
@@ -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
+ });