agentgui 1.0.454 → 1.0.455

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.
@@ -6,11 +6,6 @@ import fs from 'fs';
6
6
  import fetch from 'node-fetch';
7
7
 
8
8
  const isWindows = os.platform() === 'win32';
9
- const INSTALL_TIMEOUT_MS = 300000;
10
- const VERSION_TIMEOUT_MS = 3000;
11
- const REGISTRY_TIMEOUT_MS = 5000;
12
- const VERSION_CACHE_MS = 3600000;
13
-
14
9
  const TOOLS = [
15
10
  { id: 'gm-oc', name: 'OpenCode', pkg: 'gm-oc', binary: 'opencode', marker: path.join(os.homedir(), '.config', 'opencode', 'agents') },
16
11
  { id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc', binary: 'gemini', marker: path.join(os.homedir(), '.gemini', 'extensions', 'gm', 'agents') },
@@ -21,265 +16,73 @@ const TOOLS = [
21
16
  const versionCache = new Map();
22
17
  const installLocks = new Map();
23
18
 
24
- function log(msg) { console.log('[TOOL-MANAGER] ' + msg); }
25
-
26
- function getTool(toolId) {
27
- return TOOLS.find(t => t.id === toolId);
28
- }
19
+ const getTool = (id) => TOOLS.find(t => t.id === id);
20
+ const isInstalled = (tool) => {
21
+ const ext = isWindows ? '.cmd' : '';
22
+ if (fs.existsSync(path.join(process.cwd(), 'node_modules', '.bin', tool.binary + ext))) return true;
23
+ try { execSync(`${isWindows ? 'where' : 'which'} ${tool.binary}`, { stdio: 'pipe', timeout: 2000 }); return true; } catch (_) { return false; }
24
+ };
25
+ const detectVersion = (binary) => { try { const o = execSync(`${binary} --version 2>&1 || ${binary} -v 2>&1`, { timeout: 3000, encoding: 'utf8', stdio: 'pipe' }); return o.match(/(\d+\.\d+\.\d+)/)?.[1] || null; } catch (_) { return null; } };
26
+ const cmpVer = (v1, v2) => { const [a,b] = [v1?.split('.')?.map(Number) || [], v2?.split('.')?.map(Number) || []]; for(let i=0;i<3;i++) { const n1=a[i]||0, n2=b[i]||0; if(n1<n2)return true; if(n1>n2)return false; } return false; };
29
27
 
30
28
  export function checkToolStatus(toolId) {
31
29
  const tool = getTool(toolId);
32
30
  if (!tool) return null;
33
-
34
- const timestamp = Date.now();
35
- let installed = false;
36
- let hasConfig = false;
37
- let version = null;
38
-
39
- const ext = isWindows ? '.cmd' : '';
40
- const localBin = path.join(process.cwd(), 'node_modules', '.bin', tool.binary + ext);
41
- if (fs.existsSync(localBin)) {
42
- installed = true;
43
- } else {
44
- try {
45
- const which = isWindows ? 'where' : 'which';
46
- execSync(`${which} ${tool.binary}`, { stdio: 'pipe', timeout: 2000 });
47
- installed = true;
48
- } catch (_) {
49
- installed = false;
50
- }
51
- }
52
-
53
- if (fs.existsSync(tool.marker)) {
54
- hasConfig = true;
55
- }
56
-
57
- if (installed && !hasConfig) {
58
- installed = false;
59
- }
60
-
61
- if (installed) {
62
- version = detectVersionSync(tool);
63
- }
64
-
65
- return { toolId, installed, version, hasConfig, timestamp };
66
- }
67
-
68
- function detectVersionSync(tool) {
69
- try {
70
- const output = execSync(`${tool.binary} --version 2>&1 || ${tool.binary} -v 2>&1`, {
71
- timeout: VERSION_TIMEOUT_MS,
72
- encoding: 'utf8',
73
- stdio: 'pipe'
74
- });
75
- const match = output.match(/(\d+\.\d+\.\d+)/);
76
- return match ? match[1] : null;
77
- } catch (_) {
78
- return null;
79
- }
31
+ const installed = isInstalled(tool) && fs.existsSync(tool.marker);
32
+ const version = installed ? detectVersion(tool.binary) : null;
33
+ return { toolId, installed, version, timestamp: Date.now() };
80
34
  }
81
35
 
82
36
  export async function checkForUpdates(toolId, currentVersion) {
37
+ if (!currentVersion) return { hasUpdate: false, latestVersion: null };
83
38
  const tool = getTool(toolId);
84
- if (!tool || !currentVersion) return { hasUpdate: false, latestVersion: null };
39
+ if (!tool) return { hasUpdate: false, latestVersion: null };
85
40
 
86
41
  try {
87
42
  const cached = versionCache.get(toolId);
88
- if (cached && Date.now() - cached.timestamp < VERSION_CACHE_MS) {
89
- return compareVersions(currentVersion, cached.version) ? { hasUpdate: true, latestVersion: cached.version } : { hasUpdate: false };
90
- }
91
-
92
- const response = await fetch(`https://registry.npmjs.org/${tool.pkg}`, {
93
- timeout: REGISTRY_TIMEOUT_MS,
94
- headers: { 'Accept': 'application/json' }
95
- });
96
-
97
- if (!response.ok) return { hasUpdate: false, latestVersion: null };
98
-
99
- const data = await response.json();
100
- const latestVersion = data['dist-tags']?.latest;
43
+ if (cached && Date.now() - cached.timestamp < 3600000) return { hasUpdate: cmpVer(currentVersion, cached.version), latestVersion: cached.version };
101
44
 
102
- if (latestVersion) {
103
- versionCache.set(toolId, { version: latestVersion, timestamp: Date.now() });
104
- return { hasUpdate: compareVersions(currentVersion, latestVersion), latestVersion };
105
- }
45
+ const res = await fetch(`https://registry.npmjs.org/${tool.pkg}`, { timeout: 5000, headers: { 'Accept': 'application/json' } });
46
+ if (!res.ok) return { hasUpdate: false, latestVersion: null };
106
47
 
48
+ const data = await res.json();
49
+ const latest = data['dist-tags']?.latest;
50
+ if (latest) { versionCache.set(toolId, { version: latest, timestamp: Date.now() }); return { hasUpdate: cmpVer(currentVersion, latest), latestVersion: latest }; }
107
51
  return { hasUpdate: false, latestVersion: null };
108
- } catch (_) {
109
- return { hasUpdate: false, latestVersion: null };
110
- }
52
+ } catch (_) { return { hasUpdate: false, latestVersion: null }; }
111
53
  }
112
54
 
113
- function compareVersions(v1, v2) {
114
- const p1 = v1.split('.').map(Number);
115
- const p2 = v2.split('.').map(Number);
116
- for (let i = 0; i < 3; i++) {
117
- const n1 = p1[i] || 0;
118
- const n2 = p2[i] || 0;
119
- if (n1 > n2) return false;
120
- if (n1 < n2) return true;
121
- }
122
- return false;
123
- }
55
+ const spawnProc = (toolId, tool, pkg, onProgress) => new Promise((resolve) => {
56
+ const proc = spawn(isWindows ? 'npx.cmd' : 'npx', ['--yes', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
57
+ let completed = false, stderr = '';
58
+ const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); }}, 300000);
59
+ proc.stdout.on('data', (d) => { if (onProgress) onProgress({ type: 'progress', data: d.toString() }); });
60
+ proc.stderr.on('data', (d) => { stderr += d.toString(); if (onProgress) onProgress({ type: 'error', data: d.toString() }); });
61
+ proc.on('close', (code) => { clearTimeout(timer); if (completed) return; completed = true; if (code === 0) { const s = checkToolStatus(toolId); resolve(s?.installed ? { success: true, error: null, version: s.version } : { success: false, error: 'Tool not detected' }); } else { resolve({ success: false, error: stderr.substring(0, 1000) || 'Failed' }); } });
62
+ proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); }});
63
+ });
124
64
 
