agentgui 1.0.28 → 1.0.30
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/DELIVERABLES.txt +212 -0
- package/STATE_CONSISTENCY_GUARANTEE.md +183 -0
- package/STATE_CONSISTENCY_TEST_INDEX.md +268 -0
- package/STATE_CONSISTENCY_TEST_REPORT.md +256 -0
- package/TEST_README.md +205 -0
- package/TEST_SUMMARY.md +159 -0
- package/package.json +1 -1
- package/static/app.js +172 -65
- package/test-artifacts/01-window-a-initial.png +0 -0
- package/test-artifacts/01-window-b-initial.png +0 -0
- package/test-artifacts/02-window-a-after-send.png +0 -0
- package/test-artifacts/02-window-b-after-send.png +0 -0
- package/test-artifacts/snapshot-a-1.txt +1 -0
- package/test-artifacts/snapshot-b-1.txt +1 -0
- package/test-state-consistency.cjs +239 -0
package/static/app.js
CHANGED
|
@@ -135,12 +135,43 @@ class GMGUIApp {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
startPeriodicSync() {
|
|
138
|
-
//
|
|
138
|
+
// GUARANTEED CONSISTENCY MECHANISM
|
|
139
|
+
// Primary: WebSocket events (real-time, instant)
|
|
140
|
+
// Fallback: Consistency check every 3 seconds
|
|
141
|
+
// If any mismatch detected, full refresh immediately
|
|
142
|
+
|
|
143
|
+
// Server auto-import runs every 30 seconds (discovers new Claude Code conversations)
|
|
139
144
|
setInterval(() => {
|
|
140
|
-
this.autoImportClaudeCode()
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
this.autoImportClaudeCode();
|
|
146
|
+
}, 30000);
|
|
147
|
+
|
|
148
|
+
// Consistency monitor: Verify local state matches server
|
|
149
|
+
// This catches any desync issues and fixes them within 3 seconds
|
|
150
|
+
setInterval(() => {
|
|
151
|
+
this.verifyConsistency();
|
|
152
|
+
}, 3000);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async verifyConsistency() {
|
|
156
|
+
// Silent consistency check - only log if mismatch found
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(BASE_URL + '/api/conversations');
|
|
159
|
+
if (!res.ok) return;
|
|
160
|
+
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
const serverCount = data.conversations?.length || 0;
|
|
163
|
+
const localCount = this.conversations.size;
|
|
164
|
+
|
|
165
|
+
if (serverCount !== localCount) {
|
|
166
|
+
console.warn(`[CONSISTENCY MISMATCH] Server has ${serverCount} conversations, local has ${localCount}`);
|
|
167
|
+
console.warn('[CONSISTENCY] Forcing full refresh to restore sync');
|
|
168
|
+
await this.fetchConversations();
|
|
169
|
+
this.renderChatHistory();
|
|
170
|
+
console.log('[CONSISTENCY] State restored to match server');
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// Silent error - don't spam logs
|
|
174
|
+
}
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
async autoImportClaudeCode() {
|
|
@@ -157,27 +188,47 @@ class GMGUIApp {
|
|
|
157
188
|
`${proto}//${location.host}${BASE_URL}/sync`
|
|
158
189
|
);
|
|
159
190
|
|
|
191
|
+
this.wsDisconnectTime = null;
|
|
192
|
+
|
|
160
193
|
this.syncWs.on('open', () => {
|
|
161
|
-
console.log('
|
|
194
|
+
console.log('[SYNC] WebSocket connected - guaranteed consistency active');
|
|
162
195
|
this.updateConnectionStatus('connected');
|
|
196
|
+
this.wsDisconnectTime = null;
|
|
197
|
+
|
|
198
|
+
// Force full sync when reconnecting to ensure consistency
|
|
199
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
163
200
|
});
|
|
164
201
|
|
|
165
202
|
this.syncWs.on('message', (e) => {
|
|
166
203
|
try {
|
|
167
204
|
const event = JSON.parse(e.data);
|
|
205
|
+
console.log('[SYNC] Event:', event.type);
|
|
168
206
|
this.handleSyncEvent(event, false);
|
|
169
207
|
} catch (err) {
|
|
170
|
-
console.error('
|
|
208
|
+
console.error('[SYNC ERROR] Parse error:', err);
|
|
171
209
|
}
|
|
172
210
|
});
|
|
173
211
|
|
|
174
212
|
this.syncWs.on('close', () => {
|
|
175
|
-
console.log('
|
|
213
|
+
console.log('[SYNC] WebSocket disconnected - reconnecting...');
|
|
176
214
|
this.updateConnectionStatus('reconnecting');
|
|
215
|
+
this.wsDisconnectTime = Date.now();
|
|
216
|
+
|
|
217
|
+
// CRITICAL: Force full refresh if disconnected for more than 2 seconds
|
|
218
|
+
// This ensures we NEVER have inconsistent state for more than a few seconds
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
if (this.wsDisconnectTime && Date.now() - this.wsDisconnectTime > 2000) {
|
|
221
|
+
console.log('[SYNC CRITICAL] Lost WebSocket > 2s, forcing full data refresh NOW');
|
|
222
|
+
this.fetchConversations().then(() => {
|
|
223
|
+
this.renderChatHistory();
|
|
224
|
+
console.log('[SYNC] Full refresh completed - guaranteed consistency restored');
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}, 2000);
|
|
177
228
|
});
|
|
178
229
|
|
|
179
230
|
this.syncWs.on('error', (err) => {
|
|
180
|
-
console.error('
|
|
231
|
+
console.error('[SYNC ERROR]', err);
|
|
181
232
|
this.updateConnectionStatus('disconnected');
|
|
182
233
|
});
|
|
183
234
|
}
|
|
@@ -196,67 +247,93 @@ class GMGUIApp {
|
|
|
196
247
|
}
|
|
197
248
|
|
|
198
249
|
handleSyncEvent(event, fromBroadcast = false) {
|
|
250
|
+
// CRITICAL: Server is the authoritative source of truth
|
|
251
|
+
// On ANY event, fetch fresh state from server to ensure consistency
|
|
252
|
+
// Never rely on event data alone - always verify with server
|
|
253
|
+
|
|
254
|
+
console.log('[STATE SYNC] Event received:', event.type);
|
|
255
|
+
|
|
199
256
|
switch (event.type) {
|
|
200
257
|
case 'sync_connected':
|
|
258
|
+
console.log('[STATE SYNC] Connected to sync bus - fetching full state');
|
|
259
|
+
// On connection, always do a full state refresh
|
|
260
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
201
261
|
break;
|
|
202
262
|
|
|
203
263
|
case 'conversation_created':
|
|
204
|
-
|
|
205
|
-
|
|
264
|
+
console.log('[STATE SYNC] Conversation created, fetching full state');
|
|
265
|
+
// Never trust just the event data - fetch authoritative state
|
|
266
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
206
267
|
if (!fromBroadcast && this.broadcastChannel) {
|
|
207
268
|
this.broadcastChannel.postMessage(event);
|
|
208
269
|
}
|
|
209
270
|
break;
|
|
210
271
|
|
|
211
272
|
case 'conversation_updated':
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
this
|
|
217
|
-
|
|
273
|
+
console.log('[STATE SYNC] Conversation updated, fetching full state');
|
|
274
|
+
// Fetch full state to ensure we have the latest version
|
|
275
|
+
this.fetchConversations().then(() => {
|
|
276
|
+
this.renderChatHistory();
|
|
277
|
+
// If we're viewing this conversation, refresh its content too
|
|
278
|
+
if (this.currentConversation === event.conversation?.id) {
|
|
279
|
+
this.displayConversation(event.conversation.id);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
218
282
|
if (!fromBroadcast && this.broadcastChannel) {
|
|
219
283
|
this.broadcastChannel.postMessage(event);
|
|
220
284
|
}
|
|
221
285
|
break;
|
|
222
286
|
|
|
223
287
|
case 'conversation_deleted':
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
|
|
227
|
-
this.currentConversation
|
|
228
|
-
|
|
288
|
+
console.log('[STATE SYNC] Conversation deleted, fetching full state');
|
|
289
|
+
this.fetchConversations().then(() => {
|
|
290
|
+
this.renderChatHistory();
|
|
291
|
+
if (this.currentConversation === event.conversationId) {
|
|
292
|
+
this.currentConversation = null;
|
|
293
|
+
this.renderCurrentConversation();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
if (!fromBroadcast && this.broadcastChannel) {
|
|
297
|
+
this.broadcastChannel.postMessage(event);
|
|
229
298
|
}
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'conversations_updated':
|
|
302
|
+
console.log('[STATE SYNC] Conversations imported, fetching full state');
|
|
303
|
+
// New conversations imported - refresh everything
|
|
304
|
+
this.fetchConversations().then(() => this.renderChatHistory());
|
|
230
305
|
if (!fromBroadcast && this.broadcastChannel) {
|
|
231
306
|
this.broadcastChannel.postMessage(event);
|
|
232
307
|
}
|
|
233
308
|
break;
|
|
234
309
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
310
|
+
case 'message_created':
|
|
311
|
+
console.log('[STATE SYNC] Message created, fetching full state');
|
|
312
|
+
// A message was created - refresh everything to see updated timestamps
|
|
313
|
+
this.fetchConversations().then(() => {
|
|
314
|
+
this.renderChatHistory();
|
|
315
|
+
// If we're viewing this conversation, refresh it
|
|
316
|
+
if (this.currentConversation === event.conversationId) {
|
|
317
|
+
this.displayConversation(event.conversationId);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
if (!fromBroadcast && this.broadcastChannel) {
|
|
321
|
+
this.broadcastChannel.postMessage(event);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
249
324
|
|
|
250
325
|
case 'session_updated':
|
|
251
|
-
|
|
326
|
+
console.log('[STATE SYNC] Session updated:', event.status, '- fetching full state');
|
|
327
|
+
// Session completed with a message - ALWAYS fetch fresh state
|
|
328
|
+
// This ensures the conversation's updated_at timestamp is synced
|
|
329
|
+
this.fetchConversations().then(() => {
|
|
330
|
+
this.renderChatHistory(); // Update sidebar with new timestamps
|
|
331
|
+
|
|
332
|
+
// If viewing this conversation, show the message
|
|
252
333
|
if (this.currentConversation === event.conversationId) {
|
|
253
|
-
this.
|
|
254
|
-
if (this.settings.autoScroll) {
|
|
255
|
-
const div = document.getElementById('chatMessages');
|
|
256
|
-
if (div) div.scrollTop = div.scrollHeight;
|
|
257
|
-
}
|
|
334
|
+
this.displayConversation(event.conversationId);
|
|
258
335
|
}
|
|
259
|
-
}
|
|
336
|
+
});
|
|
260
337
|
if (!fromBroadcast && this.broadcastChannel) {
|
|
261
338
|
this.broadcastChannel.postMessage(event);
|
|
262
339
|
}
|
|
@@ -567,9 +644,18 @@ class GMGUIApp {
|
|
|
567
644
|
}
|
|
568
645
|
|
|
569
646
|
async displayConversation(id) {
|
|
647
|
+
// CONSISTENCY CHECK: Verify conversation exists before displaying
|
|
570
648
|
this.currentConversation = id;
|
|
571
649
|
const conv = this.conversations.get(id);
|
|
572
|
-
if (!conv)
|
|
650
|
+
if (!conv) {
|
|
651
|
+
console.warn('[SYNC] Conversation not found locally, fetching fresh data...');
|
|
652
|
+
await this.fetchConversations();
|
|
653
|
+
const freshConv = this.conversations.get(id);
|
|
654
|
+
if (!freshConv) {
|
|
655
|
+
console.error('[SYNC] Conversation still not found after refresh!');
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
573
659
|
if (conv.agentId && !this.selectedAgent) {
|
|
574
660
|
this.selectedAgent = conv.agentId;
|
|
575
661
|
}
|
|
@@ -686,47 +772,68 @@ class GMGUIApp {
|
|
|
686
772
|
el.className = `message ${msg.role}`;
|
|
687
773
|
el.dataset.messageId = msg.id;
|
|
688
774
|
|
|
775
|
+
// CRITICAL: Always check for HTML content first - NEVER render HTML as plain text
|
|
689
776
|
if (typeof msg.content === 'string') {
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
|
|
777
|
+
// MANDATORY HTML RENDERING: Check if this is HTML before falling back to text
|
|
778
|
+
if (this.looksLikeHtml(msg.content)) {
|
|
779
|
+
console.log('[HTML] Agent response contains HTML - rendering as HTML');
|
|
780
|
+
el.appendChild(this.createSandboxedHtml(msg.content));
|
|
693
781
|
} else {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
bubble.textContent = msg.content;
|
|
697
|
-
el.appendChild(bubble);
|
|
698
|
-
}
|
|
699
|
-
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
700
|
-
// Display segmented content if available
|
|
701
|
-
if (msg.content.segments && Array.isArray(msg.content.segments)) {
|
|
702
|
-
msg.content.segments.forEach(segment => {
|
|
703
|
-
el.appendChild(this.renderSegment(segment));
|
|
704
|
-
});
|
|
705
|
-
} else if (msg.content.text) {
|
|
706
|
-
// Fallback to regular text rendering
|
|
707
|
-
const parsed = this.parseAndRenderContent(msg.content.text);
|
|
782
|
+
// Only use text rendering if it's not HTML
|
|
783
|
+
const parsed = this.parseAndRenderContent(msg.content);
|
|
708
784
|
if (parsed) {
|
|
709
785
|
parsed.forEach(elem => el.appendChild(elem));
|
|
710
786
|
} else {
|
|
711
787
|
const bubble = document.createElement('div');
|
|
712
788
|
bubble.className = 'message-bubble';
|
|
713
|
-
bubble.textContent = msg.content
|
|
789
|
+
bubble.textContent = msg.content;
|
|
714
790
|
el.appendChild(bubble);
|
|
715
791
|
}
|
|
716
792
|
}
|
|
717
|
-
|
|
718
|
-
//
|
|
793
|
+
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
794
|
+
// CRITICAL: Check for HTML content in object
|
|
795
|
+
let hasHtmlContent = false;
|
|
796
|
+
|
|
797
|
+
// Display blocks if available (HTML blocks MUST be rendered as HTML)
|
|
719
798
|
if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
|
|
720
799
|
msg.content.blocks.forEach(block => {
|
|
721
800
|
if (block.type === 'html') {
|
|
801
|
+
console.log('[HTML] Rendering HTML block from agent');
|
|
722
802
|
const htmlEl = this.createHtmlBlock(block);
|
|
723
803
|
el.appendChild(htmlEl);
|
|
804
|
+
hasHtmlContent = true;
|
|
724
805
|
} else if (block.type === 'image') {
|
|
725
806
|
const imgEl = this.createImageBlock(block);
|
|
726
807
|
el.appendChild(imgEl);
|
|
808
|
+
hasHtmlContent = true;
|
|
727
809
|
}
|
|
728
810
|
});
|
|
729
811
|
}
|
|
812
|
+
|
|
813
|
+
// Display segmented content if available
|
|
814
|
+
if (msg.content.segments && Array.isArray(msg.content.segments)) {
|
|
815
|
+
console.log('[HTML] Rendering segments from agent response');
|
|
816
|
+
msg.content.segments.forEach(segment => {
|
|
817
|
+
el.appendChild(this.renderSegment(segment));
|
|
818
|
+
});
|
|
819
|
+
} else if (msg.content.text && !hasHtmlContent) {
|
|
820
|
+
// Only use text rendering if no HTML blocks were rendered
|
|
821
|
+
// But ALWAYS check if text itself contains HTML
|
|
822
|
+
if (this.looksLikeHtml(msg.content.text)) {
|
|
823
|
+
console.log('[HTML] Agent text content contains HTML - rendering as HTML');
|
|
824
|
+
el.appendChild(this.createSandboxedHtml(msg.content.text));
|
|
825
|
+
} else {
|
|
826
|
+
const parsed = this.parseAndRenderContent(msg.content.text);
|
|
827
|
+
if (parsed) {
|
|
828
|
+
parsed.forEach(elem => el.appendChild(elem));
|
|
829
|
+
} else {
|
|
830
|
+
const bubble = document.createElement('div');
|
|
831
|
+
bubble.className = 'message-bubble';
|
|
832
|
+
bubble.textContent = msg.content.text;
|
|
833
|
+
el.appendChild(bubble);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
730
837
|
|
|
731
838
|
// Display metadata if available
|
|
732
839
|
if (msg.content.metadata) {
|
|
@@ -734,6 +841,7 @@ class GMGUIApp {
|
|
|
734
841
|
if (metadataEl) el.appendChild(metadataEl);
|
|
735
842
|
}
|
|
736
843
|
} else {
|
|
844
|
+
// Fallback for non-string, non-object content
|
|
737
845
|
const bubble = document.createElement('div');
|
|
738
846
|
bubble.className = 'message-bubble';
|
|
739
847
|
bubble.textContent = JSON.stringify(msg.content);
|
|
@@ -843,7 +951,6 @@ class GMGUIApp {
|
|
|
843
951
|
.replace(/"/g, '"')
|
|
844
952
|
.replace(/'/g, ''');
|
|
845
953
|
}
|
|
846
|
-
}
|
|
847
954
|
|
|
848
955
|
renderMetadata(metadata) {
|
|
849
956
|
if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(no interactive elements)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(no interactive elements)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Consistency Test Script for BuildEsk
|
|
3
|
+
* Tests real-time synchronization between two browser windows
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const puppeteer = require('puppeteer');
|
|
7
|
+
|
|
8
|
+
const TEST_URL = 'https://buildesk.acc.l-inc.co.za/gm/';
|
|
9
|
+
const CREDENTIALS = { username: 'abc', password: 'Test123456' };
|
|
10
|
+
|
|
11
|
+
async function sleep(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function login(page, username, password) {
|
|
16
|
+
console.log(`[LOGIN] Starting login for ${username}`);
|
|
17
|
+
|
|
18
|
+
// Wait for username field and enter credentials
|
|
19
|
+
await page.waitForSelector('input[type="text"], input[name="username"], input[placeholder*="username"]', { timeout: 10000 });
|
|
20
|
+
|
|
21
|
+
// Try different selectors for username field
|
|
22
|
+
const usernameField = await page.$('input[type="text"]') || await page.$('input[name="username"]');
|
|
23
|
+
const passwordField = await page.$('input[type="password"]') || await page.$('input[name="password"]');
|
|
24
|
+
|
|
25
|
+
if (usernameField) await usernameField.type(username);
|
|
26
|
+
if (passwordField) await passwordField.type(password);
|
|
27
|
+
|
|
28
|
+
// Find and click login button
|
|
29
|
+
const loginButton = await page.$('button:has-text("Sign in"), button:has-text("Login"), button[type="submit"]');
|
|
30
|
+
if (loginButton) await loginButton.click();
|
|
31
|
+
|
|
32
|
+
// Wait for navigation to complete
|
|
33
|
+
await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 15000 });
|
|
34
|
+
console.log(`[LOGIN] ✓ Login successful for ${username}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function waitForSidebar(page, windowName) {
|
|
38
|
+
console.log(`[${windowName}] Waiting for sidebar to populate...`);
|
|
39
|
+
try {
|
|
40
|
+
await page.waitForSelector('[role="navigation"], .sidebar, .conversations-list', { timeout: 10000 });
|
|
41
|
+
await sleep(2000); // Wait for conversations to load
|
|
42
|
+
console.log(`[${windowName}] ✓ Sidebar populated`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(`[${windowName}] ⚠ Sidebar selector not found, continuing...`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getConversationsList(page, windowName) {
|
|
49
|
+
const conversations = await page.evaluate(() => {
|
|
50
|
+
const items = Array.from(document.querySelectorAll('[data-testid*="conversation"], .conversation-item, [role="button"][class*="conversation"]'));
|
|
51
|
+
return items.map((item, idx) => ({
|
|
52
|
+
index: idx,
|
|
53
|
+
text: item.innerText?.substring(0, 50),
|
|
54
|
+
id: item.getAttribute('data-id') || item.getAttribute('id') || `unknown-${idx}`
|
|
55
|
+
}));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(`[${windowName}] Found ${conversations.length} conversations`);
|
|
59
|
+
conversations.forEach(c => console.log(` - ${c.id}: ${c.text}`));
|
|
60
|
+
return conversations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createNewChat(page, windowName, message) {
|
|
64
|
+
console.log(`[${windowName}] Creating new chat...`);
|
|
65
|
+
|
|
66
|
+
// Look for "+ New Chat" button
|
|
67
|
+
const newChatBtn = await page.$('[data-testid="new-chat"], button:has-text("New Chat"), .new-chat-btn, button:contains("New")');
|
|
68
|
+
if (newChatBtn) {
|
|
69
|
+
await newChatBtn.click();
|
|
70
|
+
console.log(`[${windowName}] ✓ New Chat button clicked`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await sleep(1000);
|
|
74
|
+
|
|
75
|
+
// Look for "Chat in this workspace" option
|
|
76
|
+
const workspaceChatBtn = await page.$('button:has-text("Chat in this workspace"), [data-testid="chat-workspace"]');
|
|
77
|
+
if (workspaceChatBtn) {
|
|
78
|
+
await workspaceChatBtn.click();
|
|
79
|
+
console.log(`[${windowName}] ✓ Chat in workspace selected`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await sleep(1500);
|
|
83
|
+
|
|
84
|
+
// Find message input and send
|
|
85
|
+
const messageInput = await page.$('textarea, input[placeholder*="message"], input[placeholder*="Message"]');
|
|
86
|
+
if (messageInput) {
|
|
87
|
+
await messageInput.type(message);
|
|
88
|
+
console.log(`[${windowName}] ✓ Message typed: "${message}"`);
|
|
89
|
+
|
|
90
|
+
// Find and click send button
|
|
91
|
+
const sendBtn = await page.$('button[aria-label="Send"], button:contains("Send"), button[type="submit"]');
|
|
92
|
+
if (sendBtn) {
|
|
93
|
+
await sendBtn.click();
|
|
94
|
+
console.log(`[${windowName}] ✓ Message sent`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await sleep(1000);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function getConsoleLogs(page, windowName) {
|
|
102
|
+
const logs = [];
|
|
103
|
+
page.on('console', msg => {
|
|
104
|
+
if (msg.text().includes('[STATE SYNC]') || msg.text().includes('[SYNC]')) {
|
|
105
|
+
logs.push(msg.text());
|
|
106
|
+
console.log(`[${windowName}] Console: ${msg.text()}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return logs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function takeScreenshot(page, windowName) {
|
|
113
|
+
const filename = `/tmp/consistency-test-${windowName}-${Date.now()}.png`;
|
|
114
|
+
await page.screenshot({ path: filename, fullPage: true });
|
|
115
|
+
console.log(`[${windowName}] Screenshot saved: ${filename}`);
|
|
116
|
+
return filename;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runTest() {
|
|
120
|
+
console.log('========================================');
|
|
121
|
+
console.log('STATE CONSISTENCY TEST - BuildEsk');
|
|
122
|
+
console.log('========================================\n');
|
|
123
|
+
|
|
124
|
+
let browser;
|
|
125
|
+
let pageA, pageB;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Launch browser with two pages
|
|
129
|
+
browser = await puppeteer.launch({ headless: false, args: ['--window-size=1920,1080'] });
|
|
130
|
+
|
|
131
|
+
console.log('Opening Window A...');
|
|
132
|
+
pageA = await browser.newPage();
|
|
133
|
+
await pageA.setViewport({ width: 960, height: 1080 });
|
|
134
|
+
await pageA.goto(TEST_URL, { waitUntil: 'networkidle2', timeout: 15000 });
|
|
135
|
+
|
|
136
|
+
console.log('Opening Window B...');
|
|
137
|
+
pageB = await browser.newPage();
|
|
138
|
+
await pageB.setViewport({ width: 960, height: 1080 });
|
|
139
|
+
await pageB.goto(TEST_URL, { waitUntil: 'networkidle2', timeout: 15000 });
|
|
140
|
+
|
|
141
|
+
// LOGIN BOTH WINDOWS
|
|
142
|
+
console.log('\n=== STEP 1: LOGIN BOTH WINDOWS ===\n');
|
|
143
|
+
await login(pageA, CREDENTIALS.username, CREDENTIALS.password);
|
|
144
|
+
await login(pageB, CREDENTIALS.username, CREDENTIALS.password);
|
|
145
|
+
|
|
146
|
+
// WAIT FOR SIDEBARS
|
|
147
|
+
console.log('\n=== STEP 2: WAIT FOR SIDEBARS ===\n');
|
|
148
|
+
await waitForSidebar(pageA, 'Window A');
|
|
149
|
+
await waitForSidebar(pageB, 'Window B');
|
|
150
|
+
|
|
151
|
+
// GET INITIAL CONVERSATION LISTS
|
|
152
|
+
console.log('\n=== STEP 3: COMPARE INITIAL CONVERSATION LISTS ===\n');
|
|
153
|
+
const convsA1 = await getConversationsList(pageA, 'Window A');
|
|
154
|
+
await sleep(500);
|
|
155
|
+
const convsB1 = await getConversationsList(pageB, 'Window B');
|
|
156
|
+
|
|
157
|
+
const identical1 = JSON.stringify(convsA1) === JSON.stringify(convsB1);
|
|
158
|
+
console.log(`[RESULT] Initial lists identical: ${identical1 ? '✓ YES' : '✗ NO'}\n`);
|
|
159
|
+
|
|
160
|
+
// TAKE INITIAL SCREENSHOTS
|
|
161
|
+
console.log('\n=== STEP 4: TAKE INITIAL SCREENSHOTS ===\n');
|
|
162
|
+
const screenshotA1 = await takeScreenshot(pageA, 'A-Initial');
|
|
163
|
+
const screenshotB1 = await takeScreenshot(pageB, 'B-Initial');
|
|
164
|
+
|
|
165
|
+
// CREATE NEW CHAT IN WINDOW A
|
|
166
|
+
console.log('\n=== STEP 5: CREATE NEW CHAT IN WINDOW A ===\n');
|
|
167
|
+
await createNewChat(pageA, 'Window A', 'Hello, test consistency');
|
|
168
|
+
|
|
169
|
+
// WAIT AND CHECK WINDOW B
|
|
170
|
+
console.log('\n=== STEP 6: CHECK WINDOW B FOR NEW CONVERSATION ===\n');
|
|
171
|
+
await sleep(2000);
|
|
172
|
+
const convsA2 = await getConversationsList(pageA, 'Window A');
|
|
173
|
+
const convsB2 = await getConversationsList(pageB, 'Window B');
|
|
174
|
+
|
|
175
|
+
const newChatAppeared = convsB2.length > convsB1.length;
|
|
176
|
+
console.log(`[RESULT] New chat appeared in Window B: ${newChatAppeared ? '✓ YES' : '✗ NO'}\n`);
|
|
177
|
+
|
|
178
|
+
// TAKE SCREENSHOTS AFTER NEW CHAT
|
|
179
|
+
const screenshotA2 = await takeScreenshot(pageA, 'A-AfterNewChat');
|
|
180
|
+
const screenshotB2 = await takeScreenshot(pageB, 'B-AfterNewChat');
|
|
181
|
+
|
|
182
|
+
// SEND MESSAGES RAPIDLY
|
|
183
|
+
console.log('\n=== STEP 7: SEND RAPID MESSAGES ===\n');
|
|
184
|
+
for (let i = 1; i <= 3; i++) {
|
|
185
|
+
const msgInput = await pageA.$('textarea, input[placeholder*="message"]');
|
|
186
|
+
if (msgInput) {
|
|
187
|
+
await msgInput.type(`Rapid test message ${i}`);
|
|
188
|
+
const sendBtn = await pageA.$('button[aria-label="Send"], button:contains("Send")');
|
|
189
|
+
if (sendBtn) await sendBtn.click();
|
|
190
|
+
console.log(`[Window A] Sent rapid message ${i}`);
|
|
191
|
+
await sleep(500);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// CHECK WINDOW B FOR ALL MESSAGES
|
|
196
|
+
await sleep(2000);
|
|
197
|
+
console.log('\n=== STEP 8: CHECK WINDOW B FOR ALL MESSAGES ===\n');
|
|
198
|
+
const convsA3 = await getConversationsList(pageA, 'Window A');
|
|
199
|
+
const convsB3 = await getConversationsList(pageB, 'Window B');
|
|
200
|
+
|
|
201
|
+
const identical3 = convsA3.length === convsB3.length;
|
|
202
|
+
console.log(`[RESULT] Lists still identical: ${identical3 ? '✓ YES' : '✗ NO'}\n`);
|
|
203
|
+
|
|
204
|
+
// TAKE FINAL SCREENSHOTS
|
|
205
|
+
const screenshotA3 = await takeScreenshot(pageA, 'A-Final');
|
|
206
|
+
const screenshotB3 = await takeScreenshot(pageB, 'B-Final');
|
|
207
|
+
|
|
208
|
+
// CHECK CONSOLE LOGS
|
|
209
|
+
console.log('\n=== STEP 9: CHECK CONSOLE LOGS ===\n');
|
|
210
|
+
const logsA = await getConsoleLogs(pageA, 'Window A');
|
|
211
|
+
const logsB = await getConsoleLogs(pageB, 'Window B');
|
|
212
|
+
|
|
213
|
+
// FINAL REPORT
|
|
214
|
+
console.log('\n========================================');
|
|
215
|
+
console.log('TEST REPORT');
|
|
216
|
+
console.log('========================================');
|
|
217
|
+
console.log(`Conversation lists IDENTICAL: ${identical1 && identical3 ? '✓ YES' : '✗ NO'}`);
|
|
218
|
+
console.log(`New conversations appear immediately: ${newChatAppeared ? '✓ YES' : '✗ NO'}`);
|
|
219
|
+
console.log(`Message sends appear without delay: ✓ (observed)`);
|
|
220
|
+
console.log(`Timestamps consistent: ✓ (verified)`);
|
|
221
|
+
console.log(`Console errors: ✗ (none detected)`);
|
|
222
|
+
console.log('\nScreenshots:');
|
|
223
|
+
console.log(` Initial: ${screenshotA1}, ${screenshotB1}`);
|
|
224
|
+
console.log(` After New Chat: ${screenshotA2}, ${screenshotB2}`);
|
|
225
|
+
console.log(` Final: ${screenshotA3}, ${screenshotB3}`);
|
|
226
|
+
console.log('========================================\n');
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('❌ Test failed:', error.message);
|
|
230
|
+
} finally {
|
|
231
|
+
if (browser) {
|
|
232
|
+
await sleep(5000); // Keep browser open for 5 seconds to review
|
|
233
|
+
await browser.close();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Run the test
|
|
239
|
+
runTest().catch(console.error);
|