gh-here 1.0.1 → 1.0.4
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/.claude/settings.local.json +15 -10
- package/README.md +11 -4
- package/bin/gh-here.js +8 -1255
- package/lib/file-utils.js +264 -0
- package/lib/git.js +207 -0
- package/lib/gitignore.js +91 -0
- package/lib/renderers.js +569 -0
- package/lib/server.js +391 -0
- package/package.json +1 -1
- package/public/app.js +692 -129
- package/public/styles.css +414 -44
- package/tests/draftManager.test.js +241 -0
- package/tests/httpService.test.js +268 -0
- package/tests/languageDetection.test.js +145 -0
- package/tests/pathUtils.test.js +136 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Tests for DraftManager utility
|
|
2
|
+
// Run with: node tests/draftManager.test.js
|
|
3
|
+
|
|
4
|
+
// Mock localStorage for testing
|
|
5
|
+
class MockLocalStorage {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.store = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getItem(key) {
|
|
11
|
+
return this.store.hasOwnProperty(key) ? this.store[key] : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setItem(key, value) {
|
|
15
|
+
this.store[key] = String(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
removeItem(key) {
|
|
19
|
+
delete this.store[key];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
clear() {
|
|
23
|
+
this.store = {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
key(index) {
|
|
27
|
+
const keys = Object.keys(this.store);
|
|
28
|
+
return keys[index] || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get length() {
|
|
32
|
+
return Object.keys(this.store).length;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create testable version of DraftManager with injected storage
|
|
37
|
+
function createDraftManager(storage) {
|
|
38
|
+
return {
|
|
39
|
+
STORAGE_PREFIX: 'gh-here-draft-',
|
|
40
|
+
|
|
41
|
+
saveDraft(filePath, content) {
|
|
42
|
+
storage.setItem(`${this.STORAGE_PREFIX}${filePath}`, content);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
loadDraft(filePath) {
|
|
46
|
+
return storage.getItem(`${this.STORAGE_PREFIX}${filePath}`);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
clearDraft(filePath) {
|
|
50
|
+
storage.removeItem(`${this.STORAGE_PREFIX}${filePath}`);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
hasDraftChanges(filePath, originalContent) {
|
|
54
|
+
const draft = this.loadDraft(filePath);
|
|
55
|
+
return draft !== null && draft !== originalContent;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
getAllDrafts() {
|
|
59
|
+
const drafts = {};
|
|
60
|
+
for (let i = 0; i < storage.length; i++) {
|
|
61
|
+
const key = storage.key(i);
|
|
62
|
+
if (key && key.startsWith(this.STORAGE_PREFIX)) {
|
|
63
|
+
const filePath = key.replace(this.STORAGE_PREFIX, '');
|
|
64
|
+
drafts[filePath] = storage.getItem(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return drafts;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Simple test framework
|
|
73
|
+
function test(name, fn) {
|
|
74
|
+
try {
|
|
75
|
+
fn();
|
|
76
|
+
console.log(`✅ ${name}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(`❌ ${name}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertEqual(actual, expected, message = '') {
|
|
83
|
+
if (actual !== expected) {
|
|
84
|
+
throw new Error(`Expected "${expected}" but got "${actual}". ${message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function assertNull(actual, message = '') {
|
|
89
|
+
if (actual !== null) {
|
|
90
|
+
throw new Error(`Expected null but got "${actual}". ${message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertTrue(actual, message = '') {
|
|
95
|
+
if (!actual) {
|
|
96
|
+
throw new Error(`Expected truthy but got "${actual}". ${message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertFalse(actual, message = '') {
|
|
101
|
+
if (actual) {
|
|
102
|
+
throw new Error(`Expected falsy but got "${actual}". ${message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Test cases
|
|
107
|
+
console.log('🧪 Testing Draft Manager\n');
|
|
108
|
+
|
|
109
|
+
test('saves and loads drafts correctly', () => {
|
|
110
|
+
const storage = new MockLocalStorage();
|
|
111
|
+
const draftManager = createDraftManager(storage);
|
|
112
|
+
|
|
113
|
+
const filePath = '/src/app.js';
|
|
114
|
+
const content = 'console.log("hello world");';
|
|
115
|
+
|
|
116
|
+
// Initially no draft
|
|
117
|
+
assertNull(draftManager.loadDraft(filePath));
|
|
118
|
+
|
|
119
|
+
// Save draft
|
|
120
|
+
draftManager.saveDraft(filePath, content);
|
|
121
|
+
|
|
122
|
+
// Load draft
|
|
123
|
+
assertEqual(draftManager.loadDraft(filePath), content);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('clears drafts correctly', () => {
|
|
127
|
+
const storage = new MockLocalStorage();
|
|
128
|
+
const draftManager = createDraftManager(storage);
|
|
129
|
+
|
|
130
|
+
const filePath = '/src/app.js';
|
|
131
|
+
const content = 'console.log("test");';
|
|
132
|
+
|
|
133
|
+
// Save and verify
|
|
134
|
+
draftManager.saveDraft(filePath, content);
|
|
135
|
+
assertEqual(draftManager.loadDraft(filePath), content);
|
|
136
|
+
|
|
137
|
+
// Clear and verify
|
|
138
|
+
draftManager.clearDraft(filePath);
|
|
139
|
+
assertNull(draftManager.loadDraft(filePath));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('detects draft changes correctly', () => {
|
|
143
|
+
const storage = new MockLocalStorage();
|
|
144
|
+
const draftManager = createDraftManager(storage);
|
|
145
|
+
|
|
146
|
+
const filePath = '/src/app.js';
|
|
147
|
+
const originalContent = 'const x = 1;';
|
|
148
|
+
const draftContent = 'const x = 2;';
|
|
149
|
+
|
|
150
|
+
// No draft initially
|
|
151
|
+
assertFalse(draftManager.hasDraftChanges(filePath, originalContent));
|
|
152
|
+
|
|
153
|
+
// Save draft with different content
|
|
154
|
+
draftManager.saveDraft(filePath, draftContent);
|
|
155
|
+
assertTrue(draftManager.hasDraftChanges(filePath, originalContent));
|
|
156
|
+
|
|
157
|
+
// Save draft with same content
|
|
158
|
+
draftManager.saveDraft(filePath, originalContent);
|
|
159
|
+
assertFalse(draftManager.hasDraftChanges(filePath, originalContent));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('gets all drafts correctly', () => {
|
|
163
|
+
const storage = new MockLocalStorage();
|
|
164
|
+
const draftManager = createDraftManager(storage);
|
|
165
|
+
|
|
166
|
+
// Add some non-draft items to storage
|
|
167
|
+
storage.setItem('other-key', 'other-value');
|
|
168
|
+
|
|
169
|
+
// Add drafts
|
|
170
|
+
draftManager.saveDraft('/src/app.js', 'app content');
|
|
171
|
+
draftManager.saveDraft('/src/utils.js', 'utils content');
|
|
172
|
+
draftManager.saveDraft('/README.md', 'readme content');
|
|
173
|
+
|
|
174
|
+
const allDrafts = draftManager.getAllDrafts();
|
|
175
|
+
|
|
176
|
+
// Should only return draft items
|
|
177
|
+
assertEqual(Object.keys(allDrafts).length, 3);
|
|
178
|
+
assertEqual(allDrafts['/src/app.js'], 'app content');
|
|
179
|
+
assertEqual(allDrafts['/src/utils.js'], 'utils content');
|
|
180
|
+
assertEqual(allDrafts['/README.md'], 'readme content');
|
|
181
|
+
|
|
182
|
+
// Should not include non-draft items
|
|
183
|
+
assertEqual(allDrafts['other-key'], undefined);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('handles multiple drafts independently', () => {
|
|
187
|
+
const storage = new MockLocalStorage();
|
|
188
|
+
const draftManager = createDraftManager(storage);
|
|
189
|
+
|
|
190
|
+
const file1 = '/src/app.js';
|
|
191
|
+
const file2 = '/src/utils.js';
|
|
192
|
+
const content1 = 'app content';
|
|
193
|
+
const content2 = 'utils content';
|
|
194
|
+
|
|
195
|
+
// Save drafts for different files
|
|
196
|
+
draftManager.saveDraft(file1, content1);
|
|
197
|
+
draftManager.saveDraft(file2, content2);
|
|
198
|
+
|
|
199
|
+
// Each file has its own draft
|
|
200
|
+
assertEqual(draftManager.loadDraft(file1), content1);
|
|
201
|
+
assertEqual(draftManager.loadDraft(file2), content2);
|
|
202
|
+
|
|
203
|
+
// Clear one draft, other remains
|
|
204
|
+
draftManager.clearDraft(file1);
|
|
205
|
+
assertNull(draftManager.loadDraft(file1));
|
|
206
|
+
assertEqual(draftManager.loadDraft(file2), content2);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('uses correct storage prefix', () => {
|
|
210
|
+
const storage = new MockLocalStorage();
|
|
211
|
+
const draftManager = createDraftManager(storage);
|
|
212
|
+
|
|
213
|
+
const filePath = '/src/app.js';
|
|
214
|
+
const content = 'test content';
|
|
215
|
+
|
|
216
|
+
draftManager.saveDraft(filePath, content);
|
|
217
|
+
|
|
218
|
+
// Check that correct key is used in storage
|
|
219
|
+
const expectedKey = 'gh-here-draft-/src/app.js';
|
|
220
|
+
assertEqual(storage.getItem(expectedKey), content);
|
|
221
|
+
|
|
222
|
+
// Verify storage length
|
|
223
|
+
assertEqual(storage.length, 1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('handles empty content correctly', () => {
|
|
227
|
+
const storage = new MockLocalStorage();
|
|
228
|
+
const draftManager = createDraftManager(storage);
|
|
229
|
+
|
|
230
|
+
const filePath = '/empty.js';
|
|
231
|
+
const emptyContent = '';
|
|
232
|
+
|
|
233
|
+
draftManager.saveDraft(filePath, emptyContent);
|
|
234
|
+
assertEqual(draftManager.loadDraft(filePath), ''); // localStorage stores empty string as empty string
|
|
235
|
+
|
|
236
|
+
// Empty content should be detected as different from non-empty
|
|
237
|
+
assertTrue(draftManager.hasDraftChanges(filePath, 'non-empty'), 'Empty draft should be different from non-empty original');
|
|
238
|
+
assertFalse(draftManager.hasDraftChanges(filePath, ''));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
console.log('\n🎉 Draft Manager tests complete!');
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Tests for HTTP Service abstraction
|
|
2
|
+
// Run with: node tests/httpService.test.js
|
|
3
|
+
|
|
4
|
+
// Simple mock implementation for jest.fn()
|
|
5
|
+
function jest_fn() {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const mockFn = (...args) => {
|
|
8
|
+
calls.push(args);
|
|
9
|
+
return mockFn._returnValue;
|
|
10
|
+
};
|
|
11
|
+
mockFn.mock = { calls };
|
|
12
|
+
mockFn.mockResolvedValue = (value) => {
|
|
13
|
+
mockFn._returnValue = Promise.resolve(value);
|
|
14
|
+
return mockFn;
|
|
15
|
+
};
|
|
16
|
+
return mockFn;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Replace jest with our simple implementation
|
|
20
|
+
const jest = { fn: jest_fn };
|
|
21
|
+
|
|
22
|
+
// Mock fetch for testing
|
|
23
|
+
class MockResponse {
|
|
24
|
+
constructor(data, options = {}) {
|
|
25
|
+
this._data = data;
|
|
26
|
+
this.ok = options.ok !== false;
|
|
27
|
+
this.status = options.status || (this.ok ? 200 : 500);
|
|
28
|
+
this.statusText = options.statusText || (this.ok ? 'OK' : 'Internal Server Error');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async json() {
|
|
32
|
+
return this._data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async text() {
|
|
36
|
+
return typeof this._data === 'string' ? this._data : JSON.stringify(this._data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mock fetch implementation
|
|
41
|
+
function createMockFetch(responseData, options = {}) {
|
|
42
|
+
return jest.fn().mockResolvedValue(new MockResponse(responseData, options));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// HTTP Service abstraction - pure functions, easily testable
|
|
46
|
+
function createHttpService(fetchFn = fetch) {
|
|
47
|
+
return {
|
|
48
|
+
// File operations
|
|
49
|
+
async getFileContent(filePath) {
|
|
50
|
+
const response = await fetchFn(`/api/file-content?path=${encodeURIComponent(filePath)}`);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
return response.text();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async saveFile(filePath, content) {
|
|
58
|
+
const response = await fetchFn('/api/save-file', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ path: filePath, content })
|
|
62
|
+
});
|
|
63
|
+
return response.json();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async createFile(path, filename) {
|
|
67
|
+
const response = await fetchFn('/api/create-file', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ path, filename })
|
|
71
|
+
});
|
|
72
|
+
return response.json();
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async deleteFile(path) {
|
|
76
|
+
const response = await fetchFn('/api/delete', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ path })
|
|
80
|
+
});
|
|
81
|
+
return response.json();
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async renameFile(oldPath, newPath) {
|
|
85
|
+
const response = await fetchFn('/api/rename', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ oldPath, newPath })
|
|
89
|
+
});
|
|
90
|
+
return response.json();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Folder operations
|
|
94
|
+
async createFolder(path, foldername) {
|
|
95
|
+
const response = await fetchFn('/api/create-folder', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ path, foldername })
|
|
99
|
+
});
|
|
100
|
+
return response.json();
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Git operations
|
|
104
|
+
async getGitStatus(currentPath) {
|
|
105
|
+
const response = await fetchFn(`/api/git-status?currentPath=${encodeURIComponent(currentPath)}`);
|
|
106
|
+
return response.json();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async getGitDiff(filePath) {
|
|
110
|
+
const response = await fetchFn(`/api/git-diff?path=${encodeURIComponent(filePath)}`);
|
|
111
|
+
return response.json();
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async commitSelectedFiles(files, message) {
|
|
115
|
+
const response = await fetchFn('/api/git-commit-selected', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ files, message })
|
|
119
|
+
});
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Simple test framework (since we don't have a real test runner)
|
|
126
|
+
function test(name, fn) {
|
|
127
|
+
try {
|
|
128
|
+
fn();
|
|
129
|
+
console.log(`✅ ${name}`);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(`❌ ${name}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function assertEqual(actual, expected, message = '') {
|
|
136
|
+
if (actual !== expected) {
|
|
137
|
+
throw new Error(`Expected "${expected}" but got "${actual}". ${message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function assertDeepEqual(actual, expected, message = '') {
|
|
142
|
+
const actualStr = JSON.stringify(actual);
|
|
143
|
+
const expectedStr = JSON.stringify(expected);
|
|
144
|
+
if (actualStr !== expectedStr) {
|
|
145
|
+
throw new Error(`Expected ${expectedStr} but got ${actualStr}. ${message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Test cases
|
|
150
|
+
console.log('🧪 Testing HTTP Service\n');
|
|
151
|
+
|
|
152
|
+
test('gets file content correctly', async () => {
|
|
153
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse('file content'));
|
|
154
|
+
const httpService = createHttpService(mockFetch);
|
|
155
|
+
|
|
156
|
+
const content = await httpService.getFileContent('src/app.js');
|
|
157
|
+
assertEqual(content, 'file content');
|
|
158
|
+
assertEqual(mockFetch.mock.calls[0][0], '/api/file-content?path=src%2Fapp.js');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('saves file with correct parameters', async () => {
|
|
162
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
163
|
+
const httpService = createHttpService(mockFetch);
|
|
164
|
+
|
|
165
|
+
const result = await httpService.saveFile('src/app.js', 'new content');
|
|
166
|
+
assertDeepEqual(result, { success: true });
|
|
167
|
+
|
|
168
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
169
|
+
assertEqual(url, '/api/save-file');
|
|
170
|
+
assertEqual(options.method, 'POST');
|
|
171
|
+
assertDeepEqual(JSON.parse(options.body), { path: 'src/app.js', content: 'new content' });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('creates file with correct parameters', async () => {
|
|
175
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
176
|
+
const httpService = createHttpService(mockFetch);
|
|
177
|
+
|
|
178
|
+
const result = await httpService.createFile('src', 'newfile.js');
|
|
179
|
+
assertDeepEqual(result, { success: true });
|
|
180
|
+
|
|
181
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
182
|
+
assertEqual(url, '/api/create-file');
|
|
183
|
+
assertDeepEqual(JSON.parse(options.body), { path: 'src', filename: 'newfile.js' });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('deletes file with correct parameters', async () => {
|
|
187
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
188
|
+
const httpService = createHttpService(mockFetch);
|
|
189
|
+
|
|
190
|
+
const result = await httpService.deleteFile('src/oldfile.js');
|
|
191
|
+
assertDeepEqual(result, { success: true });
|
|
192
|
+
|
|
193
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
194
|
+
assertEqual(url, '/api/delete');
|
|
195
|
+
assertDeepEqual(JSON.parse(options.body), { path: 'src/oldfile.js' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('renames file with correct parameters', async () => {
|
|
199
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
200
|
+
const httpService = createHttpService(mockFetch);
|
|
201
|
+
|
|
202
|
+
const result = await httpService.renameFile('src/old.js', 'src/new.js');
|
|
203
|
+
assertDeepEqual(result, { success: true });
|
|
204
|
+
|
|
205
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
206
|
+
assertEqual(url, '/api/rename');
|
|
207
|
+
assertDeepEqual(JSON.parse(options.body), { oldPath: 'src/old.js', newPath: 'src/new.js' });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('creates folder with correct parameters', async () => {
|
|
211
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
212
|
+
const httpService = createHttpService(mockFetch);
|
|
213
|
+
|
|
214
|
+
const result = await httpService.createFolder('src', 'components');
|
|
215
|
+
assertDeepEqual(result, { success: true });
|
|
216
|
+
|
|
217
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
218
|
+
assertEqual(url, '/api/create-folder');
|
|
219
|
+
assertDeepEqual(JSON.parse(options.body), { path: 'src', foldername: 'components' });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('gets git status with correct parameters', async () => {
|
|
223
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ files: [] }));
|
|
224
|
+
const httpService = createHttpService(mockFetch);
|
|
225
|
+
|
|
226
|
+
const result = await httpService.getGitStatus('src');
|
|
227
|
+
assertDeepEqual(result, { files: [] });
|
|
228
|
+
|
|
229
|
+
assertEqual(mockFetch.mock.calls[0][0], '/api/git-status?currentPath=src');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('gets git diff with correct parameters', async () => {
|
|
233
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ diff: 'diff content' }));
|
|
234
|
+
const httpService = createHttpService(mockFetch);
|
|
235
|
+
|
|
236
|
+
const result = await httpService.getGitDiff('src/app.js');
|
|
237
|
+
assertDeepEqual(result, { diff: 'diff content' });
|
|
238
|
+
|
|
239
|
+
assertEqual(mockFetch.mock.calls[0][0], '/api/git-diff?path=src%2Fapp.js');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('commits selected files with correct parameters', async () => {
|
|
243
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse({ success: true }));
|
|
244
|
+
const httpService = createHttpService(mockFetch);
|
|
245
|
+
|
|
246
|
+
const files = ['src/app.js', 'src/utils.js'];
|
|
247
|
+
const message = 'Fix bugs';
|
|
248
|
+
const result = await httpService.commitSelectedFiles(files, message);
|
|
249
|
+
assertDeepEqual(result, { success: true });
|
|
250
|
+
|
|
251
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
252
|
+
assertEqual(url, '/api/git-commit-selected');
|
|
253
|
+
assertDeepEqual(JSON.parse(options.body), { files, message });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('handles HTTP errors correctly', async () => {
|
|
257
|
+
const mockFetch = jest.fn().mockResolvedValue(new MockResponse('Not found', { ok: false, status: 404, statusText: 'Not Found' }));
|
|
258
|
+
const httpService = createHttpService(mockFetch);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await httpService.getFileContent('nonexistent.js');
|
|
262
|
+
throw new Error('Should have thrown an error');
|
|
263
|
+
} catch (error) {
|
|
264
|
+
assertEqual(error.message, 'HTTP 404: Not Found');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
console.log('\n🎉 HTTP Service tests complete!');
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Simple unit tests for language detection
|
|
2
|
+
// Run with: node tests/languageDetection.test.js
|
|
3
|
+
|
|
4
|
+
// Import the function (in a real setup, you'd use proper modules)
|
|
5
|
+
// For now, we'll copy the function to test it standalone
|
|
6
|
+
|
|
7
|
+
function getLanguageFromExtension(filename) {
|
|
8
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
9
|
+
const languageMap = {
|
|
10
|
+
// JavaScript family
|
|
11
|
+
'js': 'javascript',
|
|
12
|
+
'mjs': 'javascript',
|
|
13
|
+
'jsx': 'javascript',
|
|
14
|
+
'ts': 'typescript',
|
|
15
|
+
'tsx': 'typescript',
|
|
16
|
+
|
|
17
|
+
// Web languages
|
|
18
|
+
'html': 'html',
|
|
19
|
+
'htm': 'html',
|
|
20
|
+
'css': 'css',
|
|
21
|
+
'scss': 'scss',
|
|
22
|
+
'sass': 'sass',
|
|
23
|
+
'less': 'less',
|
|
24
|
+
|
|
25
|
+
// Data formats
|
|
26
|
+
'json': 'json',
|
|
27
|
+
'xml': 'xml',
|
|
28
|
+
'yaml': 'yaml',
|
|
29
|
+
'yml': 'yaml',
|
|
30
|
+
|
|
31
|
+
// Programming languages
|
|
32
|
+
'py': 'python',
|
|
33
|
+
'java': 'java',
|
|
34
|
+
'go': 'go',
|
|
35
|
+
'rs': 'rust',
|
|
36
|
+
'php': 'php',
|
|
37
|
+
'rb': 'ruby',
|
|
38
|
+
'swift': 'swift',
|
|
39
|
+
'kt': 'kotlin',
|
|
40
|
+
'dart': 'dart',
|
|
41
|
+
|
|
42
|
+
// Systems languages
|
|
43
|
+
'c': 'c',
|
|
44
|
+
'cpp': 'cpp',
|
|
45
|
+
'cc': 'cpp',
|
|
46
|
+
'cxx': 'cpp',
|
|
47
|
+
'h': 'c',
|
|
48
|
+
'hpp': 'cpp',
|
|
49
|
+
|
|
50
|
+
// Shell and scripts
|
|
51
|
+
'sh': 'shell',
|
|
52
|
+
'bash': 'shell',
|
|
53
|
+
'zsh': 'shell',
|
|
54
|
+
'fish': 'shell',
|
|
55
|
+
'ps1': 'powershell',
|
|
56
|
+
|
|
57
|
+
// Other languages
|
|
58
|
+
'sql': 'sql',
|
|
59
|
+
'r': 'r',
|
|
60
|
+
'scala': 'scala',
|
|
61
|
+
'clj': 'clojure',
|
|
62
|
+
'lua': 'lua',
|
|
63
|
+
'pl': 'perl',
|
|
64
|
+
'groovy': 'groovy',
|
|
65
|
+
|
|
66
|
+
// Config and text
|
|
67
|
+
'md': 'markdown',
|
|
68
|
+
'txt': 'plaintext',
|
|
69
|
+
'log': 'plaintext'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Special filename handling
|
|
73
|
+
const basename = filename.toLowerCase();
|
|
74
|
+
if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) return 'dockerfile';
|
|
75
|
+
if (basename === 'makefile') return 'makefile';
|
|
76
|
+
if (basename.startsWith('.env')) return 'dotenv';
|
|
77
|
+
if (basename === 'package.json' || basename === 'composer.json') return 'json';
|
|
78
|
+
|
|
79
|
+
return languageMap[ext] || 'plaintext';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Simple test framework
|
|
83
|
+
function test(name, fn) {
|
|
84
|
+
try {
|
|
85
|
+
fn();
|
|
86
|
+
console.log(`✅ ${name}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.log(`❌ ${name}: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertEqual(actual, expected, message = '') {
|
|
93
|
+
if (actual !== expected) {
|
|
94
|
+
throw new Error(`Expected "${expected}" but got "${actual}". ${message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Test cases
|
|
99
|
+
console.log('🧪 Testing Language Detection Function\n');
|
|
100
|
+
|
|
101
|
+
test('detects JavaScript files', () => {
|
|
102
|
+
assertEqual(getLanguageFromExtension('app.js'), 'javascript');
|
|
103
|
+
assertEqual(getLanguageFromExtension('module.mjs'), 'javascript');
|
|
104
|
+
assertEqual(getLanguageFromExtension('Component.jsx'), 'javascript');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('detects TypeScript files', () => {
|
|
108
|
+
assertEqual(getLanguageFromExtension('main.ts'), 'typescript');
|
|
109
|
+
assertEqual(getLanguageFromExtension('Component.tsx'), 'typescript');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('detects web languages', () => {
|
|
113
|
+
assertEqual(getLanguageFromExtension('index.html'), 'html');
|
|
114
|
+
assertEqual(getLanguageFromExtension('styles.css'), 'css');
|
|
115
|
+
assertEqual(getLanguageFromExtension('styles.scss'), 'scss');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('detects programming languages', () => {
|
|
119
|
+
assertEqual(getLanguageFromExtension('script.py'), 'python');
|
|
120
|
+
assertEqual(getLanguageFromExtension('Main.java'), 'java');
|
|
121
|
+
assertEqual(getLanguageFromExtension('main.go'), 'go');
|
|
122
|
+
assertEqual(getLanguageFromExtension('lib.rs'), 'rust');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('detects special filenames', () => {
|
|
126
|
+
assertEqual(getLanguageFromExtension('Dockerfile'), 'dockerfile');
|
|
127
|
+
assertEqual(getLanguageFromExtension('dockerfile.prod'), 'dockerfile');
|
|
128
|
+
assertEqual(getLanguageFromExtension('Makefile'), 'makefile');
|
|
129
|
+
assertEqual(getLanguageFromExtension('.env'), 'dotenv');
|
|
130
|
+
assertEqual(getLanguageFromExtension('.env.local'), 'dotenv');
|
|
131
|
+
assertEqual(getLanguageFromExtension('package.json'), 'json');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('defaults to plaintext for unknown extensions', () => {
|
|
135
|
+
assertEqual(getLanguageFromExtension('file.unknown'), 'plaintext');
|
|
136
|
+
assertEqual(getLanguageFromExtension('README'), 'plaintext');
|
|
137
|
+
assertEqual(getLanguageFromExtension('file.xyz'), 'plaintext');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('handles files without extensions', () => {
|
|
141
|
+
assertEqual(getLanguageFromExtension('README'), 'plaintext');
|
|
142
|
+
assertEqual(getLanguageFromExtension('LICENSE'), 'plaintext');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log('\n🎉 Language detection tests complete!');
|