125
65
  export async function install(toolId, onProgress) {
126
66
  const tool = getTool(toolId);
127
67
  if (!tool) return { success: false, error: 'Tool not found' };
128
-
129
- if (installLocks.get(toolId)) {
130
- return { success: false, error: 'Install already in progress' };
131
- }
132
-
68
+ if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
133
69
  installLocks.set(toolId, true);
134
-
135
- try {
136
- return new Promise((resolve) => {
137
- const npxCmd = isWindows ? 'npx.cmd' : 'npx';
138
- const proc = spawn(npxCmd, ['--yes', tool.pkg], {
139
- stdio: ['pipe', 'pipe', 'pipe'],
140
- timeout: INSTALL_TIMEOUT_MS,
141
- shell: isWindows
142
- });
143
-
144
- let stdout = '';
145
- let stderr = '';
146
- let completed = false;
147
-
148
- const timer = setTimeout(() => {
149
- if (!completed) {
150
- completed = true;
151
- try { proc.kill('SIGKILL'); } catch (_) {}
152
- resolve({ success: false, error: 'Installation timeout (5 minutes)' });
153
- }
154
- }, INSTALL_TIMEOUT_MS);
155
-
156
- proc.stdout.on('data', (d) => {
157
- stdout += d.toString();
158
- if (onProgress) onProgress({ type: 'progress', data: d.toString() });
159
- });
160
-
161
- proc.stderr.on('data', (d) => {
162
- stderr += d.toString();
163
- if (onProgress) onProgress({ type: 'error', data: d.toString() });
164
- });
165
-
166
- proc.on('close', (code) => {
167
- clearTimeout(timer);
168
- if (completed) return;
169
- completed = true;
170
-
171
- if (code === 0) {
172
- const status = checkToolStatus(toolId);
173
- if (status && status.installed) {
174
- resolve({ success: true, error: null, version: status.version });
175
- } else {
176
- resolve({ success: false, error: 'Install completed but tool not detected' });
177
- }
178
- } else {
179
- const error = stderr.substring(0, 1000) || 'Installation failed';
180
- resolve({ success: false, error });
181
- }
182
- });
183
-
184
- proc.on('error', (err) => {
185
- clearTimeout(timer);
186
- if (completed) return;
187
- completed = true;
188
- resolve({ success: false, error: err.message });
189
- });
190
- });
191
- } finally {
192
- installLocks.delete(toolId);
193
- }
70
+ try { return await spawnProc(toolId, tool, tool.pkg, onProgress); } finally { installLocks.delete(toolId); }
194
71
  }
195
72
 
196
73
  export async function update(toolId, targetVersion, onProgress) {
197
74
  const tool = getTool(toolId);
198
75
  if (!tool) return { success: false, error: 'Tool not found' };
199
-
200
76
  const current = checkToolStatus(toolId);
201
- if (!current || !current.installed) {
202
- return { success: false, error: 'Tool not installed' };
203
- }
77
+ if (!current?.installed) return { success: false, error: 'Tool not installed' };
78
+ if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
204
79
 
205
- if (installLocks.get(toolId)) {
206
- return { success: false, error: 'Install already in progress' };
207
- }
208
-
209
- const target = targetVersion || await checkForUpdates(toolId, current.version).then(r => r.latestVersion);
210
- if (!target) {
211
- return { success: false, error: 'Unable to determine target version' };
212
- }
80
+ const target = targetVersion || (await checkForUpdates(toolId, current.version)).latestVersion;
81
+ if (!target) return { success: false, error: 'Unable to determine target version' };
213
82
 
214
83
  installLocks.set(toolId, true);
215
-
216
- try {
217
- return new Promise((resolve) => {
218
- const npxCmd = isWindows ? 'npx.cmd' : 'npx';
219
- const pkg = `${tool.pkg}@${target}`;
220
- const proc = spawn(npxCmd, ['--yes', pkg], {
221
- stdio: ['pipe', 'pipe', 'pipe'],
222
- timeout: INSTALL_TIMEOUT_MS,
223
- shell: isWindows
224
- });
225
-
226
- let stderr = '';
227
- let completed = false;
228
-
229
- const timer = setTimeout(() => {
230
- if (!completed) {
231
- completed = true;
232
- try { proc.kill('SIGKILL'); } catch (_) {}
233
- resolve({ success: false, error: 'Update timeout (5 minutes)' });
234
- }
235
- }, INSTALL_TIMEOUT_MS);
236
-
237
- proc.stdout.on('data', (d) => {
238
- if (onProgress) onProgress({ type: 'progress', data: d.toString() });
239
- });
240
-
241
- proc.stderr.on('data', (d) => {
242
- stderr += d.toString();
243
- if (onProgress) onProgress({ type: 'error', data: d.toString() });
244
- });
245
-
246
- proc.on('close', (code) => {
247
- clearTimeout(timer);
248
- if (completed) return;
249
- completed = true;
250
-
251
- if (code === 0) {
252
- const status = checkToolStatus(toolId);
253
- if (status && status.installed) {
254
- resolve({ success: true, error: null, version: status.version });
255
- } else {
256
- resolve({ success: false, error: 'Update completed but tool not detected' });
257
- }
258
- } else {
259
- const error = stderr.substring(0, 1000) || 'Update failed';
260
- resolve({ success: false, error });
261
- }
262
- });
263
-
264
- proc.on('error', (err) => {
265
- clearTimeout(timer);
266
- if (completed) return;
267
- completed = true;
268
- resolve({ success: false, error: err.message });
269
- });
270
- });
271
- } finally {
272
- installLocks.delete(toolId);
273
- }
84
+ try { return await spawnProc(toolId, tool, `${tool.pkg}@${target}`, onProgress); } finally { installLocks.delete(toolId); }
274
85
  }
