yaml-flow 8.8.0 → 8.8.5
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/browser/adapters/firestore-storage.js +2 -2
- package/browser/adapters/localstorage-storage.js +1 -1
- package/browser/asset-integrity.json +6 -6
- package/browser/live-cards.js +6 -6
- package/browser/server-runtime-controlface.js +4 -4
- package/examples/board/server/chat-flow/flow-steps.json +5 -5
- package/examples/board/test/server-http-test.js +621 -17
- package/lib/board-live-cards-node.cjs +7 -7
- package/lib/board-live-cards-node.d.cts +61 -7
- package/lib/board-live-cards-node.d.ts +61 -7
- package/lib/board-live-cards-node.js +7 -7
- package/lib/board-live-cards-public.cjs +1 -1
- package/lib/board-live-cards-public.js +1 -1
- package/lib/board-live-cards-server-runtime.cjs +1 -1
- package/lib/board-live-cards-server-runtime.d.cts +1 -1
- package/lib/board-live-cards-server-runtime.d.ts +1 -1
- package/lib/board-live-cards-server-runtime.js +1 -1
- package/lib/board-livegraph-runtime/index.cjs +1 -1
- package/lib/board-livegraph-runtime/index.d.cts +1 -0
- package/lib/board-livegraph-runtime/index.d.ts +1 -0
- package/lib/board-livegraph-runtime/index.js +1 -1
- package/lib/chunk-36QUKFL7.cjs +3 -0
- package/lib/{chunk-YGKDQLYP.js → chunk-4HIEOBJC.js} +2 -2
- package/lib/chunk-7QQFDYBM.js +3 -0
- package/lib/chunk-ABAVFLDP.js +7 -0
- package/lib/chunk-H22NK6KH.cjs +7 -0
- package/lib/chunk-O6II7S4M.js +3 -0
- package/lib/chunk-PN5D32NP.cjs +3 -0
- package/lib/{chunk-S6DRP2HX.cjs → chunk-XQAHHUZO.cjs} +2 -2
- package/lib/chunk-ZENTBLLA.cjs +3 -0
- package/lib/chunk-ZWVT24YW.js +3 -0
- package/lib/cloud-storage.cjs +1 -1
- package/lib/cloud-storage.js +1 -1
- package/lib/firestore-storage/index.cjs +2 -2
- package/lib/firestore-storage/index.d.cts +2 -13
- package/lib/firestore-storage/index.d.ts +2 -13
- package/lib/firestore-storage/index.js +2 -2
- package/lib/index.cjs +2 -2
- package/lib/index.js +1 -1
- package/lib/localstorage-storage/index.cjs +1 -1
- package/lib/localstorage-storage/index.js +1 -1
- package/lib/server-jobs-queue-runner/index.d.cts +1 -1
- package/lib/server-jobs-queue-runner/index.d.ts +1 -1
- package/lib/server-runtime/index.cjs +1 -1
- package/lib/server-runtime/index.d.cts +2 -2
- package/lib/server-runtime/index.d.ts +2 -2
- package/lib/server-runtime/index.js +1 -1
- package/lib/server-runtime-agentface/index.d.cts +1 -1
- package/lib/server-runtime-agentface/index.d.ts +1 -1
- package/lib/server-runtime-controlface/index.cjs +1 -1
- package/lib/server-runtime-controlface/index.d.cts +1 -1
- package/lib/server-runtime-controlface/index.d.ts +1 -1
- package/lib/server-runtime-controlface/index.js +1 -1
- package/lib/server-runtime-core/index.d.cts +3 -3
- package/lib/server-runtime-core/index.d.ts +3 -3
- package/lib/server-runtime-watchers/index.cjs +1 -1
- package/lib/server-runtime-watchers/index.d.cts +3 -3
- package/lib/server-runtime-watchers/index.d.ts +3 -3
- package/lib/server-runtime-watchers/index.js +1 -1
- package/lib/server-runtime-webhooks/index.d.cts +1 -1
- package/lib/server-runtime-webhooks/index.d.ts +1 -1
- package/lib/{sse-hub-CYXisfXJ.d.cts → sse-hub-BDjWI7JR.d.cts} +1 -1
- package/lib/{sse-hub-Dodwtc3_.d.ts → sse-hub-DM8bw-dO.d.ts} +1 -1
- package/lib/{types-BtH3scgE.d.ts → types-BsfXZyI3.d.ts} +1 -1
- package/lib/{types-Ch0u3FKP.d.cts → types-CPnYv7RC.d.cts} +1 -1
- package/package.json +1 -1
- package/examples/board/test/sse-worker.js +0 -49
- package/lib/chunk-2GSI6C45.js +0 -7
- package/lib/chunk-CMFD27ZC.cjs +0 -3
- package/lib/chunk-DOFNXJ4C.js +0 -3
- package/lib/chunk-GU3T75C4.js +0 -3
- package/lib/chunk-H3EHFCDZ.js +0 -3
- package/lib/chunk-HEEDJEKM.js +0 -2
- package/lib/chunk-IQIZA7TN.cjs +0 -7
- package/lib/chunk-NDAKMJQK.cjs +0 -3
- package/lib/chunk-O4RKTQBP.cjs +0 -3
- package/lib/chunk-PBCDDO4V.cjs +0 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Targets the 'live' board with --cards-pattern cardT* to load only the 3
|
|
7
7
|
* test cards (cardT-portfolio, cardT-market-prices, cardT-portfolio-value).
|
|
8
8
|
*
|
|
9
|
-
* T0: /sse
|
|
9
|
+
* T0: /sse streaming connect → upsert fixtures → wait for all cards to complete
|
|
10
10
|
* T1: PATCH holdings (+1 row) → verify recomputation (holdings +1, positions +1)
|
|
11
11
|
*
|
|
12
12
|
* Usage:
|
|
@@ -44,6 +44,7 @@ function isCopilotAvailable() {
|
|
|
44
44
|
|
|
45
45
|
const skipT3a = cliArgs.includes('--skip-t3a') || !isCopilotAvailable();
|
|
46
46
|
const skipT3b = cliArgs.includes('--skip-t3b');
|
|
47
|
+
const skipT3e = cliArgs.includes('--skip-t3e');
|
|
47
48
|
const skipT3d = cliArgs.includes('--skip-t3d');
|
|
48
49
|
const RUN_ID = `run-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
50
|
|
|
@@ -104,6 +105,7 @@ const NS = {
|
|
|
104
105
|
statusGeneration: 0,
|
|
105
106
|
computedValues: {},
|
|
106
107
|
chatEvents: [],
|
|
108
|
+
allChatNotifications: [],
|
|
107
109
|
boardEvents: [],
|
|
108
110
|
};
|
|
109
111
|
|
|
@@ -168,6 +170,39 @@ function parseSseBlocks(buffer) {
|
|
|
168
170
|
return { payloads, remainder: buf };
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
function parseRawSseBlocks(buffer) {
|
|
174
|
+
const frames = [];
|
|
175
|
+
let buf = buffer;
|
|
176
|
+
while (true) {
|
|
177
|
+
const idx = buf.indexOf('\n\n');
|
|
178
|
+
if (idx === -1) break;
|
|
179
|
+
const block = buf.slice(0, idx);
|
|
180
|
+
buf = buf.slice(idx + 2);
|
|
181
|
+
if (!block.trim()) continue;
|
|
182
|
+
const frame = { id: null, event: null, data: '', payload: null, raw: block };
|
|
183
|
+
const dataLines = [];
|
|
184
|
+
for (const line of block.split('\n')) {
|
|
185
|
+
if (line.startsWith(':')) continue;
|
|
186
|
+
if (line.startsWith('id:')) {
|
|
187
|
+
frame.id = line.slice(3).trim();
|
|
188
|
+
} else if (line.startsWith('event:')) {
|
|
189
|
+
frame.event = line.slice(6).trim();
|
|
190
|
+
} else if (line.startsWith('data:')) {
|
|
191
|
+
dataLines.push(line.slice(5).replace(/^ /, ''));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
frame.data = dataLines.join('\n');
|
|
195
|
+
if (!frame.id && !frame.event && !frame.data) continue;
|
|
196
|
+
if (frame.data) {
|
|
197
|
+
try {
|
|
198
|
+
frame.payload = JSON.parse(frame.data);
|
|
199
|
+
} catch { /* ignore malformed */ }
|
|
200
|
+
}
|
|
201
|
+
frames.push(frame);
|
|
202
|
+
}
|
|
203
|
+
return { frames, remainder: buf };
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
function startSseClient(sseUrl, onPayload) {
|
|
172
207
|
const req = http.get(sseUrl, (res) => {
|
|
173
208
|
let buf = '';
|
|
@@ -187,9 +222,127 @@ function startSseClient(sseUrl, onPayload) {
|
|
|
187
222
|
};
|
|
188
223
|
}
|
|
189
224
|
|
|
225
|
+
function startRawSseClient({ sseUrl, headers = {}, onResponse, onFrame, onClose, onError }) {
|
|
226
|
+
let closed = false;
|
|
227
|
+
const req = http.request(sseUrl, { headers }, (res) => {
|
|
228
|
+
let buf = '';
|
|
229
|
+
res.setEncoding('utf-8');
|
|
230
|
+
try { onResponse?.(res); } catch { /* ignore */ }
|
|
231
|
+
res.on('data', (chunk) => {
|
|
232
|
+
buf = normalizeSseChunkBuffer(buf, chunk);
|
|
233
|
+
const parsed = parseRawSseBlocks(buf);
|
|
234
|
+
buf = parsed.remainder;
|
|
235
|
+
for (const frame of parsed.frames) {
|
|
236
|
+
try { onFrame?.(frame, res); } catch { /* ignore */ }
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
const closeOnce = () => {
|
|
240
|
+
if (closed) return;
|
|
241
|
+
closed = true;
|
|
242
|
+
try { onClose?.(res); } catch { /* ignore */ }
|
|
243
|
+
};
|
|
244
|
+
res.on('end', closeOnce);
|
|
245
|
+
res.on('close', closeOnce);
|
|
246
|
+
res.on('error', (err) => {
|
|
247
|
+
try { onError?.(err); } catch { /* ignore */ }
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
req.on('error', (err) => {
|
|
251
|
+
try { onError?.(err); } catch { /* ignore */ }
|
|
252
|
+
});
|
|
253
|
+
req.end();
|
|
254
|
+
return {
|
|
255
|
+
close() {
|
|
256
|
+
try { req.destroy(); } catch { /* ignore */ }
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function waitForRawSseFrames({ sseUrl, headers = {}, until, timeoutMs = 15_000, waitForClose = false }) {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
const state = { statusCode: null, headers: {}, frames: [], closed: false };
|
|
264
|
+
let settled = false;
|
|
265
|
+
let client = null;
|
|
266
|
+
const predicate = typeof until === 'function' ? until : ((current) => current.frames.length > 0);
|
|
267
|
+
|
|
268
|
+
function maybeResolve() {
|
|
269
|
+
if (settled) return;
|
|
270
|
+
if (!predicate(state)) return;
|
|
271
|
+
if (waitForClose && !state.closed) return;
|
|
272
|
+
settled = true;
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
if (!waitForClose && client) client.close();
|
|
275
|
+
resolve(state);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const timeout = setTimeout(() => {
|
|
279
|
+
if (settled) return;
|
|
280
|
+
settled = true;
|
|
281
|
+
if (client) client.close();
|
|
282
|
+
reject(new Error(`Timeout (${timeoutMs}ms) waiting for raw SSE frames`));
|
|
283
|
+
}, timeoutMs);
|
|
284
|
+
|
|
285
|
+
client = startRawSseClient({
|
|
286
|
+
sseUrl,
|
|
287
|
+
headers,
|
|
288
|
+
onResponse(res) {
|
|
289
|
+
state.statusCode = res.statusCode ?? null;
|
|
290
|
+
state.headers = res.headers || {};
|
|
291
|
+
maybeResolve();
|
|
292
|
+
},
|
|
293
|
+
onFrame(frame) {
|
|
294
|
+
state.frames.push(frame);
|
|
295
|
+
maybeResolve();
|
|
296
|
+
},
|
|
297
|
+
onClose() {
|
|
298
|
+
state.closed = true;
|
|
299
|
+
maybeResolve();
|
|
300
|
+
},
|
|
301
|
+
onError(err) {
|
|
302
|
+
if (settled) return;
|
|
303
|
+
settled = true;
|
|
304
|
+
clearTimeout(timeout);
|
|
305
|
+
reject(err);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function waitForFirstSsePayload(sseUrl, timeoutMs = 15_000) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
let settled = false;
|
|
314
|
+
let client = null;
|
|
315
|
+
const timeout = setTimeout(() => {
|
|
316
|
+
if (settled) return;
|
|
317
|
+
settled = true;
|
|
318
|
+
if (client) client.close();
|
|
319
|
+
reject(new Error(`Timeout (${timeoutMs}ms) waiting for: first SSE payload`));
|
|
320
|
+
}, timeoutMs);
|
|
321
|
+
|
|
322
|
+
client = startSseClient(sseUrl, (payload) => {
|
|
323
|
+
if (settled) return;
|
|
324
|
+
settled = true;
|
|
325
|
+
clearTimeout(timeout);
|
|
326
|
+
client.close();
|
|
327
|
+
resolve(payload);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
190
332
|
function captureChatEvents(payload, cardId) {
|
|
191
333
|
if (!payload || payload.kind !== 'notification-batch' || !Array.isArray(payload.notifications)) return;
|
|
192
334
|
for (const n of payload.notifications) {
|
|
335
|
+
if (n && n.kind === 'card_chats' && n.cardId) {
|
|
336
|
+
const messages = Array.isArray(n.messages) ? n.messages : [];
|
|
337
|
+
NS.allChatNotifications.push({
|
|
338
|
+
at: Date.now(),
|
|
339
|
+
cardId: n.cardId,
|
|
340
|
+
processing: !!n.processing,
|
|
341
|
+
receiving: !!n.receiving,
|
|
342
|
+
messageCount: messages.length,
|
|
343
|
+
messages,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
193
346
|
if (n && n.kind === 'card_chats' && n.cardId === cardId) {
|
|
194
347
|
const messages = Array.isArray(n.messages) ? n.messages : [];
|
|
195
348
|
NS.chatEvents.push({
|
|
@@ -234,10 +387,159 @@ function waitUntil(predicate, timeoutMs, label) {
|
|
|
234
387
|
});
|
|
235
388
|
}
|
|
236
389
|
|
|
390
|
+
function waitUntilAsync(predicate, timeoutMs, label) {
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
const deadline = Date.now() + timeoutMs;
|
|
393
|
+
const interval = setInterval(async () => {
|
|
394
|
+
let result;
|
|
395
|
+
try { result = await predicate(); } catch { /* retry */ }
|
|
396
|
+
if (result !== undefined && result !== null && result !== false) {
|
|
397
|
+
clearInterval(interval);
|
|
398
|
+
resolve(result);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (Date.now() > deadline) {
|
|
402
|
+
clearInterval(interval);
|
|
403
|
+
reject(new Error(`Timeout (${timeoutMs}ms) waiting for: ${label}`));
|
|
404
|
+
}
|
|
405
|
+
}, 150);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
237
409
|
function wait(ms) {
|
|
238
410
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
239
411
|
}
|
|
240
412
|
|
|
413
|
+
function extractStatusSummaryFromPayload(payload) {
|
|
414
|
+
if (payload?.statusSnapshot?.summary && typeof payload.statusSnapshot.summary === 'object') {
|
|
415
|
+
return payload.statusSnapshot.summary;
|
|
416
|
+
}
|
|
417
|
+
if (payload?.kind === 'notification-batch' && Array.isArray(payload.notifications)) {
|
|
418
|
+
for (const notification of payload.notifications) {
|
|
419
|
+
if (notification?.kind === 'status' && notification.status?.summary && typeof notification.status.summary === 'object') {
|
|
420
|
+
return notification.status.summary;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeHydratedChatMessages(messages) {
|
|
428
|
+
return (Array.isArray(messages) ? messages : []).map((message) => ({
|
|
429
|
+
role: String(message?.role || ''),
|
|
430
|
+
text: String(message?.text || ''),
|
|
431
|
+
files: Array.isArray(message?.files) ? message.files : [],
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readHeaderValue(headers, name) {
|
|
436
|
+
const raw = headers?.[name.toLowerCase()];
|
|
437
|
+
return Array.isArray(raw) ? String(raw[0] || '') : String(raw || '');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function buildTsStaticCard(cardId, label) {
|
|
441
|
+
return {
|
|
442
|
+
id: cardId,
|
|
443
|
+
meta: {
|
|
444
|
+
title: label,
|
|
445
|
+
tags: ['ts', 'sse'],
|
|
446
|
+
desc: `${label} disposable SSE delta card`,
|
|
447
|
+
},
|
|
448
|
+
compute: [],
|
|
449
|
+
view: {
|
|
450
|
+
elements: [
|
|
451
|
+
{
|
|
452
|
+
kind: 'markdown',
|
|
453
|
+
data: {
|
|
454
|
+
bind: 'card_data.text',
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
layout: {
|
|
459
|
+
board: {
|
|
460
|
+
col: 2,
|
|
461
|
+
order: 99,
|
|
462
|
+
},
|
|
463
|
+
canvas: {
|
|
464
|
+
x: 1600,
|
|
465
|
+
y: 600,
|
|
466
|
+
w: 260,
|
|
467
|
+
h: 120,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
features: {},
|
|
471
|
+
},
|
|
472
|
+
card_data: {
|
|
473
|
+
text: label,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildChatProbeCard(cardId, label) {
|
|
479
|
+
return {
|
|
480
|
+
id: cardId,
|
|
481
|
+
meta: {
|
|
482
|
+
title: label,
|
|
483
|
+
tags: ['chat', 'probe'],
|
|
484
|
+
desc: `${label} disposable chat probe card`,
|
|
485
|
+
},
|
|
486
|
+
provides: [
|
|
487
|
+
{
|
|
488
|
+
bindTo: 'holdings',
|
|
489
|
+
ref: 'card_data.holdings',
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
compute: [],
|
|
493
|
+
view: {
|
|
494
|
+
elements: [
|
|
495
|
+
{
|
|
496
|
+
kind: 'editable-table',
|
|
497
|
+
label: 'Holdings',
|
|
498
|
+
data: {
|
|
499
|
+
bind: 'card_data.holdings',
|
|
500
|
+
writeTo: 'card_data.holdings',
|
|
501
|
+
columns: ['ticker', 'quantity', 'cost_basis'],
|
|
502
|
+
schema: {
|
|
503
|
+
properties: {
|
|
504
|
+
quantity: { type: 'number' },
|
|
505
|
+
cost_basis: { type: 'number' },
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
layout: {
|
|
512
|
+
board: {
|
|
513
|
+
col: 3,
|
|
514
|
+
order: 98,
|
|
515
|
+
},
|
|
516
|
+
canvas: {
|
|
517
|
+
x: 1400,
|
|
518
|
+
y: 420,
|
|
519
|
+
w: 320,
|
|
520
|
+
h: 260,
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
features: {
|
|
524
|
+
chat: true,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
card_data: {
|
|
528
|
+
holdings: [
|
|
529
|
+
{ ticker: 'AAPL', quantity: 1, cost_basis: 150 },
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function assertObjectContains(actual, expected, label) {
|
|
536
|
+
assert(actual && typeof actual === 'object', `${label} actual value is not an object`);
|
|
537
|
+
assert(expected && typeof expected === 'object', `${label} expected value is not an object`);
|
|
538
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
539
|
+
assert(Object.is(actual[key], value), `${label}.${key} mismatch: expected ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
241
543
|
async function stopChildProcess(proc, label) {
|
|
242
544
|
if (!proc) return;
|
|
243
545
|
if (proc.exitCode !== null) return;
|
|
@@ -316,13 +618,21 @@ function deriveProbeLifecycleMilestones(events, opts) {
|
|
|
316
618
|
|
|
317
619
|
function matchOrderedProbeLifecycle(events, opts) {
|
|
318
620
|
const milestones = deriveProbeLifecycleMilestones(events, opts);
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
621
|
+
const userIdx = milestones.indexOf('user');
|
|
622
|
+
const processingTrueIdx = milestones.indexOf('processing-true');
|
|
623
|
+
const assistantIdx = milestones.indexOf('assistant');
|
|
624
|
+
const processingFalseIdx = milestones.lastIndexOf('processing-false');
|
|
625
|
+
const inProgressIdx = milestones.indexOf('in-progress');
|
|
626
|
+
|
|
627
|
+
if (userIdx === -1 || processingTrueIdx === -1 || assistantIdx === -1 || processingFalseIdx === -1) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (Math.max(userIdx, processingTrueIdx) >= assistantIdx) return false;
|
|
632
|
+
if (assistantIdx >= processingFalseIdx) return false;
|
|
633
|
+
if (inProgressIdx !== -1 && (inProgressIdx <= processingTrueIdx || inProgressIdx >= assistantIdx)) return false;
|
|
634
|
+
|
|
635
|
+
return { milestones };
|
|
326
636
|
}
|
|
327
637
|
|
|
328
638
|
function httpGet(url) {
|
|
@@ -508,7 +818,7 @@ let chatSseClient = null;
|
|
|
508
818
|
let chatSseClientId = '';
|
|
509
819
|
|
|
510
820
|
try {
|
|
511
|
-
// ── T0:
|
|
821
|
+
// ── T0: streaming SSE connect, upsert fixtures, wait for initial completion ──
|
|
512
822
|
|
|
513
823
|
// Register the 'live' board via POST (v8 runtime requires explicit registration)
|
|
514
824
|
const regRes = await httpJson('POST', `http://127.0.0.1:${PORT}/api/boards`, { id: BOARD_ID, label: 'Live' });
|
|
@@ -784,12 +1094,12 @@ try {
|
|
|
784
1094
|
const t2AfterMessages = Array.isArray(t2After.data?.data?.messages) ? t2After.data.data.messages : [];
|
|
785
1095
|
const t2NewMessages = t2AfterMessages.slice(t2BeforeCount);
|
|
786
1096
|
t3Dbg(`step 6: validating ${t2NewMessages.length} new messages`);
|
|
787
|
-
assert(t2NewMessages.length >=
|
|
1097
|
+
assert(t2NewMessages.length >= 2, `T3 expected at least 2 new chat messages, got ${t2NewMessages.length}`);
|
|
788
1098
|
const t3McpAfter = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, turn_id: t3TurnId });
|
|
789
1099
|
const t3McpAfterData = expectMcpSuccess(t3McpAfter, 'T3 MCP post chats');
|
|
790
1100
|
const t3TurnMessages = Array.isArray(t3McpAfterData?.messages) ? t3McpAfterData.messages : [];
|
|
791
1101
|
t3Dbg(`step 6: MCP turn messages count=${t3TurnMessages.length}`);
|
|
792
|
-
assert(t3TurnMessages.length >=
|
|
1102
|
+
assert(t3TurnMessages.length >= 2, `T3 expected at least 2 MCP messages for turn ${t3TurnId}, got ${t3TurnMessages.length}`);
|
|
793
1103
|
for (const msg of t3TurnMessages) {
|
|
794
1104
|
assert(String(msg?.turn || '') === t3TurnId, 'T3 MCP turn id mismatch');
|
|
795
1105
|
}
|
|
@@ -807,7 +1117,6 @@ try {
|
|
|
807
1117
|
const t2AssistantMsg = t2NewMessages.find((m) => m?.role === 'assistant');
|
|
808
1118
|
assert(!!t2User && typeof t2User.id === 'string', 'T3 user chat message missing id');
|
|
809
1119
|
assert(String(t2User?.text || '').includes(t2ProbePrompt), 'T3 user file text mismatch');
|
|
810
|
-
assert(!!t2InProgress && typeof t2InProgress.id === 'string', 'T3 in-progress system message missing id');
|
|
811
1120
|
assert(!!t2AssistantMsg && typeof t2AssistantMsg.id === 'string', 'T3 assistant chat message missing id');
|
|
812
1121
|
assert(String(t2AssistantMsg?.text || '').includes(`Echo: ${t2ProbePrompt}`), 'T3 assistant echo file content mismatch');
|
|
813
1122
|
t3Dbg('step 6: all assertions passed');
|
|
@@ -967,18 +1276,314 @@ try {
|
|
|
967
1276
|
assert(t2bAfter.status === 200, `T3b post chats returned ${t2bAfter.status}`);
|
|
968
1277
|
const t2bAfterMessages = Array.isArray(t2bAfter.data?.data?.messages) ? t2bAfter.data.data.messages : [];
|
|
969
1278
|
const t2bNewMessages = t2bAfterMessages.slice(t2bSendBaseline);
|
|
970
|
-
assert(t2bNewMessages.length >=
|
|
1279
|
+
assert(t2bNewMessages.length >= 2, `T3b expected at least 2 chat messages after send, got ${t2bNewMessages.length}`);
|
|
971
1280
|
|
|
972
1281
|
const t2bUser = t2bNewMessages.find((m) => m?.role === 'user');
|
|
973
1282
|
const t2bInProgress = t2bNewMessages.find((m) => m?.role === 'system' && String(m?.text || '').trim().toLowerCase() === PROBE_IN_PROGRESS_TEXT);
|
|
974
1283
|
const t2bAssistantMsg = t2bNewMessages.find((m) => m?.role === 'assistant');
|
|
975
1284
|
|
|
976
1285
|
assert(!!t2bUser && typeof t2bUser.id === 'string', 'T3b missing user chat message notification');
|
|
977
|
-
assert(!!t2bInProgress && typeof t2bInProgress.id === 'string', 'T3b missing in-progress system chat message');
|
|
978
1286
|
assert(!!t2bAssistantMsg && typeof t2bAssistantMsg.id === 'string', 'T3b missing assistant chat message notification');
|
|
979
1287
|
assert(!Array.isArray(t2bUser?.files) || t2bUser.files.length === 0, 'T3b user chat message should remain text-only after add-chat-attachment upload');
|
|
980
1288
|
assert(String(t2bAssistantMsg?.text || '').includes(`Echo: ${t2bPrompt}`), 'T3b assistant probe echo mismatch');
|
|
981
1289
|
console.log('[T3b] ok: add-chat-attachment upload plus text-only chat-send preserved the normal probe lifecycle');
|
|
1290
|
+
|
|
1291
|
+
if (skipT3e) {
|
|
1292
|
+
console.log('\n=== T3e: skipped (--skip-t3e) ===');
|
|
1293
|
+
} else {
|
|
1294
|
+
console.log('\n=== T3e: subscribed chat turn with attachment plus unsubscribed negative case ===');
|
|
1295
|
+
const t3eOtherCardId = `card-t3e-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1296
|
+
const t3eOtherCard = buildChatProbeCard(t3eOtherCardId, 'T3e Chat Probe');
|
|
1297
|
+
|
|
1298
|
+
const t3eUpsertOtherRes = await httpMcp('manage.upsert-card', {
|
|
1299
|
+
card_id: t3eOtherCardId,
|
|
1300
|
+
candidate_card_content: t3eOtherCard,
|
|
1301
|
+
});
|
|
1302
|
+
assert(t3eUpsertOtherRes.status === 200, `T3e manage.upsert-card(${t3eOtherCardId}) returned ${t3eUpsertOtherRes.status}`);
|
|
1303
|
+
assert(t3eUpsertOtherRes.data?.status === 'success', `T3e manage.upsert-card(${t3eOtherCardId}) failed: ${JSON.stringify(t3eUpsertOtherRes.data)}`);
|
|
1304
|
+
await waitUntil(() => {
|
|
1305
|
+
const s = NS.statusSummary;
|
|
1306
|
+
if (s && s.card_count === T0_EXPECTED_CARD_IDS.length + 1) return s;
|
|
1307
|
+
return false;
|
|
1308
|
+
}, 30_000, 'T3e extra chat card visible in board summary');
|
|
1309
|
+
|
|
1310
|
+
try {
|
|
1311
|
+
const t3eBefore = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, all_turns: true });
|
|
1312
|
+
assert(t3eBefore.status === 200, `T3e pre chats returned ${t3eBefore.status}`);
|
|
1313
|
+
const t3eBeforeMessages = Array.isArray(t3eBefore.data?.data?.messages) ? t3eBefore.data.data.messages : [];
|
|
1314
|
+
const t3eBeforeCount = t3eBeforeMessages.length;
|
|
1315
|
+
|
|
1316
|
+
const t3eTurnId = randomTurnId();
|
|
1317
|
+
const t3eUploadRes = await httpMcpControlplane('manage.add-chat-attachment', {
|
|
1318
|
+
board_id: BOARD_ID,
|
|
1319
|
+
card_id: CHAT_CARD_ID,
|
|
1320
|
+
turn_id: t3eTurnId,
|
|
1321
|
+
file_name: 't3e-probe.txt',
|
|
1322
|
+
content_type: 'text/plain; charset=utf-8',
|
|
1323
|
+
text: 'what is the capital of japan',
|
|
1324
|
+
});
|
|
1325
|
+
assert(t3eUploadRes.status === 200, `T3e file upload returned ${t3eUploadRes.status}`);
|
|
1326
|
+
assert(t3eUploadRes.data?.status === 'success', `T3e file upload failed: ${JSON.stringify(t3eUploadRes.data)}`);
|
|
1327
|
+
const t3eUploadedFile = t3eUploadRes.data?.data?.files?.[0];
|
|
1328
|
+
assert(t3eUploadedFile && typeof t3eUploadedFile === 'object', 'T3e upload response missing file metadata');
|
|
1329
|
+
assert(!Object.prototype.hasOwnProperty.call(t3eUploadedFile, 'path'), 'T3e uploaded file metadata should not expose path');
|
|
1330
|
+
|
|
1331
|
+
const t3eCardAfterUpload = await httpMcp('manage.read-card', { card_id: CHAT_CARD_ID });
|
|
1332
|
+
assert(t3eCardAfterUpload.status === 200, `T3e card read after upload returned ${t3eCardAfterUpload.status}`);
|
|
1333
|
+
const t3eStoredFiles = Array.isArray(t3eCardAfterUpload.data?.data?.[0]?.card_data?.files)
|
|
1334
|
+
? t3eCardAfterUpload.data.data[0].card_data.files
|
|
1335
|
+
: [];
|
|
1336
|
+
const t3eStoredFile = t3eStoredFiles.find((file) => String(file?.stored_name || '') === String(t3eUploadedFile?.stored_name || ''));
|
|
1337
|
+
assert(!!t3eStoredFile, 'T3e stored file metadata missing after upload');
|
|
1338
|
+
assert(t3eStoredFile?.chat === true, 'T3e stored file should be marked as chat-origin');
|
|
1339
|
+
assert(!Object.prototype.hasOwnProperty.call(t3eStoredFile || {}, 'path'), 'T3e stored file metadata should not expose path');
|
|
1340
|
+
|
|
1341
|
+
const t3eUploadMessages = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, turn_id: t3eTurnId });
|
|
1342
|
+
assert(t3eUploadMessages.status === 200, `T3e chats after upload returned ${t3eUploadMessages.status}`);
|
|
1343
|
+
const t3eUploadTurnMessages = Array.isArray(t3eUploadMessages.data?.data?.messages) ? t3eUploadMessages.data.data.messages : [];
|
|
1344
|
+
const t3eUploadSystem = t3eUploadTurnMessages.find((message) => message?.role === 'system');
|
|
1345
|
+
assert(!!t3eUploadSystem, 'T3e upload protocol missing system chat message');
|
|
1346
|
+
assert(String(t3eUploadSystem?.text || '').toLowerCase().includes('file uploaded:'), 'T3e upload system message does not describe uploaded file');
|
|
1347
|
+
|
|
1348
|
+
const t3eEventStart = NS.chatEvents.length;
|
|
1349
|
+
const t3eAllNotificationsStart = NS.allChatNotifications.length;
|
|
1350
|
+
const t3ePrompt = `attachment probe ${Date.now()}`;
|
|
1351
|
+
const t3eProbeText = `${ECHO_PROBE_MARKER}${t3ePrompt}${ECHO_PROBE_MARKER}`;
|
|
1352
|
+
const t3eSendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1353
|
+
tool: 'chat-send',
|
|
1354
|
+
args: {
|
|
1355
|
+
card_id: CHAT_CARD_ID,
|
|
1356
|
+
payload: {
|
|
1357
|
+
text: t3eProbeText,
|
|
1358
|
+
'turn-id': t3eTurnId,
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
assert(t3eSendRes.status === 200, `T3e chat-send returned ${t3eSendRes.status}`);
|
|
1363
|
+
|
|
1364
|
+
const t3eLifecycle = await waitForChatPredicate((events) => {
|
|
1365
|
+
return matchOrderedProbeLifecycle(events.slice(t3eEventStart), {
|
|
1366
|
+
beforeCount: t3eBeforeCount + t3eUploadTurnMessages.length,
|
|
1367
|
+
beforeProcessing: false,
|
|
1368
|
+
prompt: t3ePrompt,
|
|
1369
|
+
inProgressText: PROBE_IN_PROGRESS_TEXT,
|
|
1370
|
+
});
|
|
1371
|
+
}, 60_000, 'T3e ordered lifecycle');
|
|
1372
|
+
assert(!!t3eLifecycle, 'T3e ordered lifecycle not observed');
|
|
1373
|
+
|
|
1374
|
+
const t3eAfter = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, turn_id: t3eTurnId });
|
|
1375
|
+
assert(t3eAfter.status === 200, `T3e post chats returned ${t3eAfter.status}`);
|
|
1376
|
+
const t3eFinalMessages = Array.isArray(t3eAfter.data?.data?.messages) ? t3eAfter.data.data.messages : [];
|
|
1377
|
+
const t3eFinalUser = t3eFinalMessages.find((message) => message?.role === 'user');
|
|
1378
|
+
const t3eFinalAssistant = t3eFinalMessages.find((message) => message?.role === 'assistant');
|
|
1379
|
+
assert(!!t3eFinalUser, `T3e final user message missing: ${JSON.stringify(t3eFinalMessages)}`);
|
|
1380
|
+
assert(!!t3eFinalAssistant, `T3e final assistant message missing: ${JSON.stringify(t3eFinalMessages)}`);
|
|
1381
|
+
assert(String(t3eFinalUser?.text || '') === t3ePrompt, `T3e final user text mismatch: ${JSON.stringify(t3eFinalUser)}`);
|
|
1382
|
+
assert(!Array.isArray(t3eFinalUser?.files) || t3eFinalUser.files.length === 0,
|
|
1383
|
+
`T3e final user message should remain text-only after controlplane attachment upload: ${JSON.stringify(t3eFinalUser)}`);
|
|
1384
|
+
assert(String(t3eFinalAssistant?.text || '').includes(`Echo: ${t3ePrompt}`), `T3e final probe reply mismatch: ${JSON.stringify(t3eFinalAssistant)}`);
|
|
1385
|
+
|
|
1386
|
+
const t3eNegativeTurnId = randomTurnId();
|
|
1387
|
+
const t3eNegativeSendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1388
|
+
tool: 'chat-send',
|
|
1389
|
+
args: {
|
|
1390
|
+
card_id: t3eOtherCardId,
|
|
1391
|
+
payload: {
|
|
1392
|
+
text: `${ECHO_PROBE_MARKER}negative unsubscribed ${Date.now()}${ECHO_PROBE_MARKER}`,
|
|
1393
|
+
'turn-id': t3eNegativeTurnId,
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
});
|
|
1397
|
+
assert(t3eNegativeSendRes.status === 200, `T3e negative chat-send returned ${t3eNegativeSendRes.status}`);
|
|
1398
|
+
|
|
1399
|
+
const t3eNegativePersisted = await waitUntilAsync(async () => {
|
|
1400
|
+
const result = await httpMcp('inspect.chat-messages-on-cards', { card_id: t3eOtherCardId, turn_id: t3eNegativeTurnId });
|
|
1401
|
+
if (result.status !== 200) return false;
|
|
1402
|
+
const messages = Array.isArray(result.data?.data?.messages) ? result.data.data.messages : [];
|
|
1403
|
+
return messages.find((message) => message?.role === 'assistant') ? messages : false;
|
|
1404
|
+
}, 60_000, 'T3e negative turn persisted on unsubscribed card');
|
|
1405
|
+
assert(Array.isArray(t3eNegativePersisted), 'T3e negative turn did not persist as expected');
|
|
1406
|
+
|
|
1407
|
+
await wait(1_500);
|
|
1408
|
+
const t3eUnexpectedNotification = NS.allChatNotifications.slice(t3eAllNotificationsStart)
|
|
1409
|
+
.find((event) => event?.cardId === t3eOtherCardId);
|
|
1410
|
+
assert(!t3eUnexpectedNotification,
|
|
1411
|
+
`T3e unsubscribed client unexpectedly received chat notification for ${t3eOtherCardId}: ${JSON.stringify(t3eUnexpectedNotification)}`);
|
|
1412
|
+
|
|
1413
|
+
console.log('[T3e] ok: subscribed client received attachment-bearing turn and unsubscribed card produced no chat SSE notification');
|
|
1414
|
+
} finally {
|
|
1415
|
+
const t3eRemoveOtherRes = await httpMcp('manage.remove-card', { card_id: t3eOtherCardId });
|
|
1416
|
+
assert(t3eRemoveOtherRes.status === 200, `T3e manage.remove-card(${t3eOtherCardId}) returned ${t3eRemoveOtherRes.status}`);
|
|
1417
|
+
assert(t3eRemoveOtherRes.data?.status === 'success', `T3e manage.remove-card(${t3eOtherCardId}) failed: ${JSON.stringify(t3eRemoveOtherRes.data)}`);
|
|
1418
|
+
await waitUntil(() => {
|
|
1419
|
+
const s = NS.statusSummary;
|
|
1420
|
+
if (s && s.card_count === T0_EXPECTED_CARD_IDS.length) return s;
|
|
1421
|
+
return false;
|
|
1422
|
+
}, 30_000, 'T3e cleanup card_count back to 3');
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
console.log('\n=== T3c: fresh /sse connect hydrates current board state ===');
|
|
1427
|
+
const t3cInspectStatusRes = await httpMcp('inspect.board-runtime-status', {});
|
|
1428
|
+
assert(t3cInspectStatusRes.status === 200, `T3c inspect.board-runtime-status returned ${t3cInspectStatusRes.status}`);
|
|
1429
|
+
assert(t3cInspectStatusRes.data?.status === 'success', `T3c inspect.board-runtime-status failed: ${JSON.stringify(t3cInspectStatusRes.data)}`);
|
|
1430
|
+
const t3cExpectedSummary = t3cInspectStatusRes.data?.data?.summary;
|
|
1431
|
+
assert(t3cExpectedSummary, 'T3c summary missing from inspect.board-runtime-status');
|
|
1432
|
+
|
|
1433
|
+
const t3cExpectedCards = {};
|
|
1434
|
+
for (const cardId of T0_EXPECTED_CARD_IDS) {
|
|
1435
|
+
const t3cInspectCardRes = await httpMcp('inspect.card-definition-and-runtime', { card_id: cardId });
|
|
1436
|
+
assert(t3cInspectCardRes.status === 200, `T3c inspect.card-definition-and-runtime(${cardId}) returned ${t3cInspectCardRes.status}`);
|
|
1437
|
+
assert(t3cInspectCardRes.data?.status === 'success', `T3c inspect.card-definition-and-runtime(${cardId}) failed: ${JSON.stringify(t3cInspectCardRes.data)}`);
|
|
1438
|
+
const t3cInspectCardData = t3cInspectCardRes.data?.data;
|
|
1439
|
+
assert(t3cInspectCardData && typeof t3cInspectCardData === 'object', `T3c inspect.card-definition-and-runtime(${cardId}) missing data`);
|
|
1440
|
+
t3cExpectedCards[cardId] = t3cInspectCardData;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const t3cRefreshClientId = `server-http-refresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1444
|
+
const t3cRefreshPayload = await waitForFirstSsePayload(`${BASE}/sse?clientId=${encodeURIComponent(t3cRefreshClientId)}`);
|
|
1445
|
+
assert(t3cRefreshPayload && typeof t3cRefreshPayload === 'object', 'T3c missing refresh SSE payload');
|
|
1446
|
+
|
|
1447
|
+
const t3cCardDefinitions = Array.isArray(t3cRefreshPayload.cardDefinitions) ? t3cRefreshPayload.cardDefinitions : [];
|
|
1448
|
+
const t3cCardIds = t3cCardDefinitions.map((card) => card?.id).filter((id) => typeof id === 'string').sort();
|
|
1449
|
+
assert(JSON.stringify(t3cCardIds) === JSON.stringify(T0_EXPECTED_CARD_IDS),
|
|
1450
|
+
`T3c refreshed SSE cardDefinitions mismatch: ${JSON.stringify(t3cCardIds)}`);
|
|
1451
|
+
|
|
1452
|
+
const t3cStatusSummary = t3cRefreshPayload.statusSnapshot?.summary;
|
|
1453
|
+
assert(t3cStatusSummary, 'T3c refresh SSE payload missing statusSnapshot.summary');
|
|
1454
|
+
assertObjectContains(t3cStatusSummary, t3cExpectedSummary, 'T3c refresh SSE summary');
|
|
1455
|
+
|
|
1456
|
+
const t3cCardRuntimeById = t3cRefreshPayload.cardRuntimeById && typeof t3cRefreshPayload.cardRuntimeById === 'object'
|
|
1457
|
+
? t3cRefreshPayload.cardRuntimeById
|
|
1458
|
+
: {};
|
|
1459
|
+
for (const card of t3cCardDefinitions) {
|
|
1460
|
+
const cardId = card?.id;
|
|
1461
|
+
if (typeof cardId !== 'string' || !t3cExpectedCards[cardId]) continue;
|
|
1462
|
+
const t3cExpectedCard = t3cExpectedCards[cardId];
|
|
1463
|
+
assert(JSON.stringify(card) === JSON.stringify(t3cExpectedCard.card_definition_and_static_data),
|
|
1464
|
+
`T3c refresh SSE cardDefinitions[${cardId}] mismatch`);
|
|
1465
|
+
|
|
1466
|
+
const t3cHydratedCardRuntime = t3cCardRuntimeById[cardId];
|
|
1467
|
+
assert(t3cHydratedCardRuntime && typeof t3cHydratedCardRuntime === 'object', `T3c refresh SSE payload missing cardRuntimeById.${cardId}`);
|
|
1468
|
+
assert(JSON.stringify(t3cHydratedCardRuntime.card_data || {}) === JSON.stringify(t3cExpectedCard.card_definition_and_static_data?.card_data || {}),
|
|
1469
|
+
`T3c refresh SSE cardRuntimeById.${cardId}.card_data mismatch`);
|
|
1470
|
+
assert(JSON.stringify(t3cHydratedCardRuntime.computed_values || {}) === JSON.stringify(t3cExpectedCard.runtime_data?.computed_values || {}),
|
|
1471
|
+
`T3c refresh SSE cardRuntimeById.${cardId}.computed_values mismatch`);
|
|
1472
|
+
}
|
|
1473
|
+
console.log('[T3c] ok: fresh /sse first payload hydrated the current board state');
|
|
1474
|
+
|
|
1475
|
+
console.log('\n=== TS: one-shot, raw framing, replay, delta ordering, and chat hydration ===');
|
|
1476
|
+
const tsExpectedChatRes = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, all_turns: true });
|
|
1477
|
+
assert(tsExpectedChatRes.status === 200, `TS inspect.chat-messages-on-cards returned ${tsExpectedChatRes.status}`);
|
|
1478
|
+
const tsExpectedChatMessages = normalizeHydratedChatMessages(tsExpectedChatRes.data?.data?.messages || []);
|
|
1479
|
+
|
|
1480
|
+
const tsOneShot = await waitForRawSseFrames({
|
|
1481
|
+
sseUrl: `${BASE}/sse?one-shot`,
|
|
1482
|
+
timeoutMs: 15_000,
|
|
1483
|
+
until: (state) => state.frames.length >= 1,
|
|
1484
|
+
waitForClose: true,
|
|
1485
|
+
});
|
|
1486
|
+
assert(tsOneShot.statusCode === 200, `TS one-shot returned ${tsOneShot.statusCode}`);
|
|
1487
|
+
assert(/text\/event-stream/i.test(readHeaderValue(tsOneShot.headers, 'content-type')),
|
|
1488
|
+
`TS one-shot content-type mismatch: ${readHeaderValue(tsOneShot.headers, 'content-type')}`);
|
|
1489
|
+
assert(tsOneShot.closed === true, 'TS one-shot connection should close after first frame');
|
|
1490
|
+
assert(tsOneShot.frames.length === 1, `TS one-shot expected exactly 1 frame, got ${tsOneShot.frames.length}`);
|
|
1491
|
+
const tsOneShotFrame = tsOneShot.frames[0];
|
|
1492
|
+
assert(/^\d+$/.test(String(tsOneShotFrame.id || '')), `TS one-shot frame missing numeric id: ${JSON.stringify(tsOneShotFrame)}`);
|
|
1493
|
+
assert(tsOneShotFrame.payload && typeof tsOneShotFrame.payload === 'object', 'TS one-shot frame missing JSON payload');
|
|
1494
|
+
const tsOneShotChatState = tsOneShotFrame.payload.cardChatsByCardId?.[CHAT_CARD_ID];
|
|
1495
|
+
assert(tsOneShotChatState && typeof tsOneShotChatState === 'object', `TS one-shot payload missing cardChatsByCardId.${CHAT_CARD_ID}`);
|
|
1496
|
+
assert(JSON.stringify(normalizeHydratedChatMessages(tsOneShotChatState.messages)) === JSON.stringify(tsExpectedChatMessages),
|
|
1497
|
+
'TS one-shot cardChatsByCardId hydration mismatch');
|
|
1498
|
+
|
|
1499
|
+
const tsDeltaClientId = `ts-delta-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1500
|
+
const tsRawFrames = [];
|
|
1501
|
+
const tsDeltaClient = startRawSseClient({
|
|
1502
|
+
sseUrl: `${BASE}/sse?clientId=${encodeURIComponent(tsDeltaClientId)}`,
|
|
1503
|
+
onFrame(frame) {
|
|
1504
|
+
tsRawFrames.push(frame);
|
|
1505
|
+
},
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
try {
|
|
1509
|
+
const tsInitialFrame = await waitUntil(() => tsRawFrames[0] || false, 15_000, 'TS initial raw SSE frame');
|
|
1510
|
+
assert(/^\d+$/.test(String(tsInitialFrame.id || '')), `TS initial streaming frame missing numeric id: ${JSON.stringify(tsInitialFrame)}`);
|
|
1511
|
+
const tsInitialChatState = tsInitialFrame.payload?.cardChatsByCardId?.[CHAT_CARD_ID];
|
|
1512
|
+
assert(tsInitialChatState && typeof tsInitialChatState === 'object', `TS initial streaming payload missing cardChatsByCardId.${CHAT_CARD_ID}`);
|
|
1513
|
+
assert(JSON.stringify(normalizeHydratedChatMessages(tsInitialChatState.messages)) === JSON.stringify(tsExpectedChatMessages),
|
|
1514
|
+
'TS initial streaming cardChatsByCardId hydration mismatch');
|
|
1515
|
+
|
|
1516
|
+
const tsTempCards = [
|
|
1517
|
+
buildTsStaticCard(`card-ts-${Date.now()}-a`, 'TS Delta Card A'),
|
|
1518
|
+
buildTsStaticCard(`card-ts-${Date.now()}-b`, 'TS Delta Card B'),
|
|
1519
|
+
];
|
|
1520
|
+
let tsLastEventId = Number(tsInitialFrame.id);
|
|
1521
|
+
|
|
1522
|
+
for (let idx = 0; idx < tsTempCards.length; idx += 1) {
|
|
1523
|
+
const tsCard = tsTempCards[idx];
|
|
1524
|
+
const tsExpectedCardCount = T0_EXPECTED_CARD_IDS.length + idx + 1;
|
|
1525
|
+
const tsFrameStart = tsRawFrames.length;
|
|
1526
|
+
const tsUpsertRes = await httpMcp('manage.upsert-card', {
|
|
1527
|
+
card_id: tsCard.id,
|
|
1528
|
+
candidate_card_content: tsCard,
|
|
1529
|
+
});
|
|
1530
|
+
assert(tsUpsertRes.status === 200, `TS manage.upsert-card(${tsCard.id}) returned ${tsUpsertRes.status}`);
|
|
1531
|
+
assert(tsUpsertRes.data?.status === 'success', `TS manage.upsert-card(${tsCard.id}) failed: ${JSON.stringify(tsUpsertRes.data)}`);
|
|
1532
|
+
const tsDeltaFrame = await waitUntil(() => {
|
|
1533
|
+
for (const frame of tsRawFrames.slice(tsFrameStart)) {
|
|
1534
|
+
const summary = extractStatusSummaryFromPayload(frame.payload);
|
|
1535
|
+
if (summary?.card_count === tsExpectedCardCount) return frame;
|
|
1536
|
+
}
|
|
1537
|
+
return false;
|
|
1538
|
+
}, 30_000, `TS board delta card_count=${tsExpectedCardCount}`);
|
|
1539
|
+
assert(Number(tsDeltaFrame.id) > tsLastEventId,
|
|
1540
|
+
`TS delta frame id did not increase: prev=${tsLastEventId}, next=${JSON.stringify(tsDeltaFrame.id)}`);
|
|
1541
|
+
tsLastEventId = Number(tsDeltaFrame.id);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
tsDeltaClient.close();
|
|
1545
|
+
await wait(250);
|
|
1546
|
+
|
|
1547
|
+
const tsReconnect = await waitForRawSseFrames({
|
|
1548
|
+
sseUrl: `${BASE}/sse?clientId=${encodeURIComponent(tsDeltaClientId)}`,
|
|
1549
|
+
headers: { 'Last-Event-ID': String(tsLastEventId) },
|
|
1550
|
+
timeoutMs: 15_000,
|
|
1551
|
+
until: (state) => state.frames.length >= 1,
|
|
1552
|
+
});
|
|
1553
|
+
assert(tsReconnect.statusCode === 200, `TS reconnect returned ${tsReconnect.statusCode}`);
|
|
1554
|
+
assert(/text\/event-stream/i.test(readHeaderValue(tsReconnect.headers, 'content-type')),
|
|
1555
|
+
`TS reconnect content-type mismatch: ${readHeaderValue(tsReconnect.headers, 'content-type')}`);
|
|
1556
|
+
const tsReconnectFrame = tsReconnect.frames[0];
|
|
1557
|
+
assert(Number(tsReconnectFrame.id) > tsLastEventId,
|
|
1558
|
+
`TS reconnect frame id did not advance beyond Last-Event-ID: prev=${tsLastEventId}, next=${JSON.stringify(tsReconnectFrame.id)}`);
|
|
1559
|
+
const tsReconnectPayload = tsReconnectFrame.payload;
|
|
1560
|
+
assert(tsReconnectPayload && typeof tsReconnectPayload === 'object', 'TS reconnect first frame missing JSON payload');
|
|
1561
|
+
const tsReconnectIds = (Array.isArray(tsReconnectPayload.cardDefinitions) ? tsReconnectPayload.cardDefinitions : [])
|
|
1562
|
+
.map((card) => card?.id)
|
|
1563
|
+
.filter((cardId) => typeof cardId === 'string')
|
|
1564
|
+
.sort();
|
|
1565
|
+
const tsExpectedReconnectIds = [...T0_EXPECTED_CARD_IDS, ...tsTempCards.map((card) => card.id)].sort();
|
|
1566
|
+
assert(JSON.stringify(tsReconnectIds) === JSON.stringify(tsExpectedReconnectIds),
|
|
1567
|
+
`TS reconnect snapshot mismatch: expected ${JSON.stringify(tsExpectedReconnectIds)}, got ${JSON.stringify(tsReconnectIds)}`);
|
|
1568
|
+
const tsReconnectChatState = tsReconnectPayload.cardChatsByCardId?.[CHAT_CARD_ID];
|
|
1569
|
+
assert(tsReconnectChatState && typeof tsReconnectChatState === 'object', `TS reconnect payload missing cardChatsByCardId.${CHAT_CARD_ID}`);
|
|
1570
|
+
assert(JSON.stringify(normalizeHydratedChatMessages(tsReconnectChatState.messages)) === JSON.stringify(tsExpectedChatMessages),
|
|
1571
|
+
'TS reconnect cardChatsByCardId hydration mismatch');
|
|
1572
|
+
|
|
1573
|
+
for (const tsCard of tsTempCards) {
|
|
1574
|
+
const tsRemoveRes = await httpMcp('manage.remove-card', { card_id: tsCard.id });
|
|
1575
|
+
assert(tsRemoveRes.status === 200, `TS manage.remove-card(${tsCard.id}) returned ${tsRemoveRes.status}`);
|
|
1576
|
+
assert(tsRemoveRes.data?.status === 'success', `TS manage.remove-card(${tsCard.id}) failed: ${JSON.stringify(tsRemoveRes.data)}`);
|
|
1577
|
+
}
|
|
1578
|
+
await waitUntil(() => {
|
|
1579
|
+
const summary = NS.statusSummary;
|
|
1580
|
+
if (summary && summary.card_count === T0_EXPECTED_CARD_IDS.length) return summary;
|
|
1581
|
+
return false;
|
|
1582
|
+
}, 30_000, 'TS cleanup card_count back to 3');
|
|
1583
|
+
} finally {
|
|
1584
|
+
tsDeltaClient.close();
|
|
1585
|
+
}
|
|
1586
|
+
console.log('[TS] ok: one-shot framing, event ids, Last-Event-ID reconnect, ordered board deltas, and initial chat hydration verified');
|
|
982
1587
|
}
|
|
983
1588
|
|
|
984
1589
|
// ── T3d: probe-echo chat with one AI-generated attachment ──
|
|
@@ -1019,7 +1624,7 @@ try {
|
|
|
1019
1624
|
args: {
|
|
1020
1625
|
card_id: CHAT_CARD_ID,
|
|
1021
1626
|
payload: {
|
|
1022
|
-
text: `${ECHO_PROBE_MARKER}
|
|
1627
|
+
text: `${ECHO_PROBE_MARKER}echoattach__ ${t2dPrompt}${ECHO_PROBE_MARKER}`,
|
|
1023
1628
|
'turn-id': t3dTurnId,
|
|
1024
1629
|
},
|
|
1025
1630
|
},
|
|
@@ -1041,7 +1646,7 @@ try {
|
|
|
1041
1646
|
assert(t2dAfter.status === 200, `T3d post chats returned ${t2dAfter.status}`);
|
|
1042
1647
|
const t2dAfterMessages = Array.isArray(t2dAfter.data?.data?.messages) ? t2dAfter.data.data.messages : [];
|
|
1043
1648
|
const t2dNewMessages = t2dAfterMessages.slice(t2dBeforeCount);
|
|
1044
|
-
assert(t2dNewMessages.length >=
|
|
1649
|
+
assert(t2dNewMessages.length >= 3, `T3d expected at least 3 chat messages after send, got ${t2dNewMessages.length}`);
|
|
1045
1650
|
|
|
1046
1651
|
const t2dUser = t2dNewMessages.find((m) => m?.role === 'user');
|
|
1047
1652
|
const t2dInProgress = t2dNewMessages.find((m) => m?.role === 'system' && String(m?.text || '').trim().toLowerCase() === PROBE_IN_PROGRESS_TEXT);
|
|
@@ -1049,7 +1654,6 @@ try {
|
|
|
1049
1654
|
const t2dAssistantMsg = t2dNewMessages.find((m) => m?.role === 'assistant');
|
|
1050
1655
|
|
|
1051
1656
|
assert(!!t2dUser && typeof t2dUser.id === 'string', 'T3d missing user chat message');
|
|
1052
|
-
assert(!!t2dInProgress && typeof t2dInProgress.id === 'string', 'T3d missing in-progress system chat message');
|
|
1053
1657
|
assert(!!t2dAiGenerated && typeof t2dAiGenerated.id === 'string', 'T3d missing AI-generated attachment system chat message');
|
|
1054
1658
|
assert(/#\d+\s*$/.test(String(t2dAiGenerated?.text || '')), 'T3d AI-generated system message should include merged file index');
|
|
1055
1659
|
assert(String(t2dAiGenerated?.turn || '') === t3dTurnId, 'T3d AI-generated system turn id mismatch');
|