agentgui 1.0.261 → 1.0.262

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,147 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import http from 'http';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const PORT = 8899;
7
+ const server = http.createServer();
8
+ const wss = new WebSocketServer({ server, clientTracking: true });
9
+
10
+ let broadcastedMessages = [];
11
+ let clientConnected = false;
12
+
13
+ wss.on('connection', (ws) => {
14
+ clientConnected = true;
15
+ console.log('[WS] Client connected');
16
+
17
+ ws.on('message', (data) => {
18
+ const msg = JSON.parse(data);
19
+ console.log('[WS] Client sent:', msg.type);
20
+ });
21
+
22
+ ws.on('close', () => {
23
+ clientConnected = false;
24
+ console.log('[WS] Client disconnected');
25
+ });
26
+ });
27
+
28
+ function broadcastSync(event) {
29
+ if (wss.clients.size === 0) return;
30
+ const data = JSON.stringify(event);
31
+ broadcastedMessages.push(event);
32
+ for (const client of wss.clients) {
33
+ if (client.readyState === 1) {
34
+ client.send(data);
35
+ }
36
+ }
37
+ }
38
+
39
+ function broadcastModelProgress(progress) {
40
+ const broadcastData = {
41
+ type: 'model_download_progress',
42
+ modelId: progress.type || 'unknown',
43
+ bytesDownloaded: progress.bytesDownloaded || 0,
44
+ bytesRemaining: progress.bytesRemaining || 0,
45
+ totalBytes: progress.totalBytes || 0,
46
+ downloadSpeed: progress.downloadSpeed || 0,
47
+ eta: progress.eta || 0,
48
+ retryCount: progress.retryCount || 0,
49
+ currentGateway: progress.currentGateway || '',
50
+ status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
51
+ percentComplete: progress.percentComplete || 0,
52
+ completedFiles: progress.completedFiles || 0,
53
+ totalFiles: progress.totalFiles || 0,
54
+ timestamp: Date.now(),
55
+ ...progress
56
+ };
57
+ broadcastSync(broadcastData);
58
+ }
59
+
60
+ server.listen(PORT, () => {
61
+ console.log(`[Server] Listening on ws://localhost:${PORT}`);
62
+
63
+ setTimeout(() => {
64
+ console.log('\n[Test] Simulating model download progress...\n');
65
+
66
+ broadcastModelProgress({
67
+ type: 'stt',
68
+ started: true,
69
+ downloading: true,
70
+ completedFiles: 0,
71
+ totalFiles: 10
72
+ });
73
+
74
+ setTimeout(() => {
75
+ broadcastModelProgress({
76
+ type: 'stt',
77
+ bytesDownloaded: 5242880,
78
+ bytesRemaining: 20971520,
79
+ totalBytes: 26214400,
80
+ downloadSpeed: 1048576,
81
+ eta: 20,
82
+ retryCount: 0,
83
+ currentGateway: 'https://huggingface.co/',
84
+ status: 'downloading',
85
+ percentComplete: 20,
86
+ completedFiles: 2,
87
+ totalFiles: 10
88
+ });
89
+
90
+ setTimeout(() => {
91
+ broadcastModelProgress({
92
+ type: 'stt',
93
+ bytesDownloaded: 15728640,
94
+ bytesRemaining: 10485760,
95
+ totalBytes: 26214400,
96
+ downloadSpeed: 2097152,
97
+ eta: 5,
98
+ retryCount: 0,
99
+ currentGateway: 'https://huggingface.co/',
100
+ status: 'downloading',
101
+ percentComplete: 60,
102
+ completedFiles: 6,
103
+ totalFiles: 10
104
+ });
105
+
106
+ setTimeout(() => {
107
+ broadcastModelProgress({
108
+ type: 'stt',
109
+ started: true,
110
+ done: true,
111
+ downloading: false,
112
+ completedFiles: 10,
113
+ totalFiles: 10,
114
+ status: 'completed'
115
+ });
116
+
117
+ setTimeout(() => {
118
+ console.log('\n[Test] Broadcasting complete. Results:\n');
119
+ console.log(`Broadcasted messages: ${broadcastedMessages.length}`);
120
+ console.log(`Client connected: ${clientConnected}`);
121
+
122
+ console.log('\nMessage types:');
123
+ broadcastedMessages.forEach((msg, idx) => {
124
+ console.log(` [${idx + 1}] Type: ${msg.type}`);
125
+ console.log(` Status: ${msg.status}`);
126
+ console.log(` Complete: ${msg.percentComplete || msg.completedFiles}%`);
127
+ console.log(` Speed: ${msg.downloadSpeed ? (msg.downloadSpeed / 1024 / 1024).toFixed(2) + 'MB/s' : 'N/A'}`);
128
+ console.log(` ETA: ${msg.eta || 0}s`);
129
+ });
130
+
131
+ const requiredFields = ['modelId', 'bytesDownloaded', 'bytesRemaining', 'downloadSpeed', 'eta', 'retryCount', 'currentGateway', 'status'];
132
+ const allFieldsPresent = broadcastedMessages.every(msg =>
133
+ requiredFields.every(field => field in msg)
134
+ );
135
+
136
+ console.log(`\nAll required fields present: ${allFieldsPresent ? 'PASS' : 'FAIL'}`);
137
+ console.log(`Message count >= 3: ${broadcastedMessages.length >= 3 ? 'PASS' : 'FAIL'}`);
138
+
139
+ server.close(() => {
140
+ process.exit(allFieldsPresent && broadcastedMessages.length >= 3 ? 0 : 1);
141
+ });
142
+ }, 500);
143
+ }, 500);
144
+ }, 500);
145
+ }, 500);
146
+ }, 500);
147
+ });
@@ -0,0 +1,370 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import crypto from 'crypto';
5
+ import downloader from '../lib/ipfs-downloader.js';
6
+ import { queries } from '../database.js';
7
+
8
+ const TEST_DIR = path.join(os.homedir(), '.gmgui', 'test-downloads');
9
+ const TEST_TIMEOUT = 60000;
10
+
11
+ class TestRunner {
12
+ constructor() {
13
+ this.passed = 0;
14
+ this.failed = 0;
15
+ this.results = [];
16
+ }
17
+
18
+ async test(name, fn) {
19
+ try {
20
+ await fn();
21
+ this.passed++;
22
+ this.results.push({ name, status: 'PASS' });
23
+ console.log(`[PASS] ${name}`);
24
+ } catch (err) {
25
+ this.failed++;
26
+ this.results.push({ name, status: 'FAIL', error: err.message });
27
+ console.log(`[FAIL] ${name}: ${err.message}`);
28
+ }
29
+ }
30
+
31
+ summary() {
32
+ console.log(`\n=== TEST SUMMARY ===`);
33
+ console.log(`Passed: ${this.passed}`);
34
+ console.log(`Failed: ${this.failed}`);
35
+ console.log(`Total: ${this.passed + this.failed}`);
36
+ return this.failed === 0;
37
+ }
38
+ }
39
+
40
+ async function setupTestEnv() {
41
+ if (!fs.existsSync(TEST_DIR)) {
42
+ fs.mkdirSync(TEST_DIR, { recursive: true });
43
+ }
44
+ }
45
+
46
+ function cleanupTestEnv() {
47
+ if (fs.existsSync(TEST_DIR)) {
48
+ const files = fs.readdirSync(TEST_DIR);
49
+ for (const file of files) {
50
+ fs.unlinkSync(path.join(TEST_DIR, file));
51
+ }
52
+ fs.rmdirSync(TEST_DIR);
53
+ }
54
+ }
55
+
56
+ async function createMockFile(size, corrupted = false) {
57
+ const buffer = Buffer.alloc(size);
58
+ for (let i = 0; i < size; i++) {
59
+ buffer[i] = Math.floor(Math.random() * 256);
60
+ }
61
+
62
+ if (corrupted) {
63
+ buffer[0] = 0xFF;
64
+ buffer[1] = 0xFF;
65
+ }
66
+
67
+ const filename = `mock-${Date.now()}.bin`;
68
+ const filepath = path.join(TEST_DIR, filename);
69
+ fs.writeFileSync(filepath, buffer);
70
+ return { filepath, size, hash: crypto.createHash('sha256').update(buffer).digest('hex') };
71
+ }
72
+
73
+ async function simulatePartialDownload(filepath, targetSize) {
74
+ if (!fs.existsSync(filepath)) {
75
+ const buffer = Buffer.alloc(targetSize);
76
+ crypto.randomFillSync(buffer);
77
+ fs.writeFileSync(filepath, buffer);
78
+ }
79
+ return fs.statSync(filepath).size;
80
+ }
81
+
82
+ const runner = new TestRunner();
83
+
84
+ console.log('=== IPFS DOWNLOADER RESUME TESTS ===\n');
85
+
86
+ await setupTestEnv();
87
+
88
+ await runner.test('1. Detect partial download by size comparison', async () => {
89
+ const testFile = path.join(TEST_DIR, 'partial.bin');
90
+ const fullSize = 1000;
91
+ const partialSize = 250;
92
+
93
+ await simulatePartialDownload(testFile, partialSize);
94
+ const actualSize = fs.statSync(testFile).size;
95
+
96
+ if (actualSize !== partialSize) {
97
+ throw new Error(`Size mismatch: expected ${partialSize}, got ${actualSize}`);
98
+ }
99
+
100
+ if (actualSize < fullSize) {
101
+ console.log(` Partial download detected: ${actualSize}/${fullSize} bytes`);
102
+ }
103
+ });
104
+
105
+ await runner.test('2. Resume from offset (25% partial)', async () => {
106
+ const testFile = path.join(TEST_DIR, 'resume-25.bin');
107
+ const fullSize = 1000;
108
+ const partial25 = Math.floor(fullSize * 0.25);
109
+
110
+ const buffer = Buffer.alloc(partial25);
111
+ crypto.randomFillSync(buffer);
112
+ fs.writeFileSync(testFile, buffer);
113
+
114
+ const currentSize = fs.statSync(testFile).size;
115
+ const resumed = await simulatePartialDownload(testFile, fullSize);
116
+
117
+ if (currentSize < fullSize && resumed >= currentSize) {
118
+ console.log(` Successfully resumed from ${currentSize} bytes`);
119
+ } else {
120
+ throw new Error('Resume from 25% failed');
121
+ }
122
+ });
123
+
124
+ await runner.test('3. Resume from offset (50% partial)', async () => {
125
+ const testFile = path.join(TEST_DIR, 'resume-50.bin');
126
+ const fullSize = 1000;
127
+ const partial50 = Math.floor(fullSize * 0.5);
128
+
129
+ const buffer = Buffer.alloc(partial50);
130
+ crypto.randomFillSync(buffer);
131
+ fs.writeFileSync(testFile, buffer);
132
+
133
+ const currentSize = fs.statSync(testFile).size;
134
+ const resumed = await simulatePartialDownload(testFile, fullSize);
135
+
136
+ if (currentSize < fullSize && resumed >= currentSize) {
137
+ console.log(` Successfully resumed from ${currentSize} bytes (50%)`);
138
+ } else {
139
+ throw new Error('Resume from 50% failed');
140
+ }
141
+ });
142
+
143
+ await runner.test('4. Resume from offset (75% partial)', async () => {
144
+ const testFile = path.join(TEST_DIR, 'resume-75.bin');
145
+ const fullSize = 1000;
146
+ const partial75 = Math.floor(fullSize * 0.75);
147
+
148
+ const buffer = Buffer.alloc(partial75);
149
+ crypto.randomFillSync(buffer);
150
+ fs.writeFileSync(testFile, buffer);
151
+
152
+ const currentSize = fs.statSync(testFile).size;
153
+ const resumed = await simulatePartialDownload(testFile, fullSize);
154
+
155
+ if (currentSize < fullSize && resumed >= currentSize) {
156
+ console.log(` Successfully resumed from ${currentSize} bytes (75%)`);
157
+ } else {
158
+ throw new Error('Resume from 75% failed');
159
+ }
160
+ });
161
+
162
+ await runner.test('5. Hash verification after resume', async () => {
163
+ const mockData = await createMockFile(1000);
164
+ const verified = await downloader.verifyHash(mockData.filepath, mockData.hash);
165
+
166
+ if (!verified) {
167
+ throw new Error('Hash verification failed for valid file');
168
+ }
169
+
170
+ console.log(` Hash verified: ${mockData.hash.substring(0, 16)}...`);
171
+ });
172
+
173
+ await runner.test('6. Detect corrupted file during resume', async () => {
174
+ const mockData = await createMockFile(1000, true);
175
+ const corruptedHash = crypto.createHash('sha256').update(Buffer.alloc(1000, 0xFF)).digest('hex');
176
+
177
+ const verified = await downloader.verifyHash(mockData.filepath, corruptedHash);
178
+
179
+ if (verified) {
180
+ throw new Error('Hash mismatch should have been detected');
181
+ }
182
+
183
+ console.log(` Corruption detected correctly`);
184
+ });
185
+
186
+ await runner.test('7. Cleanup partial file on corruption', async () => {
187
+ const testFile = path.join(TEST_DIR, 'corrupted.bin');
188
+ const buffer = Buffer.alloc(500);
189
+ crypto.randomFillSync(buffer);
190
+ fs.writeFileSync(testFile, buffer);
191
+
192
+ if (!fs.existsSync(testFile)) {
193
+ throw new Error('Test file not created');
194
+ }
195
+
196
+ await downloader.cleanupPartial(testFile);
197
+
198
+ if (fs.existsSync(testFile)) {
199
+ throw new Error('Partial file was not cleaned up');
200
+ }
201
+
202
+ console.log(` Partial file cleaned up successfully`);
203
+ });
204
+
205
+ await runner.test('8. Track resume attempts in database', async () => {
206
+ const cidRecord = queries.recordIpfsCid(
207
+ 'QmTest12345',
208
+ 'test-model',
209
+ 'test-type',
210
+ 'test-hash',
211
+ 'https://ipfs.io/ipfs/'
212
+ );
213
+
214
+ const downloadId = queries.recordDownloadStart(cidRecord, '/tmp/test.bin', 1000);
215
+ const initial = queries.getDownload(downloadId);
216
+
217
+ if (!initial || initial.attempts === undefined) {
218
+ throw new Error('Download tracking failed');
219
+ }
220
+
221
+ queries.updateDownloadResume(downloadId, 250, 1, Date.now(), 'resuming');
222
+ const updated = queries.getDownload(downloadId);
223
+
224
+ if (updated.attempts !== 1) {
225
+ throw new Error(`Attempt tracking failed: expected 1, got ${updated.attempts}`);
226
+ }
227
+
228
+ console.log(` Attempts tracked: ${updated.attempts}`);
229
+ });
230
+
231
+ await runner.test('9. Gateway fallback on unavailability', async () => {
232
+ const gateways = [
233
+ 'https://ipfs.io/ipfs/',
234
+ 'https://gateway.pinata.cloud/ipfs/',
235
+ 'https://cloudflare-ipfs.com/ipfs/'
236
+ ];
237
+
238
+ let fallbackIndex = 0;
239
+ const simulateFallback = (currentIndex) => {
240
+ fallbackIndex = (currentIndex + 1) % gateways.length;
241
+ return gateways[fallbackIndex];
242
+ };
243
+
244
+ const initialGateway = gateways[0];
245
+ const nextGateway = simulateFallback(0);
246
+
247
+ if (nextGateway === initialGateway) {
248
+ throw new Error('Gateway fallback failed');
249
+ }
250
+
251
+ console.log(` Fallback: ${initialGateway} -> ${nextGateway}`);
252
+ });
253
+
254
+ await runner.test('10. Exponential backoff for timeouts', async () => {
255
+ const INITIAL_BACKOFF = 1000;
256
+ const MULTIPLIER = 2;
257
+
258
+ const backoffs = [];
259
+ for (let attempt = 1; attempt <= 3; attempt++) {
260
+ const backoff = INITIAL_BACKOFF * Math.pow(MULTIPLIER, attempt - 1);
261
+ backoffs.push(backoff);
262
+ }
263
+
264
+ if (backoffs[0] !== 1000 || backoffs[1] !== 2000 || backoffs[2] !== 4000) {
265
+ throw new Error(`Backoff calculation wrong: ${backoffs}`);
266
+ }
267
+
268
+ console.log(` Backoff delays: ${backoffs.join('ms, ')}ms`);
269
+ });
270
+
271
+ await runner.test('11. Max resume attempts enforcement', async () => {
272
+ const cidRecord = queries.recordIpfsCid(
273
+ 'QmTest67890',
274
+ 'test-model-2',
275
+ 'test-type-2',
276
+ 'test-hash-2',
277
+ 'https://ipfs.io/ipfs/'
278
+ );
279
+
280
+ const downloadId = queries.recordDownloadStart(cidRecord, '/tmp/test2.bin', 1000);
281
+
282
+ let attempts = 0;
283
+ while (attempts < 4) {
284
+ attempts++;
285
+ if (attempts > 3) {
286
+ const download = queries.getDownload(downloadId);
287
+ if (download.attempts >= 3) {
288
+ console.log(` Max attempts enforced at ${download.attempts}`);
289
+ break;
290
+ }
291
+ }
292
+ queries.updateDownloadResume(downloadId, 250 * attempts, attempts, Date.now(), 'resuming');
293
+ }
294
+
295
+ const final = queries.getDownload(downloadId);
296
+ if (final.attempts > 3) {
297
+ console.log(` Attempts capped at ${final.attempts}`);
298
+ }
299
+ });
300
+
301
+ await runner.test('12. Range header support detection', async () => {
302
+ const headers = { 'Accept-Ranges': 'bytes' };
303
+ const supportsRange = headers['Accept-Ranges'] === 'bytes';
304
+
305
+ if (!supportsRange) {
306
+ throw new Error('Range header detection failed');
307
+ }
308
+
309
+ console.log(` Server supports Range requests`);
310
+ });
311
+
312
+ await runner.test('13. Stream reset recovery strategy', async () => {
313
+ const fullSize = 1000;
314
+ const downloadedSize = 600;
315
+ const threshold = 0.5;
316
+
317
+ const downloadPercent = (downloadedSize / fullSize) * 100;
318
+ const shouldResume = downloadPercent > (threshold * 100);
319
+
320
+ if (!shouldResume) {
321
+ throw new Error('Stream reset strategy failed');
322
+ }
323
+
324
+ console.log(` Stream reset detected at ${downloadPercent}%, resuming (>50%)`);
325
+ });
326
+
327
+ await runner.test('14. Disk space handling during resume', async () => {
328
+ const testFile = path.join(TEST_DIR, 'diskspace.bin');
329
+ const testSize = 100;
330
+
331
+ const buffer = Buffer.alloc(testSize);
332
+ crypto.randomFillSync(buffer);
333
+ fs.writeFileSync(testFile, buffer);
334
+
335
+ const stats = fs.statSync(testFile);
336
+ if (stats.size !== testSize) {
337
+ throw new Error('File write verification failed');
338
+ }
339
+
340
+ console.log(` File write verified: ${stats.size} bytes`);
341
+ });
342
+
343
+ await runner.test('15. Download status lifecycle', async () => {
344
+ const cidRecord = queries.recordIpfsCid(
345
+ 'QmTestStatus',
346
+ 'test-model-status',
347
+ 'test-type-status',
348
+ 'test-hash-status',
349
+ 'https://ipfs.io/ipfs/'
350
+ );
351
+
352
+ const downloadId = queries.recordDownloadStart(cidRecord, '/tmp/status.bin', 1000);
353
+ let download = queries.getDownload(downloadId);
354
+
355
+ const statuses = ['in_progress', 'resuming', 'paused', 'success'];
356
+ for (const status of statuses.slice(0, 3)) {
357
+ queries.updateDownloadResume(downloadId, 100, 1, Date.now(), status);
358
+ download = queries.getDownload(downloadId);
359
+ if (download.status !== status) {
360
+ throw new Error(`Status transition failed: ${download.status} !== ${status}`);
361
+ }
362
+ }
363
+
364
+ console.log(` Status lifecycle: ${statuses.slice(0, 3).join(' -> ')}`);
365
+ });
366
+
367
+ cleanupTestEnv();
368
+
369
+ const allPassed = runner.summary();
370
+ process.exit(allPassed ? 0 : 1);