yaml-flow 8.1.1 → 8.2.1

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.
Files changed (108) hide show
  1. package/browser/asset-integrity.json +3 -3
  2. package/browser/board-livecards-client.js +1 -1
  3. package/browser/board-livecards-localstorage.js +4 -6
  4. package/cli/{board-live-cards-lib-tjYsPt5U.d.ts → board-live-cards-lib-Iq_XAC09.d.ts} +1 -1
  5. package/cli/browser-api/board-live-cards-browser-adapter.d.ts +4 -3
  6. package/cli/browser-api/board-live-cards-browser-adapter.js +2 -2
  7. package/cli/browser-api/card-store-browser-api.d.ts +1 -1
  8. package/cli/node/artifacts-store-cli.js +8 -8
  9. package/cli/node/board-live-cards-cli.js +8 -8
  10. package/cli/node/card-store-cli.js +4 -4
  11. package/cli/node/fs-board-adapter.d.ts +6 -33
  12. package/cli/node/fs-board-adapter.js +10 -8
  13. package/cli/node/step-machine-cli.js +3 -3
  14. package/cli/{types-D2XnLbBj.d.ts → types--rXGWbSR.d.ts} +77 -5
  15. package/examples/board/.board-ws/cards/store/_index.json +17 -0
  16. package/examples/board/.board-ws/cards/store/card-market-prices.json +80 -0
  17. package/examples/board/.board-ws/cards/store/card-portfolio-value.json +90 -0
  18. package/examples/board/.board-ws/cards/store/card-portfolio.json +78 -0
  19. package/examples/board/cards/cardT-market-prices.json +6 -4
  20. package/examples/board/cards/cardT-portfolio-value.json +10 -38
  21. package/examples/board/cards/cardT-portfolio.json +9 -4
  22. package/examples/board/demo-shell-with-server.html +3 -3
  23. package/examples/board/server/board-server.js +593 -0
  24. package/examples/board/server/board-worker/source-def-flows/mock-handler/mock-db.js +13 -0
  25. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/.retain/compliance.db +0 -0
  26. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/.retain/optimus.db +0 -0
  27. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/query.cjs +51 -0
  28. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/seed-cpm.cjs +197 -0
  29. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/seed-cpmV2.cjs +128 -0
  30. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/seed-optimus.cjs +352 -0
  31. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/sqlite-config.json +3 -0
  32. package/examples/board/server/board-worker/source-def-flows/sqlite-handler/sqlite-handler.js +84 -0
  33. package/examples/board/{source-def-flows/url.flow.json → server/board-worker/source-def-flows/sqlite.flow.json} +7 -7
  34. package/examples/board/{source-def-handlers → server/board-worker/source-def-flows/url-handler}/http-source-handler.js +29 -21
  35. package/examples/board/server/board-worker/source-def-flows/url.flow.json +73 -0
  36. package/examples/board/{source_def_flows.json → server/board-worker/source_def_flows.json} +61 -115
  37. package/examples/board/server/board-worker/task-executor.js +475 -0
  38. package/examples/board/server/chat-flow/chat-clear-processing.js +41 -0
  39. package/examples/board/server/chat-flow/chat-open-turn.js +144 -0
  40. package/examples/board/server/chat-flow/chat-write-assistant.js +44 -0
  41. package/examples/board/server/chat-flow/copilot-chat/assistant.js +253 -0
  42. package/examples/board/server/chat-flow/echo-probe/assistant.js +28 -0
  43. package/examples/board/server/chat-flow/flow-steps.json +167 -0
  44. package/examples/board/server-config.json +22 -0
  45. package/examples/board/test/server-http-test.js +707 -0
  46. package/examples/board/test/{portfolio-tracker-sse-worker.js → sse-worker.js} +9 -8
  47. package/examples/board-local/demo-shell-localstorage.html +3 -3
  48. package/lib/{artifacts-store-lib-public-DBICnGL6.d.cts → artifacts-store-lib-public-C5UL5tyG.d.cts} +3 -31
  49. package/lib/{artifacts-store-lib-public-BWC3YuLa.d.ts → artifacts-store-lib-public-GD4H-fFp.d.ts} +3 -31
  50. package/lib/artifacts-store-public.d.cts +3 -3
  51. package/lib/artifacts-store-public.d.ts +3 -3
  52. package/lib/board-live-cards-node.cjs +10 -8
  53. package/lib/board-live-cards-node.d.cts +9 -8
  54. package/lib/board-live-cards-node.d.ts +9 -8
  55. package/lib/board-live-cards-node.js +10 -8
  56. package/lib/{board-live-cards-public-BF9FP0mL.d.cts → board-live-cards-public-BLXbcBNk.d.cts} +2 -2
  57. package/lib/{board-live-cards-public-dJAl5IL-.d.ts → board-live-cards-public-BZaNb2mi.d.ts} +2 -2
  58. package/lib/board-live-cards-public.cjs +2 -2
  59. package/lib/board-live-cards-public.d.cts +2 -2
  60. package/lib/board-live-cards-public.d.ts +2 -2
  61. package/lib/board-live-cards-public.js +2 -2
  62. package/lib/board-live-cards-server-runtime.cjs +4 -6
  63. package/lib/board-live-cards-server-runtime.d.cts +3 -3
  64. package/lib/board-live-cards-server-runtime.d.ts +3 -3
  65. package/lib/board-live-cards-server-runtime.js +4 -6
  66. package/lib/board-livegraph-runtime/index.cjs +2 -2
  67. package/lib/board-livegraph-runtime/index.js +2 -2
  68. package/lib/card-store-public.d.cts +2 -2
  69. package/lib/card-store-public.d.ts +2 -2
  70. package/lib/execution-refs.cjs +1 -1
  71. package/lib/execution-refs.js +1 -1
  72. package/lib/index.cjs +1 -1
  73. package/lib/index.d.cts +1 -1
  74. package/lib/index.d.ts +1 -1
  75. package/lib/index.js +1 -1
  76. package/lib/server-runtime/index.cjs +4 -6
  77. package/lib/server-runtime/index.d.cts +4 -4
  78. package/lib/server-runtime/index.d.ts +4 -4
  79. package/lib/server-runtime/index.js +4 -6
  80. package/lib/step-machine-public/index.cjs +3 -3
  81. package/lib/step-machine-public/index.d.cts +27 -10
  82. package/lib/step-machine-public/index.d.ts +27 -10
  83. package/lib/step-machine-public/index.js +3 -3
  84. package/lib/{storage-interface-BhAON-gW.d.ts → storage-interface-B6ecOulj.d.cts} +25 -3
  85. package/lib/{storage-interface-BhAON-gW.d.cts → storage-interface-B6ecOulj.d.ts} +25 -3
  86. package/lib/stores/index.d.cts +1 -1
  87. package/lib/stores/index.d.ts +1 -1
  88. package/lib/stores/kv.d.cts +1 -1
  89. package/lib/stores/kv.d.ts +1 -1
  90. package/lib/{types-CXBzvC0s.d.cts → types-Bztd1KoK.d.cts} +75 -3
  91. package/lib/{types-D48hpnTR.d.ts → types-D-xVWPdY.d.ts} +75 -3
  92. package/package.json +1 -1
  93. package/examples/board/demo-chat-handler.js +0 -169
  94. package/examples/board/demo-server-config.json +0 -10
  95. package/examples/board/demo-server.js +0 -580
  96. package/examples/board/demo-task-executor.js +0 -721
  97. package/examples/board/gandalf-cards/card-source-kinds.json +0 -36
  98. package/examples/board/gandalf-cards/cards/_index.json +0 -7
  99. package/examples/board/gandalf-cards/cards/card-source-kinds.json +0 -64
  100. package/examples/board/scripts/copilot_wrapper.bat +0 -157
  101. package/examples/board/scripts/copilot_wrapper_helper.ps1 +0 -190
  102. package/examples/board/scripts/workiq_wrapper.mjs +0 -66
  103. package/examples/board/source-def-flows/copilot.flow.json +0 -33
  104. package/examples/board/source-def-flows/url-list.flow.json +0 -33
  105. package/examples/board/source-def-flows/workiq.flow.json +0 -34
  106. package/examples/board/source-def-handlers/copilot-source-handler.js +0 -141
  107. package/examples/board/test/demo-http-test.js +0 -317
  108. /package/examples/board/{source-def-flows → server/board-worker/source-def-flows}/mock.flow.json +0 -0