275
86
 
276
- export function getAllTools() {
277
- return TOOLS.map(tool => {
278
- const status = checkToolStatus(tool.id);
279
- return { ...tool, ...status };
280
- });
281
- }
282
-
283
- export function getToolConfig(toolId) {
284
- return getTool(toolId) || null;
285
- }
87
+ export function getAllTools() { return TOOLS.map(tool => ({ ...tool, ...checkToolStatus(tool.id) })); }
88
+ export function getToolConfig(toolId) { return getTool(toolId) || null; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.454",
3
+ "version": "1.0.455",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/static/index.html CHANGED
@@ -3035,6 +3035,7 @@
3035
3035
  .toast-error { background: var(--color-error); color: white; }
3036
3036
  .toast-warning { background: var(--color-warning); color: white; }
3037
3037
  </style>
3038
+ <link rel="stylesheet" href="/gm/css/tool-status.css">
3038
3039
  </head>
3039
3040
  <body>
3040
3041
  <!-- Sidebar overlay (mobile) -->
@@ -1,323 +1,48 @@
1
1
  const ToolStatusComponent = {
2
- state: {
3
- tools: [],
4
- expandedTool: null,
5
- refreshing: false
6
- },
2
+ state: { tools: [], refreshing: false },
7
3
 
8
4
  init() {
9
5
  this.loadTools();
10
6
  if (window.wsManager) {
11
- window.wsManager.on('tool_install_started', (data) => this.handleInstallStarted(data));
12
- window.wsManager.on('tool_install_progress', (data) => this.handleProgress(data));
13
- window.wsManager.on('tool_install_complete', (data) => this.handleInstallComplete(data));
14
- window.wsManager.on('tool_install_failed', (data) => this.handleInstallFailed(data));
15
- window.wsManager.on('tool_update_complete', (data) => this.handleUpdateComplete(data));
16
- window.wsManager.on('tool_update_failed', (data) => this.handleUpdateFailed(data));
17
- window.wsManager.on('tools_refresh_complete', (data) => this.handleRefreshComplete(data));
7
+ const events = ['tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed', 'tool_update_complete', 'tool_update_failed', 'tools_refresh_complete'];
8
+ events.forEach(e => window.wsManager.on(e, (d) => this.handleEvent(e, d)));
18
9
  }
19
10
  },
20
11
 
21
12
  async loadTools() {
22
- try {
23
- const res = await fetch('/gm/api/tools');
24
- const data = await res.json();
25
- this.state.tools = data.tools || [];
26
- this.render();
27
- } catch (e) {
28
- console.error('[TOOL-STATUS] Load failed:', e.message);
29
- }
30
- },
31
-
32
- async installTool(toolId) {
33
- try {
34
- const res = await fetch(`/gm/api/tools/${toolId}/install`, { method: 'POST' });
35
- const data = await res.json();
36
- if (data.success) {
37
- const tool = this.state.tools.find(t => t.id === toolId);
38
- if (tool) tool.status = 'installing';
39
- this.render();
40
- } else {
41
- alert('Install failed: ' + (data.error || 'Unknown error'));
42
- }
43
- } catch (e) {
44
- alert('Install failed: ' + e.message);
45
- }
46
- },
47
-
48
- async updateTool(toolId) {
49
- try {
50
- const res = await fetch(`/gm/api/tools/${toolId}/update`, { method: 'POST', body: JSON.stringify({}) });
51
- const data = await res.json();
52
- if (data.success) {
53
- const tool = this.state.tools.find(t => t.id === toolId);
54
- if (tool) tool.status = 'updating';
55
- this.render();
56
- } else {
57
- alert('Update failed: ' + (data.error || 'Unknown error'));
58
- }
59
- } catch (e) {
60
- alert('Update failed: ' + e.message);
61
- }
62
- },
63
-
64
- async refreshTools() {
65
- this.state.refreshing = true;
66
- this.render();
67
- try {
68
- await fetch('/gm/api/tools/refresh-all', { method: 'POST' });
69
- } catch (e) {
70
- console.error('[TOOL-STATUS] Refresh failed:', e.message);
71
- }
13
+ try { const res = await fetch('/gm/api/tools'); this.state.tools = (await res.json()).tools || []; this.render(); } catch (e) { console.error('[TOOL-STATUS]', e.message); }
72
14
  },
73
15
 
74
- handleInstallStarted(data) {
75
- const tool = this.state.tools.find(t => t.id === data.toolId);
76
- if (tool) {
77
- tool.status = 'installing';
78
- tool.progress = 0;
79
- this.render();
80
- }
16
+ async act(action, toolId) {
17
+ try { const res = await fetch(`/gm/api/tools/${toolId}/${action}`, { method: 'POST' }); const data = await res.json(); if (!data.success) alert(`${action} failed: ${data.error || 'Unknown error'}`); } catch (e) { alert(`${action} failed: ${e.message}`); }
81
18
  },
82
19
 
83
- handleProgress(data) {
84
- const tool = this.state.tools.find(t => t.id === data.toolId);
85
- if (tool) {
86
- tool.progress = Math.min((tool.progress || 0) + 5, 90);
87
- this.render();
88
- }
89
- },
20
+ async refreshTools() { this.state.refreshing = true; this.render(); try { await fetch('/gm/api/tools/refresh-all', { method: 'POST' }); } catch (e) { console.error('[TOOL-STATUS]', e.message); } },
90
21
 
91
- handleInstallComplete(data) {
22
+ handleEvent(event, data) {
92
23
  const tool = this.state.tools.find(t => t.id === data.toolId);
93
- if (tool) {
94
- tool.status = 'installed';
95
- tool.version = data.data.version;
96
- tool.progress = 100;
97
- this.render();
98
- setTimeout(() => this.loadTools(), 1000);
99
- }
100
- },
101
-
102
- handleInstallFailed(data) {
103
- const tool = this.state.tools.find(t => t.id === data.toolId);
104
- if (tool) {
105
- tool.status = 'failed';
106
- tool.error_message = data.data.error;
107
- tool.progress = 0;
108
- this.render();
109
- }
110
- },
111
-
112
- handleUpdateComplete(data) {
113
- const tool = this.state.tools.find(t => t.id === data.toolId);
114
- if (tool) {
115
- tool.status = 'installed';
116
- tool.version = data.data.version;
117
- tool.hasUpdate = false;
118
- tool.progress = 100;
119
- this.render();
120
- setTimeout(() => this.loadTools(), 1000);
121
- }
122
- },
123
-
124
- handleUpdateFailed(data) {
125
- const tool = this.state.tools.find(t => t.id === data.toolId);
126
- if (tool) {
127
- tool.status = 'failed';
128
- tool.error_message = data.data.error;
129
- this.render();
130
- }
24
+ if (!tool) return;
25
+ if (event === 'tool_install_started') { tool.status = 'installing'; tool.progress = 0; }
26
+ else if (event === 'tool_install_progress') { tool.progress = Math.min((tool.progress || 0) + 5, 90); }
27
+ else if (event.includes('_complete')) { tool.status = 'installed'; tool.version = data.data.version; tool.hasUpdate = false; tool.progress = 100; setTimeout(() => this.loadTools(), 1000); }
28
+ else if (event.includes('_failed')) { tool.status = 'failed'; tool.error_message = data.data.error; tool.progress = 0; }
29
+ else if (event === 'tools_refresh_complete') { this.state.refreshing = false; this.loadTools(); return; }
30
+ this.render();
131
31
  },
132
32
 
133
- handleRefreshComplete(data) {
134
- this.state.refreshing = false;
135
- this.loadTools();
136
- },
33
+ getStatusColor(tool) { return tool.status === 'installed' && !tool.hasUpdate ? '#4CAF50' : tool.status === 'installed' && tool.hasUpdate ? '#FFC107' : ['installing', 'updating'].includes(tool.status) ? '#2196F3' : tool.status === 'failed' ? '#F44336' : '#9E9E9E'; },
137
34
 
138
- getStatusColor(tool) {
139
- if (tool.status === 'installed' && !tool.hasUpdate) return '#4CAF50';
140
- if (tool.status === 'installed' && tool.hasUpdate) return '#FFC107';
141
- if (tool.status === 'installing' || tool.status === 'updating') return '#2196F3';
142
- if (tool.status === 'failed') return '#F44336';
143
- return '#9E9E9E';
144
- },
145
-
146
- getStatusText(tool) {
147
- if (tool.status === 'installed') {
148
- if (tool.hasUpdate) return `Update available (v${tool.latestVersion})`;
149
- return `Installed v${tool.version || '?'}`;
150
- }
151
- if (tool.status === 'installing') return 'Installing...';
152
- if (tool.status === 'updating') return 'Updating...';
153
- if (tool.status === 'failed') return 'Failed';
154
- return 'Not installed';
155
- },
35
+ getStatusText(tool) { return tool.status === 'installed' ? (tool.hasUpdate ? `Update available (v${tool.latestVersion})` : `Installed v${tool.version || '?'}`) : tool.status === 'installing' ? 'Installing...' : tool.status === 'updating' ? 'Updating...' : tool.status === 'failed' ? 'Failed' : 'Not installed'; },
156
36
 
157
37
  render() {
158
- const container = document.getElementById('tool-status-container');
159
- if (!container) return;
160
-
161
- const html = `
162
- <div class="tool-status-header">
163
- <h3>Tools</h3>
164
- <button onclick="window.toolStatus.refreshTools()" ${this.state.refreshing ? 'disabled' : ''} class="tool-refresh-btn">
165
- ${this.state.refreshing ? 'Refreshing...' : 'Refresh'}
166
- </button>
167
- </div>
168
- <div class="tool-grid">
169
- ${this.state.tools.map(tool => `
170
- <div class="tool-card" style="border-left: 4px solid ${this.getStatusColor(tool)}">
171
- <div class="tool-name">${tool.name || tool.id}</div>
172
- <div class="tool-status">${this.getStatusText(tool)}</div>
173
- ${tool.progress !== undefined && (tool.status === 'installing' || tool.status === 'updating') ? `
174
- <div class="tool-progress">
175
- <div class="progress-bar" style="width: ${tool.progress}%"></div>
176
- </div>
177
- ` : ''}
178
- <div class="tool-actions">
179
- ${!tool.installed ? `
180
- <button onclick="window.toolStatus.installTool('${tool.id}')" class="tool-btn tool-btn-primary">Install</button>
181
- ` : tool.hasUpdate ? `
182
- <button onclick="window.toolStatus.updateTool('${tool.id}')" class="tool-btn tool-btn-primary">Update</button>
183
- ` : `
184
- <button onclick="window.toolStatus.refreshTools()" class="tool-btn tool-btn-secondary">Check Updates</button>
185
- `}
186
- ${tool.status === 'failed' ? `
187
- <button onclick="window.toolStatus.installTool('${tool.id}')" class="tool-btn tool-btn-warning">Retry</button>
188
- ` : ''}
189
- ${tool.error_message ? `
190
- <div class="tool-error" title="${tool.error_message}">
191
- Error: ${tool.error_message.substring(0, 30)}...
192
- <a href="#" onclick="alert('${tool.error_message.replace(/"/g, '\\"')}'); return false;">Details</a>
193
- </div>
194
- ` : ''}
195
- </div>
196
- </div>
197
- `).join('')}
198
- </div>
199
- <style>
200
- #tool-status-container {
201
- padding: 16px;
202
- border: 1px solid #e0e0e0;
203
- border-radius: 4px;
204
- margin-bottom: 16px;
205
- }
206
- .tool-status-header {
207
- display: flex;
208
- justify-content: space-between;
209
- align-items: center;
210
- margin-bottom: 16px;
211
- }
212
- .tool-status-header h3 {
213
- margin: 0;
214
- font-size: 18px;
215
- }
216
- .tool-refresh-btn {
217
- padding: 6px 12px;
218
- background: #2196F3;
219
- color: white;
220
- border: none;
221
- border-radius: 4px;
222
- cursor: pointer;
223
- font-size: 12px;
224
- }
225
- .tool-refresh-btn:disabled {
226
- opacity: 0.6;
227
- cursor: not-allowed;
228
- }
229
- .tool-grid {
230
- display: grid;
231
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
232
- gap: 12px;
233
- }
234
- .tool-card {
235
- padding: 12px;
236
- border: 1px solid #ddd;
237
- border-radius: 4px;
238
- background: #f9f9f9;
239
- }
240
- .tool-name {
241
- font-weight: bold;
242
- margin-bottom: 4px;
243
- font-size: 14px;
244
- }
245
- .tool-status {
246
- font-size: 12px;
247
- color: #666;
248
- margin-bottom: 8px;
249
- }
250
- .tool-progress {
251
- width: 100%;
252
- height: 6px;
253
- background: #e0e0e0;
254
- border-radius: 3px;
255
- overflow: hidden;
256
- margin-bottom: 8px;
257
- }
258
- .progress-bar {
259
- height: 100%;
260
- background: #4CAF50;
261
- transition: width 0.3s;
262
- }
263
- .tool-actions {
264
- display: flex;
265
- flex-direction: column;
266
- gap: 6px;
267
- }
268
- .tool-btn {
269
- padding: 6px;
270
- border: none;
271
- border-radius: 3px;
272
- cursor: pointer;
273
- font-size: 12px;
274
- white-space: nowrap;
275
- }
276
- .tool-btn-primary {
277
- background: #2196F3;
278
- color: white;
279
- }
280
- .tool-btn-primary:hover {
281
- background: #1976D2;
282
- }
283
- .tool-btn-secondary {
284
- background: #f0f0f0;
285
- color: #333;
286
- border: 1px solid #ddd;
287
- }
288
- .tool-btn-secondary:hover {
289
- background: #e0e0e0;
290
- }
291
- .tool-btn-warning {
292
- background: #FF9800;
293
- color: white;
294
- }
295
- .tool-btn-warning:hover {
296
- background: #F57C00;
297
- }
298
- .tool-error {
299
- font-size: 11px;
300
- color: #F44336;
301
- padding: 6px;
302
- background: #ffebee;
303
- border-radius: 2px;
304
- margin-top: 4px;
305
- }
306
- .tool-error a {
307
- color: #C62828;
308
- text-decoration: underline;
309
- cursor: pointer;
310
- }
311
- </style>
38
+ const c = document.getElementById('tool-status-container');
39
+ if (!c) return;
40
+ c.innerHTML = `
41
+ <div class="tool-status-header"><h3>Tools</h3><button onclick="window.toolStatus.refreshTools()" ${this.state.refreshing ? 'disabled' : ''} class="tool-refresh-btn">${this.state.refreshing ? 'Refreshing...' : 'Refresh'}</button></div>
42
+ <div class="tool-grid">${this.state.tools.map(t => `<div class="tool-card" style="border-left: 4px solid ${this.getStatusColor(t)}"><div class="tool-name">${t.name || t.id}</div><div class="tool-status">${this.getStatusText(t)}</div>${t.progress !== undefined && ['installing', 'updating'].includes(t.status) ? `<div class="tool-progress"><div class="progress-bar" style="width: ${t.progress}%"></div></div>` : ''}<div class="tool-actions">${!t.installed ? `<button onclick="window.toolStatus.act('install','${t.id}')" class="tool-btn tool-btn-primary">Install</button>` : t.hasUpdate ? `<button onclick="window.toolStatus.act('update','${t.id}')" class="tool-btn tool-btn-primary">Update</button>` : `<button onclick="window.toolStatus.refreshTools()" class="tool-btn tool-btn-secondary">Check Updates</button>`}${t.status === 'failed' ? `<button onclick="window.toolStatus.act('install','${t.id}')" class="tool-btn tool-btn-warning">Retry</button>` : ''}${t.error_message ? `<div class="tool-error" title="${t.error_message}">Error: ${t.error_message.substring(0, 30)}...<a href="#" onclick="alert('${t.error_message.replace(/"/g, '\\"')}'); return false;">Details</a></div>` : ''}</div></div>`).join('')}</div>
312
43
  `;
313
-
314
- container.innerHTML = html;
315
44
  window.toolStatus = this;
316
45
  }
317
46
  };
318
47
 
319
- if (document.readyState === 'loading') {
320
- document.addEventListener('DOMContentLoaded', () => ToolStatusComponent.init());
321
- } else {
322
- ToolStatusComponent.init();
323
- }
48
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ToolStatusComponent.init()); } else { ToolStatusComponent.init(); }
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const http = require('http');
4
-
5
- const BASE_URL = '/gm';
6
- const PORT = 3000;
7
-
8
- function makeRequest(method, path, body = null) {
9
- return new Promise((resolve, reject) => {
10
- const options = {
11
- hostname: 'localhost',
12
- port: PORT,
13
- path: BASE_URL + path,
14
- method: method,
15
- headers: body ? {
16
- 'Content-Type': 'application/json',
17
- 'Content-Length': Buffer.byteLength(JSON.stringify(body))
18
- } : {}
19
- };
20
-
21
- const req = http.request(options, (res) => {
22
- let data = '';
23
- res.on('data', (chunk) => { data += chunk; });
24
- res.on('end', () => {
25
- try {
26
- resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null, raw: data });
27
- } catch {
28
- resolve({ status: res.statusCode, data: null, raw: data });
29
- }
30
- });
31
- });
32
-
33
- req.on('error', reject);
34
-
35
- if (body) {
36
- req.write(JSON.stringify(body));
37
- }
38
- req.end();
39
- });
40
- }
41
-
42
- async function runTests() {
43
- console.log('Testing ACP Agents & Stateless Runs Endpoints\n');
44
-
45
- const tests = [
46
- {
47
- name: 'POST /api/agents/search - empty search',
48
- test: async () => {
49
- const res = await makeRequest('POST', '/api/agents/search', {});
50
- return res.status === 200 && res.data.agents !== undefined;
51
- }
52
- },
53
- {
54
- name: 'POST /api/agents/search - search by name',
55
- test: async () => {
56
- const res = await makeRequest('POST', '/api/agents/search', { name: 'Claude' });
57
- return res.status === 200 && Array.isArray(res.data.agents);
58
- }
59
- },
60
- {
61
- name: 'GET /api/agents/claude-code',
62
- test: async () => {
63
- const res = await makeRequest('GET', '/api/agents/claude-code');
64
- return res.status === 200 || res.status === 404;
65
- }
66
- },
67
- {
68
- name: 'GET /api/agents/claude-code/descriptor',
69
- test: async () => {
70
- const res = await makeRequest('GET', '/api/agents/claude-code/descriptor');
71
- return (res.status === 200 && res.data.metadata && res.data.specs) || res.status === 404;
72
- }
73
- },
74
- {
75
- name: 'POST /api/runs/search',
76
- test: async () => {
77
- const res = await makeRequest('POST', '/api/runs/search', {});
78
- return res.status === 200 && res.data.runs !== undefined;
79
- }
80
- },
81
- {
82
- name: 'POST /api/runs - missing agent_id',
83
- test: async () => {
84
- const res = await makeRequest('POST', '/api/runs', {});
85
- return res.status === 422;
86
- }
87
- }
88
- ];
89
-
90
- let passed = 0;
91
- let failed = 0;
92
-
93
- for (const t of tests) {
94
- try {
95
- const success = await t.test();
96
- if (success) {
97
- console.log(`✓ ${t.name}`);
98
- passed++;
99
- } else {
100
- console.log(`✗ ${t.name}`);
101
- failed++;
102
- }
103
- } catch (err) {
104
- console.log(`✗ ${t.name} - ${err.message}`);
105
- failed++;
106
- }
107
- }
108
-
109
- console.log(`\nResults: ${passed} passed, ${failed} failed`);
110
- process.exit(failed > 0 ? 1 : 0);
111
- }
112
-
113
- http.get(`http://localhost:${PORT}${BASE_URL}/`, (res) => {
114
- console.log('Server is running\n');
115
- runTests();
116
- }).on('error', () => {
117
- console.log('Server is not running. Please start with: npm run dev');
118
- process.exit(1);
119
- });
package/test-wave4-ui.mjs DELETED
@@ -1,141 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Wave 4 UI Consistency Test
4
- * Tests agent/model persistence and display consolidation
5
- */
6
-
7
- import http from 'http';
8
-
9
- const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
10
- const API_BASE = `${BASE_URL}/gm/api`;
11
-
12
- function request(method, path, body = null) {
13
- return new Promise((resolve, reject) => {
14
- const url = new URL(path, API_BASE);
15
- const options = {
16
- method,
17
- headers: body ? { 'Content-Type': 'application/json' } : {}
18
- };
19
-
20
- const req = http.request(url, options, (res) => {
21
- let data = '';
22
- res.on('data', chunk => data += chunk);
23
- res.on('end', () => {
24
- try {
25
- resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null });
26
- } catch (e) {
27
- resolve({ status: res.statusCode, data });
28
- }
29
- });
30
- });
31
-
32
- req.on('error', reject);
33
- if (body) req.write(JSON.stringify(body));
34
- req.end();
35
- });
36
- }
37
-
38
- async function test() {
39
- console.log('=== Wave 4 UI Consistency Tests ===\n');
40
-
41
- try {
42
- // Test 1: Create conversation with specific agent and model
43
- console.log('Test 1: Create conversation with agent and model');
44
- const createRes = await request('POST', '/conversations', {
45
- agentId: 'claude-code',
46
- title: 'Wave 4 Test Conversation',
47
- workingDirectory: '/tmp/test',
48
- model: 'claude-sonnet-4-5'
49
- });
50
-
51
- if (createRes.status !== 200) {
52
- console.error('❌ Failed to create conversation:', createRes.status);
53
- return;
54
- }
55
-
56
- const conversation = createRes.data.conversation;
57
- console.log('✓ Created conversation:', conversation.id);
58
- console.log(' - agentId:', conversation.agentId);
59
- console.log(' - model:', conversation.model);
60
-
61
- // Test 2: Fetch conversation and verify agent/model are returned
62
- console.log('\nTest 2: Fetch conversation via /full endpoint');
63
- const fullRes = await request('GET', `/conversations/${conversation.id}/full`);
64
-
65
- if (fullRes.status !== 200) {
66
- console.error('❌ Failed to fetch conversation:', fullRes.status);
67
- return;
68
- }
69
-
70
- const fullConv = fullRes.data.conversation;
71
- console.log('✓ Fetched conversation');
72
- console.log(' - agentId:', fullConv.agentId);
73
- console.log(' - agentType:', fullConv.agentType);
74
- console.log(' - model:', fullConv.model);
75
-
76
- if (!fullConv.agentId && !fullConv.agentType) {
77
- console.error('❌ agentId/agentType missing from response');
78
- } else {
79
- console.log('✓ agentId/agentType present');
80
- }
81
-
82
- if (!fullConv.model) {
83
- console.error('❌ model missing from response');
84
- } else {
85
- console.log('✓ model present');
86
- }
87
-
88
- // Test 3: List conversations and verify agent/model in list
89
- console.log('\nTest 3: List conversations');
90
- const listRes = await request('GET', '/conversations');
91
-
92
- if (listRes.status !== 200) {
93
- console.error('❌ Failed to list conversations:', listRes.status);
94
- return;
95
- }
96
-
97
- const listedConv = listRes.data.conversations.find(c => c.id === conversation.id);
98
- if (!listedConv) {
99
- console.error('❌ Conversation not found in list');
100
- return;
101
- }
102
-
103
- console.log('✓ Conversation in list');
104
- console.log(' - agentId:', listedConv.agentId);
105
- console.log(' - agentType:', listedConv.agentType);
106
- console.log(' - model:', listedConv.model);
107
-
108
- // Test 4: Update conversation model
109
- console.log('\nTest 4: Update conversation model');
110
- const updateRes = await request('POST', `/conversations/${conversation.id}`, {
111
- model: 'claude-opus-4-6'
112
- });
113
-
114
- if (updateRes.status !== 200) {
115
- console.error('❌ Failed to update conversation:', updateRes.status);
116
- return;
117
- }
118
-
119
- const updatedConv = updateRes.data.conversation;
120
- console.log('✓ Updated conversation');
121
- console.log(' - model:', updatedConv.model);
122
-
123
- if (updatedConv.model !== 'claude-opus-4-6') {
124
- console.error('❌ Model not updated correctly');
125
- } else {
126
- console.log('✓ Model updated correctly');
127
- }
128
-
129
- // Cleanup
130
- console.log('\nCleanup: Deleting test conversation');
131
- await request('DELETE', `/conversations/${conversation.id}`);
132
- console.log('✓ Deleted test conversation');
133
-
134
- console.log('\n=== All Tests Passed ===');
135
- } catch (error) {
136
- console.error('❌ Test error:', error.message);
137
- process.exit(1);
138
- }
139
- }
140
-
141
- test();
@@ -1,277 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * WebSocket Optimization Integration Test
4
- *
5
- * Verifies all Wave 4 Item 4.2 requirements:
6
- * - Subscription-based broadcasting
7
- * - Message batching (streaming_progress)
8
- * - Compression for large payloads
9
- * - Priority queue (high/normal/low)
10
- * - Rate limiting (100 msg/sec)
11
- * - Message deduplication
12
- * - Bandwidth monitoring
13
- */
14
-
15
- import fs from 'fs';
16
- import { WSOptimizer } from './lib/ws-optimizer.js';
17
-
18
- console.log('=== WebSocket Optimization Integration Test ===\n');
19
-
20
- let testsPassed = 0;
21
- let testsFailed = 0;
22
-
23
- function pass(testName, details = []) {
24
- console.log(`✓ ${testName}`);
25
- details.forEach(d => console.log(` ${d}`));
26
- console.log();
27
- testsPassed++;
28
- }
29
-
30
- function fail(testName, reason) {
31
- console.log(`✗ ${testName}`);
32
- console.log(` Reason: ${reason}\n`);
33
- testsFailed++;
34
- }
35
-
36
- // Test 1: WSOptimizer class exists and is properly structured
37
- console.log('Test 1: Verifying WSOptimizer class structure...');
38
- try {
39
- const optimizer = new WSOptimizer();
40
- if (typeof optimizer.sendToClient === 'function' &&
41
- typeof optimizer.removeClient === 'function' &&
42
- typeof optimizer.getStats === 'function') {
43
- pass('WSOptimizer class structure', [
44
- 'sendToClient method: present',
45
- 'removeClient method: present',
46
- 'getStats method: present'
47
- ]);
48
- } else {
49
- fail('WSOptimizer class structure', 'Missing required methods');
50
- }
51
- } catch (error) {
52
- fail('WSOptimizer class structure', error.message);
53
- }
54
-
55
- // Test 2: Priority queue implementation
56
- console.log('Test 2: Verifying priority queue implementation...');
57
- const optimizerCode = fs.readFileSync('./lib/ws-optimizer.js', 'utf8');
58
-
59
- const priorityChecks = {
60
- highPriority: optimizerCode.includes('this.highPriority'),
61
- normalPriority: optimizerCode.includes('this.normalPriority'),
62
- lowPriority: optimizerCode.includes('this.lowPriority'),
63
- getPriority: optimizerCode.includes('function getPriority'),
64
- priorityLevels: optimizerCode.includes('streaming_error') &&
65
- optimizerCode.includes('streaming_progress') &&
66
- optimizerCode.includes('model_download_progress')
67
- };
68
-
69
- if (Object.values(priorityChecks).every(v => v)) {
70
- pass('Priority queue implementation', [
71
- 'High priority queue: present',
72
- 'Normal priority queue: present',
73
- 'Low priority queue: present',
74
- 'Priority classification: present',
75
- 'Message types classified: errors (high), progress (normal), downloads (low)'
76
- ]);
77
- } else {
78
- fail('Priority queue implementation', 'Missing priority queue components');
79
- }
80
-
81
- // Test 3: Batching implementation
82
- console.log('Test 3: Verifying message batching...');
83
- const batchingChecks = {
84
- scheduleFlush: optimizerCode.includes('scheduleFlush'),
85
- batchInterval: optimizerCode.includes('getBatchInterval'),
86
- maxBatchSize: optimizerCode.includes('splice(0, 10)'), // max 10 normal messages
87
- adaptiveBatching: optimizerCode.includes('BATCH_BY_TIER') &&
88
- optimizerCode.includes('latencyTier')
89
- };
90
-
91
- if (Object.values(batchingChecks).every(v => v)) {
92
- pass('Message batching', [
93
- 'Scheduled batch flushing: present',
94
- 'Adaptive batch intervals: 16-200ms based on latency',
95
- 'Max batch size: 10 normal + 5 low priority messages',
96
- 'Latency-aware batching: present'
97
- ]);
98
- } else {
99
- fail('Message batching', 'Missing batching components');
100
- }
101
-
102
- // Test 4: Compression implementation
103
- console.log('Test 4: Verifying compression...');
104
- const compressionChecks = {
105
- zlibImport: optimizerCode.includes("import zlib from 'zlib'"),
106
- gzipSync: optimizerCode.includes('gzipSync'),
107
- threshold: optimizerCode.includes('payload.length > 1024'),
108
- compressionRatio: optimizerCode.includes('compressed.length < payload.length * 0.9')
109
- };
110
-
111
- if (Object.values(compressionChecks).every(v => v)) {
112
- pass('Compression implementation', [
113
- 'zlib module imported: yes',
114
- 'Compression method: gzip',
115
- 'Compression threshold: 1KB',
116
- 'Compression ratio check: only send if >10% savings'
117
- ]);
118
- } else {
119
- fail('Compression implementation', 'Missing compression components');
120
- }
121
-
122
- // Test 5: Rate limiting
123
- console.log('Test 5: Verifying rate limiting...');
124
- const rateLimitChecks = {
125
- messageCount: optimizerCode.includes('this.messageCount'),
126
- windowTracking: optimizerCode.includes('this.windowStart'),
127
- limit100: optimizerCode.includes('messagesThisSecond > 100'),
128
- rateLimitWarning: optimizerCode.includes('rate limited'),
129
- windowReset: optimizerCode.includes('windowDuration >= 1000')
130
- };
131
-
132
- if (Object.values(rateLimitChecks).every(v => v)) {
133
- pass('Rate limiting', [
134
- 'Message count tracking: present',
135
- 'Time window tracking: 1 second',
136
- 'Rate limit: 100 messages/sec',
137
- 'Warning on limit exceeded: yes',
138
- 'Automatic window reset: yes'
139
- ]);
140
- } else {
141
- fail('Rate limiting', 'Missing rate limiting components');
142
- }
143
-
144
- // Test 6: Deduplication
145
- console.log('Test 6: Verifying message deduplication...');
146
- const deduplicationChecks = {
147
- lastMessage: optimizerCode.includes('this.lastMessage'),
148
- deduplicationCheck: optimizerCode.includes('if (this.lastMessage === data) return'),
149
- assignment: optimizerCode.includes('this.lastMessage = data')
150
- };
151
-
152
- if (Object.values(deduplicationChecks).every(v => v)) {
153
- pass('Message deduplication', [
154
- 'Last message tracking: present',
155
- 'Deduplication check: skips identical consecutive messages',
156
- 'Message tracking update: present'
157
- ]);
158
- } else {
159
- fail('Message deduplication', 'Missing deduplication components');
160
- }
161
-
162
- // Test 7: Bandwidth monitoring
163
- console.log('Test 7: Verifying bandwidth monitoring...');
164
- const monitoringChecks = {
165
- bytesSent: optimizerCode.includes('this.bytesSent'),
166
- bandwidthCalc: optimizerCode.includes('/ 1024 / 1024'),
167
- highBandwidthWarning: optimizerCode.includes('high bandwidth'),
168
- threshold: optimizerCode.includes('3 * 1024 * 1024'), // 3MB over 3 seconds = 1MB/s
169
- getStats: optimizerCode.includes('getStats()')
170
- };
171
-
172
- if (Object.values(monitoringChecks).every(v => v)) {
173
- pass('Bandwidth monitoring', [
174
- 'Bytes sent tracking: present',
175
- 'MB/sec calculation: present',
176
- 'High bandwidth warning: >1MB/sec sustained',
177
- 'Statistics API: getStats() method available',
178
- 'Per-client monitoring: yes'
179
- ]);
180
- } else {
181
- fail('Bandwidth monitoring', 'Missing monitoring components');
182
- }
183
-
184
- // Test 8: Subscription filtering in server.js
185
- console.log('Test 8: Verifying subscription-based broadcasting...');
186
- const serverCode = fs.readFileSync('./server.js', 'utf8');
187
-
188
- const subscriptionChecks = {
189
- subscriptionIndex: serverCode.includes('subscriptionIndex'),
190
- broadcastTypes: serverCode.includes('BROADCAST_TYPES'),
191
- targetedDelivery: serverCode.includes('const targets = new Set()'),
192
- sessionIdFiltering: serverCode.includes('event.sessionId') && serverCode.includes('subscriptionIndex.get'),
193
- conversationIdFiltering: serverCode.includes('event.conversationId') && serverCode.includes('conv-')
194
- };
195
-
196
- if (Object.values(subscriptionChecks).every(v => v)) {
197
- pass('Subscription-based broadcasting', [
198
- 'Subscription index: tracks client subscriptions',
199
- 'Broadcast types: global messages (conversation_created, etc.)',
200
- 'Targeted delivery: session/conversation-specific messages',
201
- 'Session ID filtering: only send to subscribed clients',
202
- 'Conversation ID filtering: only send to subscribed clients'
203
- ]);
204
- } else {
205
- fail('Subscription-based broadcasting', 'Missing subscription filtering');
206
- }
207
-
208
- // Test 9: Integration verification
209
- console.log('Test 9: Verifying broadcastSync integration...');
210
- const integrationChecks = {
211
- wsOptimizerUsage: serverCode.includes('wsOptimizer.sendToClient'),
212
- wsOptimizerInstance: serverCode.includes('new WSOptimizer()'),
213
- broadcastSyncFunction: serverCode.includes('function broadcastSync'),
214
- clientRemoval: serverCode.includes('wsOptimizer.removeClient')
215
- };
216
-
217
- if (Object.values(integrationChecks).every(v => v)) {
218
- pass('broadcastSync integration', [
219
- 'WSOptimizer instantiated: yes',
220
- 'Used in broadcastSync: yes',
221
- 'Client cleanup on disconnect: yes',
222
- 'All broadcasts route through optimizer: yes'
223
- ]);
224
- } else {
225
- fail('broadcastSync integration', 'WSOptimizer not properly integrated');
226
- }
227
-
228
- // Test 10: Adaptive batching based on latency
229
- console.log('Test 10: Verifying adaptive batching...');
230
- const adaptiveChecks = {
231
- batchByTier: optimizerCode.includes('BATCH_BY_TIER'),
232
- tierLevels: optimizerCode.includes('excellent') &&
233
- optimizerCode.includes('good') &&
234
- optimizerCode.includes('fair') &&
235
- optimizerCode.includes('poor'),
236
- trendAdaptation: optimizerCode.includes('latencyTrend') &&
237
- optimizerCode.includes('rising') &&
238
- optimizerCode.includes('falling'),
239
- intervalRange: optimizerCode.includes('16') && optimizerCode.includes('200')
240
- };
241
-
242
- if (Object.values(adaptiveChecks).every(v => v)) {
243
- pass('Adaptive batching', [
244
- 'Latency-based intervals: 16ms (excellent) to 200ms (bad)',
245
- 'Tier levels: excellent, good, fair, poor, bad',
246
- 'Trend adaptation: adjusts interval based on latency trend',
247
- 'Dynamic optimization: yes'
248
- ]);
249
- } else {
250
- fail('Adaptive batching', 'Missing adaptive batching features');
251
- }
252
-
253
- // Summary
254
- console.log('=== Test Summary ===');
255
- console.log(`Total tests: ${testsPassed + testsFailed}`);
256
- console.log(`Passed: ${testsPassed}`);
257
- console.log(`Failed: ${testsFailed}`);
258
- console.log(`Success rate: ${((testsPassed / (testsPassed + testsFailed)) * 100).toFixed(1)}%\n`);
259
-
260
- if (testsFailed === 0) {
261
- console.log('✓ All WebSocket optimization requirements verified!\n');
262
- console.log('Wave 4 Item 4.2 Implementation Summary:');
263
- console.log('────────────────────────────────────────');
264
- console.log('✓ Subscription filtering: Only broadcasts to subscribed clients');
265
- console.log('✓ Message batching: Max 10 normal + 5 low priority per flush');
266
- console.log('✓ Adaptive intervals: 16-200ms based on latency tier');
267
- console.log('✓ Compression: gzip for payloads >1KB (>10% savings)');
268
- console.log('✓ Priority queuing: High (errors) > Normal (progress) > Low (downloads)');
269
- console.log('✓ Rate limiting: 100 messages/sec per client');
270
- console.log('✓ Deduplication: Skips identical consecutive messages');
271
- console.log('✓ Bandwidth monitoring: Warns if >1MB/sec sustained');
272
- console.log('\nExpected bandwidth reduction: 60-80% for high-frequency streaming');
273
- process.exit(0);
274
- } else {
275
- console.log('✗ Some optimization requirements not met');
276
- process.exit(1);
277
- }