yaml-flow 8.7.1 → 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/firebase-storage.js +2 -2
- package/browser/adapters/firestore-storage.js +2 -2
- package/browser/adapters/localstorage-storage.js +3 -3
- package/browser/asset-integrity.json +9 -13
- package/browser/live-cards.js +6 -6
- package/browser/server-runtime-controlface.js +5 -5
- package/examples/ARCHITECTURE.md +0 -27
- package/examples/board/server/board-server.js +150 -100
- package/examples/board/server/board-worker/source_def_flows.json +0 -8
- package/examples/board/server/board-worker/task-executor.js +1 -3
- package/examples/board/server/chat-flow/flow-steps.json +5 -5
- package/examples/board/test/server-http-test.js +726 -87
- package/examples/board-firestore/server/worker.js +8 -0
- package/examples/portfolio-tracker/portfolio-tracker.js +11 -1
- package/examples/portfolio-tracker/test/portfolio-t4.js +12 -2
- package/lib/{artifacts-store-lib-D9nMkVcE.d.cts → artifacts-store-lib-C6qBpMfU.d.cts} +1 -1
- package/lib/{artifacts-store-lib-DSSMqVL2.d.ts → artifacts-store-lib-D4qf7Q-7.d.ts} +1 -1
- package/lib/artifacts-store-public.d.cts +3 -3
- package/lib/artifacts-store-public.d.ts +3 -3
- package/lib/board-live-cards-mcp.cjs +1 -1
- package/lib/board-live-cards-mcp.d.cts +6 -7
- package/lib/board-live-cards-mcp.d.ts +6 -7
- package/lib/board-live-cards-mcp.js +1 -1
- package/lib/board-live-cards-node.cjs +8 -8
- package/lib/board-live-cards-node.d.cts +70 -15
- package/lib/board-live-cards-node.d.ts +70 -15
- package/lib/board-live-cards-node.js +8 -8
- package/lib/{board-live-cards-public-LlVUQPL2.d.cts → board-live-cards-public-BT5HrgqZ.d.cts} +82 -59
- package/lib/{board-live-cards-public-JNRKfBZy.d.ts → board-live-cards-public-DSRamFm8.d.ts} +82 -59
- package/lib/{board-live-cards-public-async-Di9QB141.d.cts → board-live-cards-public-async-CYjr4mgX.d.cts} +18 -8
- package/lib/{board-live-cards-public-async-fwd1QI82.d.ts → board-live-cards-public-async-DlyC3PgC.d.ts} +18 -8
- package/lib/board-live-cards-public.cjs +1 -1
- package/lib/board-live-cards-public.d.cts +2 -2
- package/lib/board-live-cards-public.d.ts +2 -2
- 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 +6 -8
- package/lib/board-live-cards-server-runtime.d.ts +6 -8
- 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/{board-platform-adapter-async-BfHmHdx2.d.cts → board-platform-adapter-async-BZIftm36.d.cts} +18 -14
- package/lib/{board-platform-adapter-async-DYahVzIK.d.ts → board-platform-adapter-async-JP9V-U5E.d.ts} +18 -14
- package/lib/board-worker-adapter.cjs +1 -24
- package/lib/board-worker-adapter.d.cts +68 -3
- package/lib/board-worker-adapter.d.ts +68 -3
- package/lib/board-worker-adapter.js +1 -24
- package/lib/card-store-public.d.cts +2 -2
- package/lib/card-store-public.d.ts +2 -2
- package/lib/chat-store-public.cjs +1 -1
- package/lib/chat-store-public.d.cts +20 -20
- package/lib/chat-store-public.d.ts +20 -20
- package/lib/chat-store-public.js +1 -1
- package/lib/chunk-35N7ONTH.js +2 -0
- package/lib/chunk-36QUKFL7.cjs +3 -0
- package/lib/chunk-37HDEW26.cjs +2 -0
- package/lib/{chunk-PMUSJQSR.cjs → chunk-3CZCGNY4.cjs} +2 -2
- package/lib/{chunk-BQS3EIEK.js → chunk-44L64VQ2.js} +3 -3
- package/lib/{chunk-YGKDQLYP.js → chunk-4HIEOBJC.js} +2 -2
- package/lib/chunk-6OPXQPSC.js +2 -0
- package/lib/chunk-7BTZCOT5.js +2 -0
- package/lib/{chunk-U2N6MCD5.cjs → chunk-7JVHYHT2.cjs} +2 -2
- package/lib/chunk-7QQFDYBM.js +3 -0
- package/lib/chunk-7QZ267XP.cjs +2 -0
- package/lib/chunk-ABAVFLDP.js +7 -0
- package/lib/{chunk-XQRNDX4Q.js → chunk-ANKA7HEJ.js} +2 -2
- package/lib/{chunk-KAWQPLIE.cjs → chunk-BQUQTOPB.cjs} +2 -2
- package/lib/chunk-ETW3BXHD.cjs +2 -0
- package/lib/{chunk-SGV7PU4H.js → chunk-FOFGEABN.js} +2 -2
- package/lib/chunk-GPCMBPLK.cjs +2 -0
- package/lib/chunk-H22NK6KH.cjs +7 -0
- package/lib/chunk-H4TYOSMD.cjs +45 -0
- package/lib/chunk-HFW7E2Z7.cjs +4 -0
- package/lib/chunk-J4MHQ7JF.js +45 -0
- package/lib/chunk-MCPADH33.cjs +2 -0
- package/lib/chunk-NBJTYAYN.cjs +2 -0
- package/lib/chunk-NNSBBO5R.js +2 -0
- package/lib/chunk-NU5NO5NM.js +2 -0
- package/lib/chunk-O5UYCGIN.js +2 -0
- package/lib/chunk-O6II7S4M.js +3 -0
- package/lib/chunk-PN5D32NP.cjs +3 -0
- package/lib/chunk-Q3OTUDIE.js +2 -0
- package/lib/chunk-R44X3RQB.cjs +2 -0
- package/lib/chunk-RKKSVOP2.js +2 -0
- package/lib/chunk-UB54HZA4.cjs +2 -0
- package/lib/{chunk-CIAJNUR4.js → chunk-VGDLSS2H.js} +2 -2
- package/lib/{chunk-SFVO2LB2.cjs → chunk-VQCIOKJV.cjs} +3 -3
- package/lib/chunk-VS3BXEYK.js +4 -0
- package/lib/{chunk-S6DRP2HX.cjs → chunk-XQAHHUZO.cjs} +2 -2
- package/lib/chunk-Y4WK7HE4.js +2 -0
- package/lib/chunk-ZENTBLLA.cjs +3 -0
- package/lib/chunk-ZK3E7L4Y.cjs +2 -0
- package/lib/chunk-ZWVT24YW.js +3 -0
- package/lib/cloud-storage.cjs +1 -1
- package/lib/cloud-storage.d.cts +6 -6
- package/lib/cloud-storage.d.ts +6 -6
- package/lib/cloud-storage.js +1 -1
- package/lib/execution-refs.cjs +1 -1
- package/lib/execution-refs.js +1 -1
- package/lib/firebase-storage/index.cjs +2 -2
- package/lib/firebase-storage/index.d.cts +2 -2
- package/lib/firebase-storage/index.d.ts +2 -2
- package/lib/firebase-storage/index.js +2 -2
- package/lib/firestore-storage/index.cjs +2 -2
- package/lib/firestore-storage/index.d.cts +12 -21
- package/lib/firestore-storage/index.d.ts +12 -21
- package/lib/firestore-storage/index.js +2 -2
- package/lib/index.cjs +2 -2
- package/lib/index.d.cts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/localstorage-storage/index.cjs +1 -1
- package/lib/localstorage-storage/index.d.cts +10 -6
- package/lib/localstorage-storage/index.d.ts +10 -6
- package/lib/localstorage-storage/index.js +1 -1
- package/lib/{mcp-tool-registries-W3TRj6O5.d.cts → mcp-tool-registries-CRtea2x4.d.cts} +3 -0
- package/lib/{mcp-tool-registries-BBObLYga.d.ts → mcp-tool-registries-D3rWSppt.d.ts} +3 -0
- package/lib/server-jobs-queue-runner/index.cjs +1 -1
- package/lib/server-jobs-queue-runner/index.d.cts +12 -9
- package/lib/server-jobs-queue-runner/index.d.ts +12 -9
- package/lib/server-jobs-queue-runner/index.js +1 -1
- package/lib/server-runtime/index.cjs +1 -1
- package/lib/server-runtime/index.d.cts +7 -9
- package/lib/server-runtime/index.d.ts +7 -9
- package/lib/server-runtime/index.js +1 -1
- package/lib/server-runtime-agentface/index.d.cts +7 -9
- package/lib/server-runtime-agentface/index.d.ts +7 -9
- package/lib/server-runtime-controlface/index.cjs +1 -1
- package/lib/server-runtime-controlface/index.d.cts +8 -9
- package/lib/server-runtime-controlface/index.d.ts +8 -9
- package/lib/server-runtime-controlface/index.js +1 -1
- package/lib/server-runtime-core/index.cjs +1 -1
- package/lib/server-runtime-core/index.d.cts +59 -21
- package/lib/server-runtime-core/index.d.ts +59 -21
- package/lib/server-runtime-core/index.js +1 -1
- package/lib/server-runtime-watchers/index.cjs +1 -1
- package/lib/server-runtime-watchers/index.d.cts +9 -65
- package/lib/server-runtime-watchers/index.d.ts +9 -65
- package/lib/server-runtime-watchers/index.js +1 -1
- package/lib/server-runtime-webhooks/index.d.cts +7 -9
- package/lib/server-runtime-webhooks/index.d.ts +7 -9
- package/lib/sse-hub-BDjWI7JR.d.cts +63 -0
- package/lib/sse-hub-DM8bw-dO.d.ts +63 -0
- package/lib/step-machine-public/index.cjs +1 -1
- package/lib/step-machine-public/index.d.cts +1 -1
- package/lib/step-machine-public/index.d.ts +1 -1
- package/lib/step-machine-public/index.js +1 -1
- package/lib/{storage-async-interface-BRR4eBjx.d.cts → storage-async-interface-CG0bMqvE.d.ts} +20 -1
- package/lib/{storage-async-interface-DhlOVPSp.d.ts → storage-async-interface-CyO-zwVQ.d.cts} +20 -1
- package/lib/{storage-interface-BFiD3kyB.d.ts → storage-interface-D-iEiTJA.d.cts} +45 -1
- package/lib/{storage-interface-BFiD3kyB.d.cts → storage-interface-D-iEiTJA.d.ts} +45 -1
- package/lib/stores/index.d.cts +1 -1
- package/lib/stores/index.d.ts +1 -1
- package/lib/stores/kv.d.cts +1 -1
- package/lib/stores/kv.d.ts +1 -1
- package/lib/{types-SO5OZm4s.d.ts → types-BsfXZyI3.d.ts} +64 -29
- package/lib/{types-Ba8H5_Wo.d.cts → types-CPnYv7RC.d.cts} +64 -29
- package/package.json +4 -5
- package/browser/board-livecards-client.js +0 -2
- package/examples/board/demo-shell-with-server.html +0 -272
- package/examples/board/doc.html +0 -465
- package/examples/board/server-config.json +0 -24
- package/examples/board/test/sse-worker.js +0 -49
- package/lib/chat-storage-lib-B9Q34Dyv.d.cts +0 -54
- package/lib/chat-storage-lib-DB9iSai2.d.ts +0 -54
- package/lib/chunk-5XHOHTLZ.cjs +0 -2
- package/lib/chunk-6APH25VI.js +0 -2
- package/lib/chunk-76C7N4YT.js +0 -3
- package/lib/chunk-76ON3V7R.js +0 -2
- package/lib/chunk-7ICPAABP.cjs +0 -7
- package/lib/chunk-ASR44K7H.cjs +0 -3
- package/lib/chunk-CPAXTVBQ.cjs +0 -2
- package/lib/chunk-EGRHWZRV.js +0 -2
- package/lib/chunk-EZENHAVZ.cjs +0 -2
- package/lib/chunk-GL2OHR2E.cjs +0 -2
- package/lib/chunk-GYQXDNNI.cjs +0 -2
- package/lib/chunk-HEEDJEKM.js +0 -2
- package/lib/chunk-IPLSRN6P.cjs +0 -4
- package/lib/chunk-J6EGN6S4.cjs +0 -3
- package/lib/chunk-JH37NJGP.js +0 -3
- package/lib/chunk-JJL5VOQZ.cjs +0 -3
- package/lib/chunk-NJJ7WEDT.cjs +0 -2
- package/lib/chunk-NKIQRCOM.cjs +0 -2
- package/lib/chunk-PBCDDO4V.cjs +0 -2
- package/lib/chunk-PBOQ4HYB.cjs +0 -2
- package/lib/chunk-PRKRXAVN.js +0 -3
- package/lib/chunk-QJVR3FWQ.js +0 -2
- package/lib/chunk-S44QZUDX.js +0 -2
- package/lib/chunk-TSN3RTXT.js +0 -4
- package/lib/chunk-VXJHBWK3.js +0 -2
- package/lib/chunk-WHDEBJLT.js +0 -7
- package/lib/chunk-YGALANRO.js +0 -2
- package/lib/chunk-ZCNN6XPV.js +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:
|
|
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:
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { spawn, spawnSync } from 'node:child_process';
|
|
17
|
-
import { Worker } from 'node:worker_threads';
|
|
18
17
|
import { fileURLToPath } from 'node:url';
|
|
19
18
|
import path from 'node:path';
|
|
20
19
|
import http from 'node:http';
|
|
@@ -45,14 +44,24 @@ function isCopilotAvailable() {
|
|
|
45
44
|
|
|
46
45
|
const skipT3a = cliArgs.includes('--skip-t3a') || !isCopilotAvailable();
|
|
47
46
|
const skipT3b = cliArgs.includes('--skip-t3b');
|
|
47
|
+
const skipT3e = cliArgs.includes('--skip-t3e');
|
|
48
48
|
const skipT3d = cliArgs.includes('--skip-t3d');
|
|
49
49
|
const RUN_ID = `run-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
50
50
|
|
|
51
51
|
const BOARD_ID = 'live';
|
|
52
52
|
const BOARD_DIR = path.resolve(__dirname, '..');
|
|
53
53
|
const SERVER_SCRIPT = path.resolve(BOARD_DIR, 'server', 'board-server.js');
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// Force the board server to start with zero cards; the test upserts the
|
|
55
|
+
// three cardT-* fixtures itself in T0 so the SSE upsert/delta path is
|
|
56
|
+
// exercised end-to-end.
|
|
57
|
+
const CARD_PATTERN = '__none__*';
|
|
58
|
+
const CARDS_DIR = path.resolve(BOARD_DIR, 'cards');
|
|
59
|
+
const T0_CARD_FILES = [
|
|
60
|
+
'cardT-portfolio.json',
|
|
61
|
+
'cardT-market-prices.json',
|
|
62
|
+
'cardT-portfolio-value.json',
|
|
63
|
+
];
|
|
64
|
+
const T0_EXPECTED_CARD_IDS = ['card-market-prices', 'card-portfolio', 'card-portfolio-value'];
|
|
56
65
|
const T2_FILE_CARD_ID = 'card-market-prices';
|
|
57
66
|
const CHAT_CARD_ID = 'card-portfolio';
|
|
58
67
|
|
|
@@ -92,19 +101,16 @@ if (fs.existsSync(SETUP_DIR)) {
|
|
|
92
101
|
// ---------------------------------------------------------------------------
|
|
93
102
|
|
|
94
103
|
const NS = {
|
|
95
|
-
initialPayload: null,
|
|
96
104
|
statusSummary: null,
|
|
97
105
|
statusGeneration: 0,
|
|
98
106
|
computedValues: {},
|
|
99
107
|
chatEvents: [],
|
|
108
|
+
allChatNotifications: [],
|
|
100
109
|
boardEvents: [],
|
|
101
110
|
};
|
|
102
111
|
|
|
103
112
|
function applyFrame(payload) {
|
|
104
113
|
if (payload && Array.isArray(payload.cardDefinitions)) {
|
|
105
|
-
if (!NS.initialPayload && payload.cardDefinitions.length > 0) {
|
|
106
|
-
NS.initialPayload = payload;
|
|
107
|
-
}
|
|
108
114
|
const summary = payload.statusSnapshot && payload.statusSnapshot.summary;
|
|
109
115
|
if (summary) {
|
|
110
116
|
NS.statusSummary = summary;
|
|
@@ -164,6 +170,39 @@ function parseSseBlocks(buffer) {
|
|
|
164
170
|
return { payloads, remainder: buf };
|
|
165
171
|
}
|
|
166
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
|
+
|
|
167
206
|
function startSseClient(sseUrl, onPayload) {
|
|
168
207
|
const req = http.get(sseUrl, (res) => {
|
|
169
208
|
let buf = '';
|
|
@@ -183,9 +222,127 @@ function startSseClient(sseUrl, onPayload) {
|
|
|
183
222
|
};
|
|
184
223
|
}
|
|
185
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
|
+
|
|
186
332
|
function captureChatEvents(payload, cardId) {
|
|
187
333
|
if (!payload || payload.kind !== 'notification-batch' || !Array.isArray(payload.notifications)) return;
|
|
188
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
|
+
}
|
|
189
346
|
if (n && n.kind === 'card_chats' && n.cardId === cardId) {
|
|
190
347
|
const messages = Array.isArray(n.messages) ? n.messages : [];
|
|
191
348
|
NS.chatEvents.push({
|
|
@@ -230,10 +387,159 @@ function waitUntil(predicate, timeoutMs, label) {
|
|
|
230
387
|
});
|
|
231
388
|
}
|
|
232
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
|
+
|
|
233
409
|
function wait(ms) {
|
|
234
410
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
235
411
|
}
|
|
236
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
|
+
|
|
237
543
|
async function stopChildProcess(proc, label) {
|
|
238
544
|
if (!proc) return;
|
|
239
545
|
if (proc.exitCode !== null) return;
|
|
@@ -267,9 +573,6 @@ async function stopChildProcess(proc, label) {
|
|
|
267
573
|
}
|
|
268
574
|
}
|
|
269
575
|
|
|
270
|
-
const waitForInitialPayload = (ms = 15_000) =>
|
|
271
|
-
waitUntil(() => NS.initialPayload || false, ms, 'initial SSE payload');
|
|
272
|
-
|
|
273
576
|
const waitForAllCompleted = (ms = 60_000, label = 'all completed') =>
|
|
274
577
|
waitUntil(() => {
|
|
275
578
|
const s = NS.statusSummary;
|
|
@@ -315,13 +618,21 @@ function deriveProbeLifecycleMilestones(events, opts) {
|
|
|
315
618
|
|
|
316
619
|
function matchOrderedProbeLifecycle(events, opts) {
|
|
317
620
|
const milestones = deriveProbeLifecycleMilestones(events, opts);
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
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 };
|
|
325
636
|
}
|
|
326
637
|
|
|
327
638
|
function httpGet(url) {
|
|
@@ -502,12 +813,12 @@ console.log(`target: ${BASE}`);
|
|
|
502
813
|
console.log(`card pattern: ${CARD_PATTERN}`);
|
|
503
814
|
|
|
504
815
|
const serverProc = await startServer(PORT);
|
|
505
|
-
let
|
|
816
|
+
let boardSseClient = null;
|
|
506
817
|
let chatSseClient = null;
|
|
507
818
|
let chatSseClientId = '';
|
|
508
819
|
|
|
509
820
|
try {
|
|
510
|
-
// ── T0:
|
|
821
|
+
// ── T0: streaming SSE connect, upsert fixtures, wait for initial completion ──
|
|
511
822
|
|
|
512
823
|
// Register the 'live' board via POST (v8 runtime requires explicit registration)
|
|
513
824
|
const regRes = await httpJson('POST', `http://127.0.0.1:${PORT}/api/boards`, { id: BOARD_ID, label: 'Live' });
|
|
@@ -515,31 +826,34 @@ try {
|
|
|
515
826
|
`POST /api/boards returned ${regRes.status}: ${JSON.stringify(regRes.data)}`);
|
|
516
827
|
console.log(`[setup] board '${BOARD_ID}' registered (${regRes.status})`);
|
|
517
828
|
|
|
518
|
-
console.log('\n=== T0 Step 1:
|
|
519
|
-
const initRes = await httpGet(`${BASE}/init-board`);
|
|
520
|
-
assert(initRes.status === 200, `init-board returned ${initRes.status}`);
|
|
521
|
-
console.log('[T0.1] init-board ok');
|
|
522
|
-
|
|
523
|
-
console.log('\n=== T0 Step 2: start SSE worker ===');
|
|
829
|
+
console.log('\n=== T0 Step 1: start SSE client (board expected empty) ===');
|
|
524
830
|
const sseClientId = `server-http-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
525
831
|
const sseUrl = `${BASE}/sse?clientId=${encodeURIComponent(sseClientId)}`;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
832
|
+
boardSseClient = startSseClient(sseUrl, applyFrame);
|
|
833
|
+
// Give the streaming endpoint a moment to deliver the initial (empty) snapshot.
|
|
834
|
+
await wait(500);
|
|
835
|
+
console.log('[T0.1] SSE client connected');
|
|
836
|
+
|
|
837
|
+
console.log('\n=== T0 Step 2: upsert 3 cardT-* fixtures ===');
|
|
838
|
+
for (const fileName of T0_CARD_FILES) {
|
|
839
|
+
const cardJson = JSON.parse(fs.readFileSync(path.join(CARDS_DIR, fileName), 'utf-8'));
|
|
840
|
+
const cardId = cardJson?.id;
|
|
841
|
+
assert(typeof cardId === 'string' && cardId.length > 0, `fixture ${fileName} missing id`);
|
|
842
|
+
const upsertRes = await httpMcp('manage.upsert-card', {
|
|
843
|
+
card_id: cardId,
|
|
844
|
+
candidate_card_content: cardJson,
|
|
845
|
+
});
|
|
846
|
+
assert(upsertRes.status === 200, `manage.upsert-card(${cardId}) returned ${upsertRes.status}: ${JSON.stringify(upsertRes.data)}`);
|
|
847
|
+
assert(upsertRes.data?.status === 'success', `manage.upsert-card(${cardId}) failed: ${JSON.stringify(upsertRes.data)}`);
|
|
848
|
+
console.log(`[T0.2] upserted ${cardId}`);
|
|
849
|
+
}
|
|
540
850
|
|
|
541
|
-
console.log('\n=== T0 Step 3: wait for all cards to complete ===');
|
|
542
|
-
const t0Summary = await
|
|
851
|
+
console.log('\n=== T0 Step 3: wait for all 3 cards to complete via SSE ===');
|
|
852
|
+
const t0Summary = await waitUntil(() => {
|
|
853
|
+
const s = NS.statusSummary;
|
|
854
|
+
if (s && s.card_count === 3 && s.completed === 3) return s;
|
|
855
|
+
return false;
|
|
856
|
+
}, 60_000, 'T0 initial completion (3 cards)');
|
|
543
857
|
assert(t0Summary.failed === 0, `T0 expected failed=0, got ${t0Summary.failed}`);
|
|
544
858
|
console.log(`[T0.3] completed: ${JSON.stringify(t0Summary)}`);
|
|
545
859
|
|
|
@@ -549,6 +863,8 @@ try {
|
|
|
549
863
|
assert(statusMcpRes.data?.status === 'success', `inspect.board-runtime-status failed: ${JSON.stringify(statusMcpRes.data)}`);
|
|
550
864
|
const mcpSummary = statusMcpRes.data?.data?.summary;
|
|
551
865
|
assert(mcpSummary, 'summary missing from inspect.board-runtime-status');
|
|
866
|
+
assert(mcpSummary.card_count === T0_EXPECTED_CARD_IDS.length,
|
|
867
|
+
`expected card_count=${T0_EXPECTED_CARD_IDS.length}, got ${mcpSummary.card_count}`);
|
|
552
868
|
assert(mcpSummary.completed === mcpSummary.card_count, `not all complete: ${JSON.stringify(mcpSummary)}`);
|
|
553
869
|
console.log(`[T0.4] board-status: ${JSON.stringify(mcpSummary)}`);
|
|
554
870
|
|
|
@@ -740,25 +1056,34 @@ try {
|
|
|
740
1056
|
|
|
741
1057
|
const t3TurnId = randomTurnId();
|
|
742
1058
|
t3Dbg(`step 4: posting probe chat-send (turn-id=${t3TurnId})`);
|
|
743
|
-
const t2SendRes = await httpJson('POST', `${BASE}/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1059
|
+
const t2SendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1060
|
+
tool: 'chat-send',
|
|
1061
|
+
args: {
|
|
1062
|
+
card_id: CHAT_CARD_ID,
|
|
1063
|
+
payload: {
|
|
1064
|
+
text: `${ECHO_PROBE_MARKER}${t2ProbePrompt}${ECHO_PROBE_MARKER}`,
|
|
1065
|
+
'turn-id': t3TurnId,
|
|
1066
|
+
},
|
|
748
1067
|
},
|
|
749
1068
|
});
|
|
750
1069
|
t3Dbg(`step 4: chat-send returned status=${t2SendRes.status}`);
|
|
751
1070
|
assert(t2SendRes.status === 200, `T3 chat-send returned ${t2SendRes.status}`);
|
|
752
1071
|
|
|
753
1072
|
t3Dbg('step 5: waiting for ordered probe lifecycle on chat SSE');
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1073
|
+
let t2Lifecycle;
|
|
1074
|
+
try {
|
|
1075
|
+
t2Lifecycle = await waitForChatPredicate((events) => {
|
|
1076
|
+
return matchOrderedProbeLifecycle(events.slice(t2EventStart), {
|
|
1077
|
+
beforeCount: t2BeforeCount,
|
|
1078
|
+
beforeProcessing: false,
|
|
1079
|
+
prompt: t2ProbePrompt,
|
|
1080
|
+
inProgressText: PROBE_IN_PROGRESS_TEXT,
|
|
1081
|
+
});
|
|
1082
|
+
}, 45_000, 'T3 ordered lifecycle');
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
t3Dbg(`step 5: lifecycle timeout; events=${JSON.stringify(NS.chatEvents.slice(t2EventStart), null, 2)}`);
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
762
1087
|
t3Dbg('step 5: ordered lifecycle observed');
|
|
763
1088
|
assert(!!t2Lifecycle, 'T3 ordered lifecycle not observed');
|
|
764
1089
|
|
|
@@ -769,12 +1094,12 @@ try {
|
|
|
769
1094
|
const t2AfterMessages = Array.isArray(t2After.data?.data?.messages) ? t2After.data.data.messages : [];
|
|
770
1095
|
const t2NewMessages = t2AfterMessages.slice(t2BeforeCount);
|
|
771
1096
|
t3Dbg(`step 6: validating ${t2NewMessages.length} new messages`);
|
|
772
|
-
assert(t2NewMessages.length >=
|
|
1097
|
+
assert(t2NewMessages.length >= 2, `T3 expected at least 2 new chat messages, got ${t2NewMessages.length}`);
|
|
773
1098
|
const t3McpAfter = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, turn_id: t3TurnId });
|
|
774
1099
|
const t3McpAfterData = expectMcpSuccess(t3McpAfter, 'T3 MCP post chats');
|
|
775
1100
|
const t3TurnMessages = Array.isArray(t3McpAfterData?.messages) ? t3McpAfterData.messages : [];
|
|
776
1101
|
t3Dbg(`step 6: MCP turn messages count=${t3TurnMessages.length}`);
|
|
777
|
-
assert(t3TurnMessages.length >=
|
|
1102
|
+
assert(t3TurnMessages.length >= 2, `T3 expected at least 2 MCP messages for turn ${t3TurnId}, got ${t3TurnMessages.length}`);
|
|
778
1103
|
for (const msg of t3TurnMessages) {
|
|
779
1104
|
assert(String(msg?.turn || '') === t3TurnId, 'T3 MCP turn id mismatch');
|
|
780
1105
|
}
|
|
@@ -792,7 +1117,6 @@ try {
|
|
|
792
1117
|
const t2AssistantMsg = t2NewMessages.find((m) => m?.role === 'assistant');
|
|
793
1118
|
assert(!!t2User && typeof t2User.id === 'string', 'T3 user chat message missing id');
|
|
794
1119
|
assert(String(t2User?.text || '').includes(t2ProbePrompt), 'T3 user file text mismatch');
|
|
795
|
-
assert(!!t2InProgress && typeof t2InProgress.id === 'string', 'T3 in-progress system message missing id');
|
|
796
1120
|
assert(!!t2AssistantMsg && typeof t2AssistantMsg.id === 'string', 'T3 assistant chat message missing id');
|
|
797
1121
|
assert(String(t2AssistantMsg?.text || '').includes(`Echo: ${t2ProbePrompt}`), 'T3 assistant echo file content mismatch');
|
|
798
1122
|
t3Dbg('step 6: all assertions passed');
|
|
@@ -818,14 +1142,17 @@ try {
|
|
|
818
1142
|
|
|
819
1143
|
const t3aTurnId = randomTurnId();
|
|
820
1144
|
t3aDbg(`step 2: posting non-probe chat-send (turn-id=${t3aTurnId})`);
|
|
821
|
-
const t2aSendRes = await httpJson('POST', `${BASE}/
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1145
|
+
const t2aSendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1146
|
+
tool: 'chat-send',
|
|
1147
|
+
args: {
|
|
1148
|
+
card_id: CHAT_CARD_ID,
|
|
1149
|
+
payload: {
|
|
1150
|
+
text: JSON.stringify({
|
|
1151
|
+
prompt: t2aPrompt,
|
|
1152
|
+
chatTimeoutMs: 180000,
|
|
1153
|
+
}),
|
|
1154
|
+
'turn-id': t3aTurnId,
|
|
1155
|
+
},
|
|
829
1156
|
},
|
|
830
1157
|
});
|
|
831
1158
|
t3aDbg(`step 2: chat-send returned status=${t2aSendRes.status}`);
|
|
@@ -910,12 +1237,14 @@ try {
|
|
|
910
1237
|
const t2bEventStart = NS.chatEvents.length;
|
|
911
1238
|
|
|
912
1239
|
const t2bPrompt = `probe echo file-upload validation ${Date.now()}`;
|
|
913
|
-
const t2bSendRes = await httpJson('POST', `${BASE}/
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1240
|
+
const t2bSendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1241
|
+
tool: 'chat-send',
|
|
1242
|
+
args: {
|
|
1243
|
+
card_id: CHAT_CARD_ID,
|
|
1244
|
+
payload: {
|
|
1245
|
+
text: `${ECHO_PROBE_MARKER}${t2bPrompt}${ECHO_PROBE_MARKER}`,
|
|
1246
|
+
'turn-id': t3bTurnId,
|
|
1247
|
+
},
|
|
919
1248
|
},
|
|
920
1249
|
});
|
|
921
1250
|
assert(t2bSendRes.status === 200, `T3b chat-send returned ${t2bSendRes.status}`);
|
|
@@ -925,28 +1254,336 @@ try {
|
|
|
925
1254
|
beforeCount: t2bSendBaseline,
|
|
926
1255
|
beforeProcessing: false,
|
|
927
1256
|
prompt: t2bPrompt,
|
|
928
|
-
assistantText: 'tokyo',
|
|
929
1257
|
inProgressText: PROBE_IN_PROGRESS_TEXT,
|
|
930
1258
|
});
|
|
931
|
-
}, 60_000, 'T3b ordered lifecycle')
|
|
1259
|
+
}, 60_000, 'T3b ordered lifecycle').catch(async (err) => {
|
|
1260
|
+
const t2bEvents = NS.chatEvents.slice(t2bEventStart);
|
|
1261
|
+
const t2bMilestones = deriveProbeLifecycleMilestones(t2bEvents, {
|
|
1262
|
+
beforeCount: t2bSendBaseline,
|
|
1263
|
+
beforeProcessing: false,
|
|
1264
|
+
prompt: t2bPrompt,
|
|
1265
|
+
inProgressText: PROBE_IN_PROGRESS_TEXT,
|
|
1266
|
+
});
|
|
1267
|
+
const t2bCurrent = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, all_turns: true });
|
|
1268
|
+
console.error('[T3b.DBG timeout] milestones=', JSON.stringify(t2bMilestones));
|
|
1269
|
+
console.error('[T3b.DBG timeout] events=', JSON.stringify(t2bEvents));
|
|
1270
|
+
console.error('[T3b.DBG timeout] inspect=', JSON.stringify(t2bCurrent?.data ?? null));
|
|
1271
|
+
throw err;
|
|
1272
|
+
});
|
|
932
1273
|
assert(!!t2bLifecycle, 'T3b ordered lifecycle not observed');
|
|
933
1274
|
|
|
934
1275
|
const t2bAfter = await httpMcp('inspect.chat-messages-on-cards', { card_id: CHAT_CARD_ID, all_turns: true });
|
|
935
1276
|
assert(t2bAfter.status === 200, `T3b post chats returned ${t2bAfter.status}`);
|
|
936
1277
|
const t2bAfterMessages = Array.isArray(t2bAfter.data?.data?.messages) ? t2bAfter.data.data.messages : [];
|
|
937
1278
|
const t2bNewMessages = t2bAfterMessages.slice(t2bSendBaseline);
|
|
938
|
-
assert(t2bNewMessages.length >=
|
|
1279
|
+
assert(t2bNewMessages.length >= 2, `T3b expected at least 2 chat messages after send, got ${t2bNewMessages.length}`);
|
|
939
1280
|
|
|
940
1281
|
const t2bUser = t2bNewMessages.find((m) => m?.role === 'user');
|
|
941
1282
|
const t2bInProgress = t2bNewMessages.find((m) => m?.role === 'system' && String(m?.text || '').trim().toLowerCase() === PROBE_IN_PROGRESS_TEXT);
|
|
942
1283
|
const t2bAssistantMsg = t2bNewMessages.find((m) => m?.role === 'assistant');
|
|
943
1284
|
|
|
944
1285
|
assert(!!t2bUser && typeof t2bUser.id === 'string', 'T3b missing user chat message notification');
|
|
945
|
-
assert(!!t2bInProgress && typeof t2bInProgress.id === 'string', 'T3b missing in-progress system chat message');
|
|
946
1286
|
assert(!!t2bAssistantMsg && typeof t2bAssistantMsg.id === 'string', 'T3b missing assistant chat message notification');
|
|
947
|
-
assert(Array.isArray(t2bUser?.files)
|
|
948
|
-
assert(String(t2bAssistantMsg?.text || '').
|
|
949
|
-
console.log('[T3b] ok: upload
|
|
1287
|
+
assert(!Array.isArray(t2bUser?.files) || t2bUser.files.length === 0, 'T3b user chat message should remain text-only after add-chat-attachment upload');
|
|
1288
|
+
assert(String(t2bAssistantMsg?.text || '').includes(`Echo: ${t2bPrompt}`), 'T3b assistant probe echo mismatch');
|
|
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');
|
|
950
1587
|
}
|
|
951
1588
|
|
|
952
1589
|
// ── T3d: probe-echo chat with one AI-generated attachment ──
|
|
@@ -982,11 +1619,14 @@ try {
|
|
|
982
1619
|
const t3dTurnId = randomTurnId();
|
|
983
1620
|
const t2dPrompt = `probe generated attachment validation ${Date.now()}`;
|
|
984
1621
|
const t2dEventStart = NS.chatEvents.length;
|
|
985
|
-
const t2dSendRes = await httpJson('POST', `${BASE}/
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1622
|
+
const t2dSendRes = await httpJson('POST', `${BASE}/mcp-actions`, {
|
|
1623
|
+
tool: 'chat-send',
|
|
1624
|
+
args: {
|
|
1625
|
+
card_id: CHAT_CARD_ID,
|
|
1626
|
+
payload: {
|
|
1627
|
+
text: `${ECHO_PROBE_MARKER}echoattach__ ${t2dPrompt}${ECHO_PROBE_MARKER}`,
|
|
1628
|
+
'turn-id': t3dTurnId,
|
|
1629
|
+
},
|
|
990
1630
|
},
|
|
991
1631
|
});
|
|
992
1632
|
assert(t2dSendRes.status === 200, `T3d chat-send returned ${t2dSendRes.status}`);
|
|
@@ -1006,7 +1646,7 @@ try {
|
|
|
1006
1646
|
assert(t2dAfter.status === 200, `T3d post chats returned ${t2dAfter.status}`);
|
|
1007
1647
|
const t2dAfterMessages = Array.isArray(t2dAfter.data?.data?.messages) ? t2dAfter.data.data.messages : [];
|
|
1008
1648
|
const t2dNewMessages = t2dAfterMessages.slice(t2dBeforeCount);
|
|
1009
|
-
assert(t2dNewMessages.length >=
|
|
1649
|
+
assert(t2dNewMessages.length >= 3, `T3d expected at least 3 chat messages after send, got ${t2dNewMessages.length}`);
|
|
1010
1650
|
|
|
1011
1651
|
const t2dUser = t2dNewMessages.find((m) => m?.role === 'user');
|
|
1012
1652
|
const t2dInProgress = t2dNewMessages.find((m) => m?.role === 'system' && String(m?.text || '').trim().toLowerCase() === PROBE_IN_PROGRESS_TEXT);
|
|
@@ -1014,7 +1654,6 @@ try {
|
|
|
1014
1654
|
const t2dAssistantMsg = t2dNewMessages.find((m) => m?.role === 'assistant');
|
|
1015
1655
|
|
|
1016
1656
|
assert(!!t2dUser && typeof t2dUser.id === 'string', 'T3d missing user chat message');
|
|
1017
|
-
assert(!!t2dInProgress && typeof t2dInProgress.id === 'string', 'T3d missing in-progress system chat message');
|
|
1018
1657
|
assert(!!t2dAiGenerated && typeof t2dAiGenerated.id === 'string', 'T3d missing AI-generated attachment system chat message');
|
|
1019
1658
|
assert(/#\d+\s*$/.test(String(t2dAiGenerated?.text || '')), 'T3d AI-generated system message should include merged file index');
|
|
1020
1659
|
assert(String(t2dAiGenerated?.turn || '') === t3dTurnId, 'T3d AI-generated system turn id mismatch');
|
|
@@ -1643,8 +2282,8 @@ try {
|
|
|
1643
2282
|
} catch { /* ignore */ }
|
|
1644
2283
|
}
|
|
1645
2284
|
if (chatSseClient) chatSseClient.close();
|
|
2285
|
+
if (boardSseClient) boardSseClient.close();
|
|
1646
2286
|
await stopChildProcess(serverProc, 'demo board server');
|
|
1647
|
-
if (sseWorker) await sseWorker.terminate();
|
|
1648
2287
|
|
|
1649
2288
|
// Clean up the test setup directory
|
|
1650
2289
|
if (fs.existsSync(SETUP_DIR)) {
|