@@ -0,0 +1,707 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * demo-http-test.js
4
+ *
5
+ * Smoke test for demo-board/server/board-server.js over HTTP + SSE.
6
+ * Targets the 'live' board with --cards-pattern cardT* to load only the 3
7
+ * test cards (cardT-portfolio, cardT-market-prices, cardT-portfolio-value).
8
+ *
9
+ * T0: init-board → SSE initial payload → wait for all cards to complete
10
+ * T1: PATCH holdings (+1 row) → verify recomputation (holdings +1, positions +1)
11
+ *
12
+ * Usage:
13
+ * node test/server-http-test.js [--port 7799]
14
+ */
15
+
16
+ import { spawn } from 'node:child_process';
17
+ import { Worker } from 'node:worker_threads';
18
+ import { fileURLToPath } from 'node:url';
19
+ import path from 'node:path';
20
+ import http from 'node:http';
21
+ import fs from 'node:fs';
22
+
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+
26
+ const cliArgs = process.argv.slice(2);
27
+ const portArg = cliArgs.indexOf('--port');
28
+ const cliPort = portArg !== -1 ? parseInt(cliArgs[portArg + 1], 10) : NaN;
29
+ const RUN_ID = `run-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
30
+
31
+ const BOARD_ID = 'live';
32
+ const BOARD_DIR = path.resolve(__dirname, '..');
33
+ const SERVER_SCRIPT = path.resolve(BOARD_DIR, 'server', 'board-server.js');
34
+ const SSE_WORKER_SCRIPT = path.join(__dirname, 'sse-worker.js');
35
+ const CARD_PATTERN = 'cardT*';
36
+ const CHAT_CARD_ID = 'card-portfolio';
37
+
38
+ function resolveServerPort() {
39
+ if (Number.isInteger(cliPort) && cliPort > 0) return cliPort;
40
+ const configPath = path.join(BOARD_DIR, 'server-config.json');
41
+ try {
42
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
43
+ const configured = Number(cfg?.port);
44
+ if (Number.isInteger(configured) && configured > 0) return configured;
45
+ } catch { /* ignore */ }
46
+ return 7799;
47
+ }
48
+
49
+ const PORT = resolveServerPort();
50
+ const BASE = `http://127.0.0.1:${PORT}/api/boards/${BOARD_ID}`;
51
+
52
+ // Resolve and wipe the setup directory so each test run starts clean.
53
+ function resolveSetupDirRoot() {
54
+ const configPath = path.join(BOARD_DIR, 'server-config.json');
55
+ try {
56
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
57
+ const boardSetupDir = cfg?.boards?.[BOARD_ID]?.setupDir;
58
+ if (typeof boardSetupDir === 'string' && boardSetupDir.trim()) {
59
+ return path.resolve(BOARD_DIR, boardSetupDir.trim());
60
+ }
61
+ if (cfg && typeof cfg.setupDir === 'string' && cfg.setupDir.trim()) {
62
+ return path.resolve(BOARD_DIR, cfg.setupDir.trim());
63
+ }
64
+ } catch { /* ignore */ }
65
+ return path.join(BOARD_DIR, '.demo-setup');
66
+ }
67
+
68
+ const SETUP_DIR = path.join(resolveSetupDirRoot(), RUN_ID);
69
+ const BOARD_SETUP_ROOT = path.join(SETUP_DIR, 'boards');
70
+ if (fs.existsSync(SETUP_DIR)) {
71
+ fs.rmSync(SETUP_DIR, { recursive: true, force: true });
72
+ console.log(`[demo-http-test] wiped setup dir: ${SETUP_DIR}`);
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Shared state — accumulated from SSE frames
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const NS = {
80
+ initialPayload: null,
81
+ statusSummary: null,
82
+ statusGeneration: 0,
83
+ computedValues: {},
84
+ chatEvents: [],
85
+ };
86
+
87
+ function applyFrame(payload) {
88
+ if (payload && Array.isArray(payload.cardDefinitions)) {
89
+ if (!NS.initialPayload && payload.cardDefinitions.length > 0) {
90
+ NS.initialPayload = payload;
91
+ }
92
+ const summary = payload.statusSnapshot && payload.statusSnapshot.summary;
93
+ if (summary) {
94
+ NS.statusSummary = summary;
95
+ NS.statusGeneration += 1;
96
+ }
97
+ if (payload.cardRuntimeById) {
98
+ for (const [cardId, runtime] of Object.entries(payload.cardRuntimeById)) {
99
+ if (runtime?.computed_values && Object.keys(runtime.computed_values).length > 0) {
100
+ NS.computedValues[cardId] = runtime.computed_values;
101
+ }
102
+ }
103
+ }
104
+ return;
105
+ }
106
+
107
+ if (payload && payload.kind === 'notification-batch' && Array.isArray(payload.notifications)) {
108
+ for (const n of payload.notifications) {
109
+ const summary = n && n.kind === 'status' && n.status && n.status.summary;
110
+ if (summary) {
111
+ NS.statusSummary = summary;
112
+ NS.statusGeneration += 1;
113
+ }
114
+ if (n && n.kind === 'computed_values' && n.cardId) {
115
+ NS.computedValues[n.cardId] = n.values;
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ function normalizeSseChunkBuffer(buf, chunk) {
122
+ return (buf + chunk.replace(/\r\n/g, '\n'));
123
+ }
124
+
125
+ function parseSseBlocks(buffer) {
126
+ const payloads = [];
127
+ let buf = buffer;
128
+ while (true) {
129
+ const idx = buf.indexOf('\n\n');
130
+ if (idx === -1) break;
131
+ const block = buf.slice(0, idx);
132
+ buf = buf.slice(idx + 2);
133
+ const dataLines = [];
134
+ for (const line of block.split('\n')) {
135
+ if (line.startsWith('data:')) {
136
+ dataLines.push(line.slice(5).replace(/^ /, ''));
137
+ }
138
+ }
139
+ const data = dataLines.join('\n');
140
+ if (!data) continue;
141
+ try {
142
+ payloads.push(JSON.parse(data));
143
+ } catch { /* ignore malformed */ }
144
+ }
145
+ return { payloads, remainder: buf };
146
+ }
147
+
148
+ function startSseClient(sseUrl, onPayload) {
149
+ const req = http.get(sseUrl, (res) => {
150
+ let buf = '';
151
+ res.setEncoding('utf-8');
152
+ res.on('data', (chunk) => {
153
+ buf = normalizeSseChunkBuffer(buf, chunk);
154
+ const parsed = parseSseBlocks(buf);
155
+ buf = parsed.remainder;
156
+ for (const payload of parsed.payloads) onPayload(payload);
157
+ });
158
+ });
159
+ req.on('error', () => {});
160
+ return {
161
+ close() {
162
+ try { req.destroy(); } catch { /* */ }
163
+ },
164
+ };
165
+ }
166
+
167
+ function captureChatEvents(payload, cardId) {
168
+ if (!payload || payload.kind !== 'notification-batch' || !Array.isArray(payload.notifications)) return;
169
+ for (const n of payload.notifications) {
170
+ if (n && n.kind === 'card_chats' && n.cardId === cardId) {
171
+ const messages = Array.isArray(n.messages) ? n.messages : [];
172
+ NS.chatEvents.push({
173
+ at: Date.now(),
174
+ cardId: n.cardId,
175
+ processing: !!n.processing,
176
+ receiving: !!n.receiving,
177
+ messageCount: messages.length,
178
+ messages,
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ function assert(condition, message) {
185
+ if (!condition) {
186
+ console.error(`\n[ASSERT FAILED] ${message}`);
187
+ process.exit(1);
188
+ }
189
+ }
190
+
191
+ function waitUntil(predicate, timeoutMs, label) {
192
+ return new Promise((resolve, reject) => {
193
+ const deadline = Date.now() + timeoutMs;
194
+ const interval = setInterval(() => {
195
+ let result;
196
+ try { result = predicate(); } catch { /* retry */ }
197
+ if (result !== undefined && result !== null && result !== false) {
198
+ clearInterval(interval);
199
+ resolve(result);
200
+ return;
201
+ }
202
+ if (Date.now() > deadline) {
203
+ clearInterval(interval);
204
+ reject(new Error(`Timeout (${timeoutMs}ms) waiting for: ${label}`));
205
+ }
206
+ }, 150);
207
+ });
208
+ }
209
+
210
+ const waitForInitialPayload = (ms = 15_000) =>
211
+ waitUntil(() => NS.initialPayload || false, ms, 'initial SSE payload');
212
+
213
+ const waitForAllCompleted = (ms = 60_000, label = 'all completed') =>
214
+ waitUntil(() => {
215
+ const s = NS.statusSummary;
216
+ if (s && s.card_count > 0 && s.completed === s.card_count) return s;
217
+ return false;
218
+ }, ms, label);
219
+
220
+ const waitForChatPredicate = (predicate, ms, label) =>
221
+ waitUntil(() => predicate(NS.chatEvents) || false, ms, label);
222
+
223
+ function httpGet(url) {
224
+ return new Promise((resolve, reject) => {
225
+ http.get(url, (res) => {
226
+ let body = '';
227
+ res.on('data', c => { body += c; });
228
+ res.on('end', () => {
229
+ try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
230
+ catch { resolve({ status: res.statusCode, data: body }); }
231
+ });
232
+ }).on('error', reject);
233
+ });
234
+ }
235
+
236
+ function httpGetRaw(url) {
237
+ return new Promise((resolve, reject) => {
238
+ http.get(url, (res) => {
239
+ const chunks = [];
240
+ res.on('data', c => { chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)); });
241
+ res.on('end', () => {
242
+ resolve({
243
+ status: res.statusCode,
244
+ body: Buffer.concat(chunks),
245
+ headers: res.headers,
246
+ });
247
+ });
248
+ }).on('error', reject);
249
+ });
250
+ }
251
+
252
+ function httpJson(method, url, payload) {
253
+ return new Promise((resolve, reject) => {
254
+ const u = new URL(url);
255
+ const data = payload != null ? JSON.stringify(payload) : null;
256
+ const opts = {
257
+ hostname: u.hostname,
258
+ port: u.port,
259
+ path: u.pathname,
260
+ method,
261
+ headers: data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {},
262
+ };
263
+ const req = http.request(opts, (res) => {
264
+ let body = '';
265
+ res.on('data', c => { body += c; });
266
+ res.on('end', () => {
267
+ try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
268
+ catch { resolve({ status: res.statusCode, data: body }); }
269
+ });
270
+ });
271
+ req.on('error', reject);
272
+ if (data) req.write(data);
273
+ req.end();
274
+ });
275
+ }
276
+
277
+ function httpUploadChatFile(url, fileName, content, contentType = 'text/plain; charset=utf-8') {
278
+ return new Promise((resolve, reject) => {
279
+ const u = new URL(url);
280
+ const data = Buffer.from(content, 'utf-8');
281
+ const opts = {
282
+ hostname: u.hostname,
283
+ port: u.port,
284
+ path: u.pathname + u.search,
285
+ method: 'POST',
286
+ headers: {
287
+ 'Content-Type': contentType,
288
+ 'Content-Length': data.length,
289
+ 'x-file-name': encodeURIComponent(fileName),
290
+ },
291
+ };
292
+ const req = http.request(opts, (res) => {
293
+ let body = '';
294
+ res.on('data', c => { body += c; });
295
+ res.on('end', () => {
296
+ try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
297
+ catch { resolve({ status: res.statusCode, data: body }); }
298
+ });
299
+ });
300
+ req.on('error', reject);
301
+ req.write(data);
302
+ req.end();
303
+ });
304
+ }
305
+
306
+ function startServer(port) {
307
+ return new Promise((resolve, reject) => {
308
+ const proc = spawn(process.execPath, [SERVER_SCRIPT], {
309
+ stdio: ['ignore', 'pipe', 'pipe'],
310
+ windowsHide: true,
311
+ env: {
312
+ ...process.env,
313
+ DEMO_SERVER_PORT: String(port),
314
+ DEMO_SETUP_DIR: SETUP_DIR,
315
+ DEMO_BOARD_SETUP_ROOT: BOARD_SETUP_ROOT,
316
+ DEMO_CARDS_PATTERN: CARD_PATTERN,
317
+ },
318
+ });
319
+ let ready = false;
320
+
321
+ proc.stdout.on('data', (chunk) => {
322
+ const text = chunk.toString('utf-8');
323
+ process.stdout.write(`[server] ${text}`);
324
+ if (!ready && text.includes('listening on')) {
325
+ ready = true;
326
+ resolve(proc);
327
+ }
328
+ });
329
+ proc.stderr.on('data', (chunk) => process.stderr.write(`[server:err] ${chunk}`));
330
+ proc.on('error', reject);
331
+ proc.on('exit', (code) => {
332
+ if (!ready) reject(new Error(`Server exited early: code ${code}`));
333
+ });
334
+
335
+ setTimeout(() => {
336
+ if (!ready) reject(new Error('Server startup timeout (15s)'));
337
+ }, 15_000);
338
+ });
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Test sequence
343
+ // ---------------------------------------------------------------------------
344
+
345
+ console.log('\n=== live board HTTP+SSE smoke test ===');
346
+ console.log(`target: ${BASE}`);
347
+ console.log(`card pattern: ${CARD_PATTERN}`);
348
+
349
+ const serverProc = await startServer(PORT);
350
+ let sseWorker = null;
351
+ let chatSseClient = null;
352
+ let chatSseClientId = '';
353
+
354
+ try {
355
+ // ── T0: init-board, SSE connect, wait for initial completion ──
356
+
357
+ // Register the 'live' board via POST (v8 runtime requires explicit registration)
358
+ const regRes = await httpJson('POST', `http://127.0.0.1:${PORT}/api/boards`, { id: BOARD_ID, label: 'Live' });
359
+ assert(regRes.status === 200 || regRes.status === 201 || regRes.status === 409,
360
+ `POST /api/boards returned ${regRes.status}: ${JSON.stringify(regRes.data)}`);
361
+ console.log(`[setup] board '${BOARD_ID}' registered (${regRes.status})`);
362
+
363
+ console.log('\n=== T0 Step 1: init-board ===');
364
+ const initRes = await httpGet(`${BASE}/init-board`);
365
+ assert(initRes.status === 200, `init-board returned ${initRes.status}`);
366
+ console.log('[T0.1] init-board ok');
367
+
368
+ console.log('\n=== T0 Step 2: start SSE worker ===');
369
+ const sseClientId = `server-http-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
370
+ const sseUrl = `${BASE}/sse?clientId=${encodeURIComponent(sseClientId)}`;
371
+ sseWorker = new Worker(SSE_WORKER_SCRIPT, {
372
+ workerData: { sseUrl },
373
+ });
374
+ sseWorker.on('message', (msg) => {
375
+ if (msg.type === 'frame') applyFrame(msg.payload);
376
+ else if (msg.type === 'error') console.error(`[sse-worker] ${msg.message}`);
377
+ });
378
+ sseWorker.on('error', (err) => console.error(`[sse-worker] uncaught: ${err.message}`));
379
+
380
+ const initialPayload = await waitForInitialPayload();
381
+ const cardCount = Array.isArray(initialPayload.cardDefinitions) ? initialPayload.cardDefinitions.length : 0;
382
+ assert(cardCount === 3, `expected 3 cards (cardT*), got ${cardCount}`);
383
+ const cardIds = initialPayload.cardDefinitions.map(c => c.id).sort();
384
+ console.log(`[T0.2] SSE initial payload received (${cardCount} cards: ${cardIds.join(', ')})`);
385
+
386
+ console.log('\n=== T0 Step 3: wait for all cards to complete ===');
387
+ const t0Summary = await waitForAllCompleted(30_000, 'T0 initial completion');
388
+ assert(t0Summary.failed === 0, `T0 expected failed=0, got ${t0Summary.failed}`);
389
+ console.log(`[T0.3] completed: ${JSON.stringify(t0Summary)}`);
390
+
391
+ console.log('\n=== T0 Step 4: board-status cross-check ===');
392
+ const statusRes = await httpGet(`${BASE}/board-status`);
393
+ assert(statusRes.status === 200, `board-status returned ${statusRes.status}`);
394
+ const httpSummary = statusRes.data?.statusSnapshot?.summary;
395
+ assert(httpSummary, 'statusSnapshot.summary missing from board-status');
396
+ assert(httpSummary.completed === httpSummary.card_count, `not all complete: ${JSON.stringify(httpSummary)}`);
397
+ console.log(`[T0.4] board-status: ${JSON.stringify(httpSummary)}`);
398
+
399
+ // Verify computed_values arrived for portfolio-value card
400
+ const t0Positions = NS.computedValues['card-portfolio-value']?.positions;
401
+ assert(Array.isArray(t0Positions) && t0Positions.length > 0, 'T0 positions missing from computed_values');
402
+ console.log(`[T0] ok: ${t0Positions.length} positions computed`);
403
+
404
+ // ── T1: PATCH holdings (+1 row), verify recomputation ──
405
+ console.log('\n=== T1: patch holdings (+1 row) ===');
406
+
407
+ // Read current holdings from card store
408
+ const portfolioCardRes = await httpGet(`${BASE}/cards/card-portfolio`);
409
+ assert(portfolioCardRes.status === 200, `GET card-portfolio returned ${portfolioCardRes.status}`);
410
+ const existingHoldings = portfolioCardRes.data?.card_data?.holdings;
411
+ assert(Array.isArray(existingHoldings), 'card-portfolio.card_data.holdings missing');
412
+ const t0HoldingsCount = existingHoldings.length;
413
+ const t0PositionsCount = t0Positions.length;
414
+
415
+ // Pick a ticker not already in holdings
416
+ const candidates = ['AAPL', 'MSFT', 'AMZN', 'TSLA', 'META', 'GOOG', 'NVDA', 'NFLX', 'INTC', 'AMD',
417
+ 'IBM', 'ORCL', 'ADBE', 'CRM', 'QCOM'];
418
+ const existingTickers = new Set(existingHoldings.map(r => r.ticker));
419
+ const available = candidates.filter(t => !existingTickers.has(t));
420
+ assert(available.length > 0, 'No available ticker to add');
421
+ const newTicker = available[0];
422
+
423
+ const newHoldings = [...existingHoldings, { ticker: newTicker, quantity: 1, cost_basis: 100 }];
424
+ const patchRes = await httpJson('PATCH', `${BASE}/cards/card-portfolio`, { card_data: { holdings: newHoldings } });
425
+ assert(patchRes.status === 200, `PATCH card-portfolio returned ${patchRes.status}`);
426
+
427
+ // Wait for re-completion after the patch triggers a new cycle
428
+ NS.statusSummary = null;
429
+ await new Promise(r => setTimeout(r, 4000));
430
+ const t1Summary = await waitForAllCompleted(30_000, 'T1 holdings patch');
431
+ assert(t1Summary.failed === 0, `T1 failed=${t1Summary.failed}`);
432
+
433
+ // Verify holdings +1 from card store
434
+ const t1PortfolioRes = await httpGet(`${BASE}/cards/card-portfolio`);
435
+ assert(t1PortfolioRes.status === 200, `GET card-portfolio after patch returned ${t1PortfolioRes.status}`);
436
+ const afterHoldings = t1PortfolioRes.data?.card_data?.holdings;
437
+ const afterHoldingsCount = Array.isArray(afterHoldings) ? afterHoldings.length : 0;
438
+
439
+ // Verify positions +1 from computed_values captured via SSE
440
+ const afterPositions = NS.computedValues['card-portfolio-value']?.positions;
441
+ const afterPositionsCount = Array.isArray(afterPositions) ? afterPositions.length : 0;
442
+
443
+ assert(afterHoldingsCount === t0HoldingsCount + 1,
444
+ `Expected holdings rows +1 (before=${t0HoldingsCount}, after=${afterHoldingsCount})`);
445
+ assert(afterPositionsCount === t0PositionsCount + 1,
446
+ `Expected positions rows +1 (before=${t0PositionsCount}, after=${afterPositionsCount})`);
447
+ console.log(`[T1] ok: holdings ${t0HoldingsCount}->${afterHoldingsCount}, ` +
448
+ `positions ${t0PositionsCount}->${afterPositionsCount}, added=${newTicker}`);
449
+
450
+ // ── T2: plain file upload API + card_data.files + download roundtrip ──
451
+ console.log('\n=== T2: plain file upload -> card_data.files -> download ===');
452
+ const t2CardBefore = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}`);
453
+ assert(t2CardBefore.status === 200, `T2 pre card read returned ${t2CardBefore.status}`);
454
+ const t2FilesBefore = Array.isArray(t2CardBefore.data?.card_data?.files)
455
+ ? t2CardBefore.data.card_data.files
456
+ : [];
457
+ const t2BeforeCount = t2FilesBefore.length;
458
+
459
+ const t2UploadText = `plain-file-upload-${Date.now()}`;
460
+ const t2UploadName = 't2-upload.txt';
461
+ const t2UploadRes = await httpUploadChatFile(
462
+ `${BASE}/cards/${CHAT_CARD_ID}/files`,
463
+ t2UploadName,
464
+ t2UploadText,
465
+ );
466
+ assert(t2UploadRes.status === 200, `T2 file upload returned ${t2UploadRes.status}`);
467
+ const t2UploadedFile = t2UploadRes.data?.file;
468
+ assert(t2UploadedFile && typeof t2UploadedFile === 'object', 'T2 upload response missing file metadata');
469
+ assert(String(t2UploadedFile?.name || '') === t2UploadName, 'T2 uploaded file name mismatch');
470
+
471
+ const t2CardAfter = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}`);
472
+ assert(t2CardAfter.status === 200, `T2 post card read returned ${t2CardAfter.status}`);
473
+ const t2FilesAfter = Array.isArray(t2CardAfter.data?.card_data?.files)
474
+ ? t2CardAfter.data.card_data.files
475
+ : [];
476
+ assert(t2FilesAfter.length === t2BeforeCount + 1, `T2 expected files +1 (before=${t2BeforeCount}, after=${t2FilesAfter.length})`);
477
+
478
+ const t2FileIndex = t2FilesAfter.findIndex((f) => String(f?.stored_name || '') === String(t2UploadedFile?.stored_name || ''));
479
+ assert(t2FileIndex >= 0, 'T2 uploaded file metadata not found in card_data.files');
480
+
481
+ const t2DownloadRes = await httpGetRaw(
482
+ `${BASE}/cards/${CHAT_CARD_ID}/files/${t2FileIndex}?sn=${encodeURIComponent(String(t2UploadedFile?.stored_name || ''))}`,
483
+ );
484
+ assert(t2DownloadRes.status === 200, `T2 file download returned ${t2DownloadRes.status}`);
485
+ const t2DownloadedText = t2DownloadRes.body.toString('utf-8');
486
+ assert(t2DownloadedText === t2UploadText, 'T2 downloaded content mismatch');
487
+ console.log('[T2] ok: card_data.files updated and file download endpoint returned exact bytes');
488
+
489
+ // ── T3*: chat protocol over API + SSE ──
490
+ {
491
+ console.log('\n=== T3: probe chat protocol (SSE lifecycle) ===');
492
+ chatSseClientId = `chat-proto-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
493
+ chatSseClient = startSseClient(`${BASE}/sse?clientId=${encodeURIComponent(chatSseClientId)}`, (payload) => {
494
+ captureChatEvents(payload, CHAT_CARD_ID);
495
+ });
496
+ await new Promise((r) => setTimeout(r, 400));
497
+
498
+ const subRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/chats/subscribe-sse`, { clientId: chatSseClientId });
499
+ assert(subRes.status === 200, `chat subscribe returned ${subRes.status}`);
500
+
501
+ const t2Before = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
502
+ assert(t2Before.status === 200, `T3 pre chats returned ${t2Before.status}`);
503
+ const t2BeforeMessages = Array.isArray(t2Before.data?.messages) ? t2Before.data.messages : [];
504
+ const t2BeforeCount = t2BeforeMessages.length;
505
+ const t2ProbePrompt = `Probe protocol validation ${Date.now()}`;
506
+
507
+ const t2SendRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/actions`, {
508
+ actionType: 'chat-send',
509
+ payload: {
510
+ text: JSON.stringify({
511
+ prompt: t2ProbePrompt,
512
+ probe: true,
513
+ chatTimeMs: 2200,
514
+ chatTimeoutMs: 20000,
515
+ }),
516
+ },
517
+ });
518
+ assert(t2SendRes.status === 200, `T3 chat-send returned ${t2SendRes.status}`);
519
+
520
+ const t2UserOrProcessing = await waitForChatPredicate((events) => {
521
+ const slice = events.filter((e) => e.messageCount >= t2BeforeCount + 1 || e.processing === true);
522
+ return slice.length > 0 ? slice[slice.length - 1] : false;
523
+ }, 30_000, 'T3 user/proc signal');
524
+ assert(!!t2UserOrProcessing, 'T3 missing user/proc signal');
525
+
526
+ const t2Assistant = await waitForChatPredicate((events) => {
527
+ for (let i = events.length - 1; i >= 0; i -= 1) {
528
+ const e = events[i];
529
+ if (e.messageCount < t2BeforeCount + 2) continue;
530
+ const last = e.messages[e.messages.length - 1];
531
+ if (last?.role === 'assistant' && String(last.text || '').includes(`Echo: ${t2ProbePrompt}`)) {
532
+ return e;
533
+ }
534
+ }
535
+ return false;
536
+ }, 45_000, 'T3 assistant echo');
537
+ assert(!!t2Assistant, 'T3 assistant echo not observed on SSE');
538
+
539
+ const t2ProcessingCleared = await waitForChatPredicate((events) => {
540
+ for (let i = events.length - 1; i >= 0; i -= 1) {
541
+ const e = events[i];
542
+ if (e.messageCount >= t2BeforeCount + 2 && e.processing === false) return e;
543
+ }
544
+ return false;
545
+ }, 30_000, 'T3 processing clear');
546
+ assert(!!t2ProcessingCleared, 'T3 processing clear not observed');
547
+
548
+ const t2After = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
549
+ assert(t2After.status === 200, `T3 post chats returned ${t2After.status}`);
550
+ const t2AfterMessages = Array.isArray(t2After.data?.messages) ? t2After.data.messages : [];
551
+ const t2NewMessages = t2AfterMessages.slice(t2BeforeCount);
552
+ assert(t2NewMessages.length >= 2, `T3 expected at least 2 new chat messages, got ${t2NewMessages.length}`);
553
+ const t2User = t2NewMessages.find((m) => m?.role === 'user');
554
+ const t2AssistantMsg = t2NewMessages.find((m) => m?.role === 'assistant');
555
+ assert(!!t2User && typeof t2User.id === 'string', 'T3 user chat message missing id');
556
+ assert(String(t2User?.text || '').includes(t2ProbePrompt), 'T3 user file text mismatch');
557
+ assert(!!t2AssistantMsg && typeof t2AssistantMsg.id === 'string', 'T3 assistant chat message missing id');
558
+ assert(String(t2AssistantMsg?.text || '').includes(`Echo: ${t2ProbePrompt}`), 'T3 assistant echo file content mismatch');
559
+ console.log('[T3] ok: probe lifecycle observed (processing/user any-order, assistant write, processing clear)');
560
+
561
+ /*
562
+ // ── T3a: non-probe chat protocol over API + SSE ──
563
+ // Disabled in the public example — requires a configured Azure Foundry
564
+ // endpoint and agent_id in server-config.json.
565
+ console.log('\n=== T3a: non-probe chat protocol (expect paris) ===');
566
+ const t2aBefore = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
567
+ assert(t2aBefore.status === 200, `T3a pre chats returned ${t2aBefore.status}`);
568
+ const t2aBeforeMessages = Array.isArray(t2aBefore.data?.messages) ? t2aBefore.data.messages : [];
569
+ const t2aBeforeCount = t2aBeforeMessages.length;
570
+ const t2aPrompt = 'Just answer what is the capital of France. No Fluff. No COmmentary. No Markup Respond in lower case in one word.';
571
+
572
+ const t2aSendRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/actions`, {
573
+ actionType: 'chat-send',
574
+ payload: {
575
+ text: JSON.stringify({
576
+ prompt: t2aPrompt,
577
+ chatTimeoutMs: 180000,
578
+ }),
579
+ },
580
+ });
581
+ assert(t2aSendRes.status === 200, `T3a chat-send returned ${t2aSendRes.status}`);
582
+
583
+ const t2aAssistant = await waitForChatPredicate((events) => {
584
+ for (let i = events.length - 1; i >= 0; i -= 1) {
585
+ const e = events[i];
586
+ if (e.messageCount < t2aBeforeCount + 2) continue;
587
+ const last = e.messages[e.messages.length - 1];
588
+ if (last?.role === 'assistant' && /paris/i.test(String(last.text || ''))) return e;
589
+ }
590
+ return false;
591
+ }, 240_000, 'T3a assistant response with paris');
592
+ assert(!!t2aAssistant, 'T3a assistant response with paris not observed on SSE');
593
+
594
+ const t2aAfter = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
595
+ assert(t2aAfter.status === 200, `T3a post chats returned ${t2aAfter.status}`);
596
+ const t2aAfterMessages = Array.isArray(t2aAfter.data?.messages) ? t2aAfter.data.messages : [];
597
+ const t2aNewMessages = t2aAfterMessages.slice(t2aBeforeCount);
598
+ assert(t2aNewMessages.length >= 2, `T3a expected at least 2 new chat messages, got ${t2aNewMessages.length}`);
599
+ const t2aAssistantMsg = [...t2aNewMessages].reverse().find((m) => m?.role === 'assistant');
600
+ assert(!!t2aAssistantMsg && typeof t2aAssistantMsg.id === 'string', 'T3a assistant chat message missing id');
601
+ assert(/paris/i.test(String(t2aAssistantMsg?.text || '')), 'T3a assistant file content missing paris');
602
+ console.log('[T3a] ok: non-probe response contains paris');
603
+ */
604
+
605
+ // ── T3b: probe-echo chat + file upload protocol over API + SSE ──
606
+ console.log('\n=== T3b: probe-echo chat with file upload protocol ===');
607
+ const t2bBefore = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
608
+ assert(t2bBefore.status === 200, `T3b pre chats returned ${t2bBefore.status}`);
609
+ const t2bBeforeMessages = Array.isArray(t2bBefore.data?.messages) ? t2bBefore.data.messages : [];
610
+ const t2bBeforeCount = t2bBeforeMessages.length;
611
+
612
+ const t2bUploadRes = await httpUploadChatFile(
613
+ `${BASE}/cards/${CHAT_CARD_ID}/files?inChat=true`,
614
+ 'q1.txt',
615
+ 'what is the capital of japan',
616
+ );
617
+ assert(t2bUploadRes.status === 200, `T3b file upload returned ${t2bUploadRes.status}`);
618
+ const uploadedFile = t2bUploadRes.data?.file;
619
+ assert(uploadedFile && typeof uploadedFile === 'object', 'T3b upload response missing file metadata');
620
+
621
+ const t2bAfterUpload = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
622
+ assert(t2bAfterUpload.status === 200, `T3b chats after upload returned ${t2bAfterUpload.status}`);
623
+ const t2bUploadMessages = Array.isArray(t2bAfterUpload.data?.messages) ? t2bAfterUpload.data.messages : [];
624
+ const t2bUploadNewMessages = t2bUploadMessages.slice(t2bBeforeCount);
625
+ const t2bUploadSystem = t2bUploadNewMessages.find((m) => m?.role === 'system');
626
+ assert(!!t2bUploadSystem, 'T3b upload protocol missing system chat file');
627
+ assert(String(t2bUploadSystem?.text || '').toLowerCase().includes('file uploaded:'), 'T3b upload system message does not describe uploaded file');
628
+
629
+ const t2bSendBaseline = t2bUploadMessages.length;
630
+
631
+ const t2bPrompt = `probe echo file-upload validation ${Date.now()}`;
632
+ const t2bSendRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/actions`, {
633
+ actionType: 'chat-send',
634
+ payload: {
635
+ text: JSON.stringify({
636
+ prompt: t2bPrompt,
637
+ probe: true,
638
+ chatTimeMs: 2200,
639
+ }),
640
+ files: [uploadedFile],
641
+ },
642
+ });
643
+ assert(t2bSendRes.status === 200, `T3b chat-send returned ${t2bSendRes.status}`);
644
+
645
+ const t2bUserOrProcessing = await waitForChatPredicate((events) => {
646
+ for (let i = events.length - 1; i >= 0; i -= 1) {
647
+ const e = events[i];
648
+ if (e.messageCount >= t2bSendBaseline + 1 || e.processing === true) return e;
649
+ }
650
+ return false;
651
+ }, 45_000, 'T3b user/proc signal');
652
+ assert(!!t2bUserOrProcessing, 'T3b user/proc signal not observed');
653
+
654
+ const t2bAssistantOnSse = await waitForChatPredicate((events) => {
655
+ for (let i = events.length - 1; i >= 0; i -= 1) {
656
+ const e = events[i];
657
+ if (e.messageCount < t2bSendBaseline + 2) continue;
658
+ const last = e.messages[e.messages.length - 1];
659
+ if (last?.role === 'assistant' && String(last.text || '').includes(`Echo: ${t2bPrompt}`)) return e;
660
+ }
661
+ return false;
662
+ }, 60_000, 'T3b assistant response');
663
+ assert(!!t2bAssistantOnSse, 'T3b assistant response not observed on SSE');
664
+
665
+ const t2bAfter = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats`);
666
+ assert(t2bAfter.status === 200, `T3b post chats returned ${t2bAfter.status}`);
667
+ const t2bAfterMessages = Array.isArray(t2bAfter.data?.messages) ? t2bAfter.data.messages : [];
668
+ const t2bNewMessages = t2bAfterMessages.slice(t2bSendBaseline);
669
+ assert(t2bNewMessages.length >= 2, `T3b expected at least 2 chat messages after send, got ${t2bNewMessages.length}`);
670
+
671
+ const t2bUser = t2bNewMessages.find((m) => m?.role === 'user');
672
+ const t2bAssistantMsg = t2bNewMessages.find((m) => m?.role === 'assistant');
673
+
674
+ assert(!!t2bUser && typeof t2bUser.id === 'string', 'T3b missing user chat message notification');
675
+ assert(!!t2bAssistantMsg && typeof t2bAssistantMsg.id === 'string', 'T3b missing assistant chat message notification');
676
+ assert(Array.isArray(t2bUser?.files) && t2bUser.files.length === 1, 'T3b user chat message missing uploaded file metadata');
677
+ assert(String(t2bAssistantMsg?.text || '').includes(`Echo: ${t2bPrompt}`), 'T3b assistant file content mismatch');
678
+
679
+ const t2bProcessingCleared = await waitForChatPredicate((events) => {
680
+ for (let i = events.length - 1; i >= 0; i -= 1) {
681
+ const e = events[i];
682
+ if (e.messageCount >= t2bSendBaseline + 2 && e.processing === false) return e;
683
+ }
684
+ return false;
685
+ }, 30_000, 'T3b processing clear');
686
+ assert(!!t2bProcessingCleared, 'T3b processing clear not observed');
687
+ console.log('[T3b] ok: upload protocol (system/user) and chat protocol (.processing/user/assistant/.processing clear) observed');
688
+ }
689
+
690
+ console.log('\n=== All smoke checks passed ===\n');
691
+ } finally {
692
+ if (chatSseClientId) {
693
+ try {
694
+ await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/chats/unsubscribe-sse`, { clientId: chatSseClientId });
695
+ } catch { /* ignore */ }
696
+ }
697
+ if (chatSseClient) chatSseClient.close();
698
+ serverProc.kill();
699
+ await new Promise((r) => serverProc.on('exit', r));
700
+ if (sseWorker) await sseWorker.terminate();
701
+
702
+ // Clean up the test setup directory
703
+ if (fs.existsSync(SETUP_DIR)) {
704
+ fs.rmSync(SETUP_DIR, { recursive: true, force: true });
705
+ }
706
+ console.log('[demo-http-test] server stopped, setup dir cleaned');
707
+ }