agentgui 1.0.29 → 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_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 +42 -20
- 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
|
@@ -772,47 +772,68 @@ class GMGUIApp {
|
|
|
772
772
|
el.className = `message ${msg.role}`;
|
|
773
773
|
el.dataset.messageId = msg.id;
|
|
774
774
|
|
|
775
|
+
// CRITICAL: Always check for HTML content first - NEVER render HTML as plain text
|
|
775
776
|
if (typeof msg.content === 'string') {
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
|
|
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));
|
|
779
781
|
} else {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
bubble.textContent = msg.content;
|
|
783
|
-
el.appendChild(bubble);
|
|
784
|
-
}
|
|
785
|
-
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
786
|
-
// Display segmented content if available
|
|
787
|
-
if (msg.content.segments && Array.isArray(msg.content.segments)) {
|
|
788
|
-
msg.content.segments.forEach(segment => {
|
|
789
|
-
el.appendChild(this.renderSegment(segment));
|
|
790
|
-
});
|
|
791
|
-
} else if (msg.content.text) {
|
|
792
|
-
// Fallback to regular text rendering
|
|
793
|
-
const parsed = this.parseAndRenderContent(msg.content.text);
|
|
782
|
+
// Only use text rendering if it's not HTML
|
|
783
|
+
const parsed = this.parseAndRenderContent(msg.content);
|
|
794
784
|
if (parsed) {
|
|
795
785
|
parsed.forEach(elem => el.appendChild(elem));
|
|
796
786
|
} else {
|
|
797
787
|
const bubble = document.createElement('div');
|
|
798
788
|
bubble.className = 'message-bubble';
|
|
799
|
-
bubble.textContent = msg.content
|
|
789
|
+
bubble.textContent = msg.content;
|
|
800
790
|
el.appendChild(bubble);
|
|
801
791
|
}
|
|
802
792
|
}
|
|
803
|
-
|
|
804
|
-
//
|
|
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)
|
|
805
798
|
if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
|
|
806
799
|
msg.content.blocks.forEach(block => {
|
|
807
800
|
if (block.type === 'html') {
|
|
801
|
+
console.log('[HTML] Rendering HTML block from agent');
|
|
808
802
|
const htmlEl = this.createHtmlBlock(block);
|
|
809
803
|
el.appendChild(htmlEl);
|
|
804
|
+
hasHtmlContent = true;
|
|
810
805
|
} else if (block.type === 'image') {
|
|
811
806
|
const imgEl = this.createImageBlock(block);
|
|
812
807
|
el.appendChild(imgEl);
|
|
808
|
+
hasHtmlContent = true;
|
|
813
809
|
}
|
|
814
810
|
});
|
|
815
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
|
+
}
|
|
816
837
|
|
|
817
838
|
// Display metadata if available
|
|
818
839
|
if (msg.content.metadata) {
|
|
@@ -820,6 +841,7 @@ class GMGUIApp {
|
|
|
820
841
|
if (metadataEl) el.appendChild(metadataEl);
|
|
821
842
|
}
|
|
822
843
|
} else {
|
|
844
|
+
// Fallback for non-string, non-object content
|
|
823
845
|
const bubble = document.createElement('div');
|
|
824
846
|
bubble.className = 'message-bubble';
|
|
825
847
|
bubble.textContent = JSON.stringify(msg.content);
|
|
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);
|