yaml-flow 7.0.0 → 7.1.0

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 (86) hide show
  1. package/browser/asset-integrity.json +1 -1
  2. package/browser/board-livecards-client.js +1 -1
  3. package/browser/board-livecards-client.js.map +1 -1
  4. package/browser/board-livecards-localstorage.js +5 -5
  5. package/browser/board-livecards-localstorage.js.map +1 -1
  6. package/browser/live-cards.js +3 -1
  7. package/dist/{board-live-cards-public-CW5074xr.d.cts → board-live-cards-public-5n1-syA3.d.cts} +1 -2
  8. package/dist/{board-live-cards-public-hnZo0mAf.d.ts → board-live-cards-public-CK_J8uv0.d.ts} +1 -2
  9. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -2
  10. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -1
  11. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +2 -2
  12. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +2 -2
  13. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -2
  14. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -1
  15. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -1
  16. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -1
  17. package/dist/cli/node/artifacts-store-cli.cjs +5 -5
  18. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -1
  19. package/dist/cli/node/artifacts-store-cli.js +5 -5
  20. package/dist/cli/node/artifacts-store-cli.js.map +1 -1
  21. package/dist/cli/node/board-live-cards-cli.cjs +7 -7
  22. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -1
  23. package/dist/cli/node/board-live-cards-cli.js +7 -7
  24. package/dist/cli/node/board-live-cards-cli.js.map +1 -1
  25. package/dist/cli/node/card-store-cli.cjs +4 -4
  26. package/dist/cli/node/card-store-cli.cjs.map +1 -1
  27. package/dist/cli/node/card-store-cli.js +4 -4
  28. package/dist/cli/node/card-store-cli.js.map +1 -1
  29. package/dist/cli/node/execution-adapter.cjs +1 -1
  30. package/dist/cli/node/execution-adapter.cjs.map +1 -1
  31. package/dist/cli/node/execution-adapter.js +1 -1
  32. package/dist/cli/node/execution-adapter.js.map +1 -1
  33. package/dist/cli/node/fs-board-adapter.cjs +7 -7
  34. package/dist/cli/node/fs-board-adapter.cjs.map +1 -1
  35. package/dist/cli/node/fs-board-adapter.d.cts +2 -2
  36. package/dist/cli/node/fs-board-adapter.d.ts +2 -2
  37. package/dist/cli/node/fs-board-adapter.js +7 -7
  38. package/dist/cli/node/fs-board-adapter.js.map +1 -1
  39. package/dist/cli/node/source-cli-task-executor.cjs +2 -2
  40. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -1
  41. package/dist/cli/node/source-cli-task-executor.js +2 -2
  42. package/dist/cli/node/source-cli-task-executor.js.map +1 -1
  43. package/dist/execution-refs.cjs +2 -2
  44. package/dist/execution-refs.cjs.map +1 -1
  45. package/dist/execution-refs.d.cts +9 -4
  46. package/dist/execution-refs.d.ts +9 -4
  47. package/dist/execution-refs.js +2 -2
  48. package/dist/execution-refs.js.map +1 -1
  49. package/dist/server-runtime/index.cjs +4 -4
  50. package/dist/server-runtime/index.cjs.map +1 -1
  51. package/dist/server-runtime/index.d.cts +3 -3
  52. package/dist/server-runtime/index.d.ts +3 -3
  53. package/dist/server-runtime/index.js +4 -4
  54. package/dist/server-runtime/index.js.map +1 -1
  55. package/dist/step-machine-public/index.cjs +2 -1
  56. package/dist/step-machine-public/index.cjs.map +1 -1
  57. package/dist/step-machine-public/index.d.cts +7 -0
  58. package/dist/step-machine-public/index.d.ts +7 -0
  59. package/dist/step-machine-public/index.js +2 -1
  60. package/dist/step-machine-public/index.js.map +1 -1
  61. package/dist/storage-refs.cjs +2 -2
  62. package/dist/storage-refs.cjs.map +1 -1
  63. package/dist/storage-refs.d.cts +1 -2
  64. package/dist/storage-refs.d.ts +1 -2
  65. package/dist/storage-refs.js +2 -2
  66. package/dist/storage-refs.js.map +1 -1
  67. package/dist/{types-BxEFcVK9.d.cts → types-CU3DjTKL.d.cts} +1 -1
  68. package/dist/{types-B1ZRa4aI.d.ts → types-HGDTWIun.d.ts} +1 -1
  69. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +13 -0
  70. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.py +398 -0
  71. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +4 -4
  72. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +1 -1
  73. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +1 -1
  74. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +1 -1
  75. package/examples/example-board/agent-instructions.md +1 -1
  76. package/examples/example-board/cards/{card-market-prices.json → cardT-market-prices.json} +2 -2
  77. package/examples/example-board/cards/{card-portfolio.json → cardT-portfolio.json} +3 -13
  78. package/examples/example-board/demo-server-config.json +1 -1
  79. package/examples/example-board/demo-server.js +48 -25
  80. package/examples/example-board/demo-shell-localstorage.html +3 -3
  81. package/examples/example-board/demo-shell-with-server.html +2 -2
  82. package/examples/example-board/demo-task-executor.js +4 -8
  83. package/package.json +2 -2
  84. package/step-machine-cli.js +1 -1
  85. package/examples/example-board/cards/_index.json +0 -47
  86. /package/examples/example-board/cards/{card-portfolio-value.json → cardT-portfolio-value.json} +0 -0
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env python3
2
+ """portfolio-tracker-http-test.py
3
+
4
+ E2E test for the portfolio-tracker board via HTTP + SSE.
5
+
6
+ Architecture mirrors portfolio-tracker-http-test.js:
7
+ - background SSE consumer thread accumulates NotificationState (NS)
8
+ - main thread drives HTTP PATCH/GET and polls NS for waits
9
+
10
+ Usage:
11
+ python portfolio-tracker-http-test.py [--port 7801] [--server node|py]
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import socket
20
+ import subprocess
21
+ import sys
22
+ import threading
23
+ import time
24
+ import urllib.error
25
+ import urllib.request
26
+ from pathlib import Path
27
+
28
+
29
+ HERE = Path(__file__).resolve().parent
30
+ SERVER_SCRIPT = HERE / "portfolio-tracker-server.js"
31
+ PY_SERVER_SCRIPT = HERE / "portfolio-tracker-server.py"
32
+
33
+
34
+ def parse_args() -> argparse.Namespace:
35
+ parser = argparse.ArgumentParser()
36
+ parser.add_argument("--port", type=int, default=None)
37
+ parser.add_argument("--server", choices=["node", "py"], default="py")
38
+ return parser.parse_args()
39
+
40
+
41
+ ARGS = parse_args()
42
+ PORT = ARGS.port if ARGS.port is not None else (7801 if ARGS.server == "py" else 7800)
43
+ BASE = f"http://127.0.0.1:{PORT}/api/board"
44
+
45
+
46
+ class TestFailure(RuntimeError):
47
+ pass
48
+
49
+
50
+ def assert_true(condition: bool, message: str) -> None:
51
+ if not condition:
52
+ raise TestFailure(message)
53
+
54
+
55
+ NS_LOCK = threading.Lock()
56
+ NS = {
57
+ "initialPayload": None,
58
+ "statusSummary": None,
59
+ "statusGeneration": 0,
60
+ "dataObjects": {},
61
+ "computedValues": {},
62
+ }
63
+
64
+
65
+ def apply_frame(payload: dict) -> None:
66
+ with NS_LOCK:
67
+ if isinstance(payload, dict) and payload.get("cardDefinitions") is not None:
68
+ NS["initialPayload"] = payload
69
+ status = payload.get("statusSnapshot") or {}
70
+ if isinstance(status, dict) and isinstance(status.get("summary"), dict):
71
+ NS["statusSummary"] = status["summary"]
72
+ NS["statusGeneration"] += 1
73
+
74
+ dot = payload.get("dataObjectsByToken")
75
+ if isinstance(dot, dict):
76
+ NS["dataObjects"].update(dot)
77
+
78
+ runtimes = payload.get("cardRuntimeById")
79
+ if isinstance(runtimes, dict):
80
+ for card_id, runtime in runtimes.items():
81
+ if not isinstance(runtime, dict):
82
+ continue
83
+ cv = runtime.get("computed_values")
84
+ if isinstance(cv, dict) and cv:
85
+ NS["computedValues"][card_id] = cv
86
+ return
87
+
88
+ if (
89
+ isinstance(payload, dict)
90
+ and payload.get("kind") == "notification-batch"
91
+ and isinstance(payload.get("notifications"), list)
92
+ ):
93
+ for note in payload["notifications"]:
94
+ if not isinstance(note, dict):
95
+ continue
96
+ kind = note.get("kind")
97
+ if kind == "status" and isinstance((note.get("status") or {}).get("summary"), dict):
98
+ NS["statusSummary"] = note["status"]["summary"]
99
+ NS["statusGeneration"] += 1
100
+ elif kind == "data_object" and isinstance(note.get("key"), str):
101
+ NS["dataObjects"][note["key"]] = note.get("payload")
102
+ elif kind == "computed_values" and isinstance(note.get("cardId"), str):
103
+ NS["computedValues"][note["cardId"]] = note.get("values")
104
+
105
+
106
+ def get_ns_snapshot() -> dict:
107
+ with NS_LOCK:
108
+ return {
109
+ "initialPayload": NS["initialPayload"],
110
+ "statusSummary": NS["statusSummary"],
111
+ "statusGeneration": NS["statusGeneration"],
112
+ "dataObjects": dict(NS["dataObjects"]),
113
+ "computedValues": dict(NS["computedValues"]),
114
+ }
115
+
116
+
117
+ def wait_until(predicate, timeout_s: float, label: str):
118
+ deadline = time.monotonic() + timeout_s
119
+ while time.monotonic() < deadline:
120
+ try:
121
+ result = predicate()
122
+ except Exception:
123
+ result = None
124
+ if result not in (None, False):
125
+ return result
126
+ time.sleep(0.15)
127
+
128
+ snap = get_ns_snapshot()
129
+ raise TestFailure(
130
+ f"Timeout ({timeout_s}s) waiting for: {label}\n"
131
+ f" NS.statusSummary={json.dumps(snap['statusSummary'])}\n"
132
+ f" dataObjects={json.dumps(sorted(snap['dataObjects'].keys()))}"
133
+ )
134
+
135
+
136
+ def wait_for_initial_payload(timeout_s: float = 15.0):
137
+ return wait_until(lambda: get_ns_snapshot()["initialPayload"], timeout_s, "initial SSE payload")
138
+
139
+
140
+ def wait_for_all_completed(timeout_s: float = 60.0, label: str = "all completed"):
141
+ def _pred():
142
+ s = get_ns_snapshot()["statusSummary"]
143
+ if isinstance(s, dict) and s.get("card_count", 0) > 0 and s.get("completed") == s.get("card_count"):
144
+ return s
145
+ return False
146
+
147
+ return wait_until(_pred, timeout_s, label)
148
+
149
+
150
+ def wait_for_price_symbols(expected_symbols: list[str], timeout_s: float = 30.0, label: str = "price symbols"):
151
+ expected = ",".join(sorted(expected_symbols))
152
+
153
+ def _pred():
154
+ prices = get_ns_snapshot()["dataObjects"].get("prices")
155
+ if not isinstance(prices, dict):
156
+ return False
157
+ actual = ",".join(sorted(prices.keys()))
158
+ return prices if actual == expected else False
159
+
160
+ return wait_until(_pred, timeout_s, f"{label}: expected [{expected}]")
161
+
162
+
163
+ def http_json(method: str, path: str, payload: dict | None = None) -> tuple[int, object]:
164
+ data = None
165
+ headers = {}
166
+ if payload is not None:
167
+ data = json.dumps(payload).encode("utf-8")
168
+ headers = {
169
+ "Content-Type": "application/json",
170
+ "Content-Length": str(len(data)),
171
+ }
172
+
173
+ req = urllib.request.Request(f"{BASE}{path}", data=data, method=method.upper(), headers=headers)
174
+ try:
175
+ with urllib.request.urlopen(req, timeout=30) as resp:
176
+ raw = resp.read().decode("utf-8")
177
+ try:
178
+ body = json.loads(raw) if raw else {}
179
+ except json.JSONDecodeError:
180
+ body = raw
181
+ return resp.status, body
182
+ except urllib.error.HTTPError as e:
183
+ raw = e.read().decode("utf-8", errors="replace")
184
+ try:
185
+ body = json.loads(raw)
186
+ except json.JSONDecodeError:
187
+ body = raw
188
+ return e.code, body
189
+
190
+
191
+ def make_holdings_patch(holdings_map: dict[str, int]) -> dict:
192
+ return {
193
+ "card_data": {
194
+ "holdings": [{"symbol": symbol, "qty": qty} for symbol, qty in holdings_map.items()]
195
+ }
196
+ }
197
+
198
+
199
+ def start_server() -> subprocess.Popen:
200
+ if ARGS.server == "py":
201
+ python = sys.executable or "python"
202
+ cmd = [python, str(PY_SERVER_SCRIPT), "--port", str(PORT), "--reset"]
203
+ else:
204
+ cmd = ["node", str(SERVER_SCRIPT), "--port", str(PORT), "--reset"]
205
+
206
+ proc = subprocess.Popen(
207
+ cmd,
208
+ stdin=subprocess.DEVNULL,
209
+ stdout=subprocess.PIPE,
210
+ stderr=subprocess.PIPE,
211
+ text=True,
212
+ bufsize=1,
213
+ )
214
+
215
+ def _pump_stdout():
216
+ assert proc.stdout is not None
217
+ for line in proc.stdout:
218
+ print(f"[server] {line}", end="")
219
+
220
+ def _pump_stderr():
221
+ assert proc.stderr is not None
222
+ for line in proc.stderr:
223
+ print(f"[server:err] {line}", end="", file=sys.stderr)
224
+
225
+ threading.Thread(target=_pump_stdout, daemon=True).start()
226
+ threading.Thread(target=_pump_stderr, daemon=True).start()
227
+
228
+ deadline = time.monotonic() + 15
229
+ ready = False
230
+ while time.monotonic() < deadline:
231
+ if proc.poll() is not None:
232
+ break
233
+ try:
234
+ with socket.create_connection(("127.0.0.1", PORT), timeout=0.5):
235
+ ready = True
236
+ break
237
+ except OSError:
238
+ time.sleep(0.2)
239
+
240
+ if not ready:
241
+ proc.terminate()
242
+ raise TestFailure("Server startup timeout (15s)")
243
+
244
+ return proc
245
+
246
+
247
+ def start_sse_consumer(stop_event: threading.Event) -> threading.Thread:
248
+ def _run():
249
+ req = urllib.request.Request(f"{BASE}/sse", headers={"Accept": "text/event-stream"})
250
+ try:
251
+ with urllib.request.urlopen(req, timeout=60) as resp:
252
+ block_lines: list[str] = []
253
+ while not stop_event.is_set():
254
+ raw = resp.readline()
255
+ if not raw:
256
+ break
257
+ line = raw.decode("utf-8", errors="replace").rstrip("\r\n")
258
+ if line == "":
259
+ data_lines = [l[6:] for l in block_lines if l.startswith("data: ")]
260
+ block_lines = []
261
+ if not data_lines:
262
+ continue
263
+ data_text = "\n".join(data_lines)
264
+ try:
265
+ payload = json.loads(data_text)
266
+ except json.JSONDecodeError:
267
+ continue
268
+ apply_frame(payload)
269
+ else:
270
+ block_lines.append(line)
271
+ except Exception as err:
272
+ if not stop_event.is_set():
273
+ print(f"[sse] error: {err}", file=sys.stderr)
274
+
275
+ th = threading.Thread(target=_run, daemon=True)
276
+ th.start()
277
+ return th
278
+
279
+
280
+ def run() -> None:
281
+ print("\n=== portfolio-tracker HTTP E2E test (python) ===")
282
+ print(f"target: {BASE} [server: {ARGS.server}]")
283
+ print("architecture: main-thread (driver) + background SSE consumer\n")
284
+
285
+ server_proc = start_server()
286
+ time.sleep(0.3)
287
+
288
+ sse_stop = threading.Event()
289
+ try:
290
+ print("\n=== Step 1: init-board ===")
291
+ status, _ = http_json("GET", "/init-board")
292
+ assert_true(status == 200, f"init-board returned {status}")
293
+ print("[step1] ok")
294
+
295
+ print("\n=== Step 2: Start SSE consumer ===")
296
+ start_sse_consumer(sse_stop)
297
+ initial_payload = wait_for_initial_payload(15.0)
298
+ snap = get_ns_snapshot()
299
+ print(f"[step2] SSE online — initial payload ({len(initial_payload.get('cardDefinitions', []))} cards)")
300
+ print(f" statusGen={snap['statusGeneration']}, dataObjects={json.dumps(sorted(snap['dataObjects'].keys()))}")
301
+
302
+ print("\n=== T1: Wait for initial completion ===")
303
+ t1_summary = wait_for_all_completed(60.0, "T1 initial drain")
304
+ print(f"[T1] board completed — {json.dumps(t1_summary)}")
305
+
306
+ t1_prices = wait_for_price_symbols(["AAPL", "MSFT"], 30.0, "T1 prices")
307
+ assert_true(all(isinstance(v, (int, float)) for v in t1_prices.values()), "T1: all prices must be numbers")
308
+ t1_table = (get_ns_snapshot()["computedValues"].get("holdings-table") or {}).get("table")
309
+ assert_true(isinstance(t1_table, dict) and isinstance(t1_table.get("rows"), list) and len(t1_table["rows"]) == 2,
310
+ f"T1: expected 2 rows, got {len(t1_table.get('rows', [])) if isinstance(t1_table, dict) else 'n/a'}")
311
+ t1_total = (get_ns_snapshot()["computedValues"].get("portfolio-value") or {}).get("totalValue")
312
+ assert_true(isinstance(t1_total, (int, float)) and t1_total > 0, f"T1: totalValue must be positive, got {t1_total}")
313
+ print(f"[T1] passed: prices=[AAPL,MSFT], rows=2, totalValue={float(t1_total):.2f}")
314
+
315
+ print("\n=== T2a: Update holdings — add GOOG ===")
316
+ status, _ = http_json("PATCH", "/cards/portfolio-form", make_holdings_patch({"AAPL": 50, "MSFT": 30, "GOOG": 100}))
317
+ assert_true(status == 200, f"PATCH portfolio-form returned {status}")
318
+ print("[T2a] PATCH ok — consumer will receive SSE notifications")
319
+
320
+ print("\n=== T2b: Wait for 3-ticker completion ===")
321
+ t2_summary = wait_for_all_completed(60.0, "T2b 3-ticker drain")
322
+ print(f"[T2b] completed — {json.dumps(t2_summary)}")
323
+
324
+ wait_for_price_symbols(["AAPL", "GOOG", "MSFT"], 30.0, "T2b prices")
325
+ t2_table = (get_ns_snapshot()["computedValues"].get("holdings-table") or {}).get("table")
326
+ assert_true(isinstance(t2_table, dict) and isinstance(t2_table.get("rows"), list) and len(t2_table["rows"]) == 3,
327
+ f"T2b: expected 3 rows, got {len(t2_table.get('rows', [])) if isinstance(t2_table, dict) else 'n/a'}")
328
+ t2_total = (get_ns_snapshot()["computedValues"].get("portfolio-value") or {}).get("totalValue")
329
+ assert_true(isinstance(t2_total, (int, float)) and t2_total > 0, "T2b: totalValue must be positive")
330
+ print(f"[T2b] passed: prices=[AAPL,GOOG,MSFT], rows=3, totalValue={float(t2_total):.2f}")
331
+
332
+ print("\n=== T3: Rapid 3x holdings updates ===")
333
+ rapid_updates = [
334
+ {"AAPL": 45, "MSFT": 30, "GOOG": 110, "TSLA": 60},
335
+ {"AAPL": 45, "MSFT": 30, "GOOG": 110, "AMZN": 100},
336
+ {"AAPL": 40, "MSFT": 35, "GOOG": 120, "TSLA": 70},
337
+ ]
338
+ for holdings in rapid_updates:
339
+ http_json("PATCH", "/cards/portfolio-form", make_holdings_patch(holdings))
340
+ print("[T3] rapid PATCHes sent — SSE state continues to accumulate")
341
+
342
+ wait_for_all_completed(60.0, "T3 rapid-update drain")
343
+ t3_prices = wait_for_price_symbols(["AAPL", "GOOG", "MSFT", "TSLA"], 30.0, "T3 final prices")
344
+ t3_table = (get_ns_snapshot()["computedValues"].get("holdings-table") or {}).get("table")
345
+ assert_true(isinstance(t3_table, dict) and isinstance(t3_table.get("rows"), list) and len(t3_table["rows"]) == 4,
346
+ f"T3: expected 4 rows, got {len(t3_table.get('rows', [])) if isinstance(t3_table, dict) else 'n/a'}")
347
+ assert_true("AMZN" not in t3_prices, f"T3: AMZN must not be present (got {json.dumps(sorted(t3_prices.keys()))})")
348
+ print(f"[T3] passed: prices={json.dumps(sorted(t3_prices.keys()))}, rows=4, AMZN absent")
349
+
350
+ print("\n=== T4: Cross-verify totalValue ===")
351
+ t4_total = (get_ns_snapshot()["computedValues"].get("portfolio-value") or {}).get("totalValue")
352
+ assert_true(isinstance(t4_total, (int, float)) and t4_total > 0, f"T4: totalValue must be positive, got {t4_total}")
353
+ sum_rows = sum(float(r.get("value", 0)) for r in t3_table["rows"])
354
+ assert_true(abs(sum_rows - float(t4_total)) < 0.01, f"T4: mismatch: sumRows={sum_rows}, totalValue={t4_total}")
355
+ print(f"[T4] passed: totalValue={float(t4_total):.2f}, sumRows={sum_rows:.2f}")
356
+
357
+ print("\n=== T5: board-status HTTP cross-check ===")
358
+ status, t5_body = http_json("GET", "/board-status")
359
+ assert_true(status == 200, f"board-status returned {status}")
360
+ assert_true(isinstance(t5_body, dict), "T5: board-status body is not JSON object")
361
+ t5_summary = ((t5_body.get("statusSnapshot") or {}).get("summary") if isinstance(t5_body, dict) else None)
362
+ assert_true(isinstance(t5_summary, dict), "T5: statusSnapshot.summary missing from board-status")
363
+ assert_true(t5_summary.get("completed") == t5_summary.get("card_count"),
364
+ f"T5: completed={t5_summary.get('completed')} != card_count={t5_summary.get('card_count')}")
365
+ assert_true(t5_summary.get("failed") == 0, f"T5: failed={t5_summary.get('failed')} (expected 0)")
366
+
367
+ http_keys = sorted(((t5_body.get("dataObjectsByToken") or {}).keys()))
368
+ worker_keys = sorted(get_ns_snapshot()["dataObjects"].keys())
369
+ assert_true(http_keys == worker_keys,
370
+ f"T5: HTTP dataObjects keys {http_keys} differ from SSE-accumulated {worker_keys}")
371
+
372
+ print(f"[T5] summary: {json.dumps(t5_summary)}")
373
+ print(f"[T5] HTTP vs SSE dataObjects agree: {json.dumps(worker_keys)}")
374
+ print(f"[T5] statusGen at end: {get_ns_snapshot()['statusGeneration']}")
375
+ print("[T5] all assertions passed")
376
+
377
+ print("\n=== All tests passed ===\n")
378
+
379
+ finally:
380
+ sse_stop.set()
381
+ server_proc.terminate()
382
+ try:
383
+ server_proc.wait(timeout=10)
384
+ except subprocess.TimeoutExpired:
385
+ server_proc.kill()
386
+ server_proc.wait(timeout=5)
387
+ print(f"[portfolio-tracker-http-test.py] server stopped ({ARGS.server})")
388
+
389
+
390
+ if __name__ == "__main__":
391
+ try:
392
+ run()
393
+ except TestFailure as e:
394
+ print(f"\n[ASSERT FAILED] {e}", file=sys.stderr)
395
+ sys.exit(1)
396
+ except KeyboardInterrupt:
397
+ print("\nInterrupted.", file=sys.stderr)
398
+ sys.exit(130)
@@ -12,7 +12,7 @@ steps:
12
12
  handler:
13
13
  type: ref
14
14
  howToRun: local-node
15
- whatToRun: "::fs-path::./handlers/reset-board-dir-cli.js"
15
+ whatToRun: {kind: fs-path, value: ./handlers/reset-board-dir-cli.js}
16
16
  argsMassaging:
17
17
  bodyTemplate: "{ 'BOARD_DIR': runtime_root & '/' & board_name }"
18
18
  transitions:
@@ -27,7 +27,7 @@ steps:
27
27
  handler:
28
28
  type: ref
29
29
  howToRun: local-node
30
- whatToRun: "::fs-path::./handlers/init-board-cli.js"
30
+ whatToRun: {kind: fs-path, value: ./handlers/init-board-cli.js}
31
31
  argsMassaging:
32
32
  bodyTemplate: "{ 'BOARD_DIR': board_dir }"
33
33
  transitions:
@@ -42,7 +42,7 @@ steps:
42
42
  handler:
43
43
  type: ref
44
44
  howToRun: local-node
45
- whatToRun: "::fs-path::./handlers/add-cards-cli.js"
45
+ whatToRun: {kind: fs-path, value: ./handlers/add-cards-cli.js}
46
46
  argsMassaging:
47
47
  bodyTemplate: "{ 'BOARD_DIR': board_dir, 'CARDS': cards }"
48
48
  transitions:
@@ -57,7 +57,7 @@ steps:
57
57
  handler:
58
58
  type: ref
59
59
  howToRun: local-node
60
- whatToRun: "::fs-path::./handlers/poll-status-cli.js"
60
+ whatToRun: {kind: fs-path, value: ./handlers/poll-status-cli.js}
61
61
  argsMassaging:
62
62
  bodyTemplate: "{ 'BOARD_DIR': board_dir, 'EXPECTED_CARD_COUNT': expected_card_count, 'TIMEOUT_MS': '30000', 'POLL_MS': '500' }"
63
63
  transitions:
@@ -25,7 +25,7 @@ steps:
25
25
  handler:
26
26
  type: ref
27
27
  howToRun: local-node
28
- whatToRun: "::fs-path::./jsonata-init-board-cli.js"
28
+ whatToRun: {kind: fs-path, value: ./jsonata-init-board-cli.js}
29
29
  transitions:
30
30
  success: t2_map_output
31
31
  failure: failed_state
@@ -8,7 +8,7 @@ steps:
8
8
  handler:
9
9
  type: ref
10
10
  howToRun: local-node
11
- whatToRun: "::fs-path::./step-cli-echo-y.js"
11
+ whatToRun: {kind: fs-path, value: ./step-cli-echo-y.js}
12
12
  transitions:
13
13
  success: success_state
14
14
  failure: failed_state
@@ -28,7 +28,7 @@ steps:
28
28
  handler:
29
29
  type: ref
30
30
  howToRun: local-node
31
- whatToRun: "::fs-path::./step2-double-cli.js"
31
+ whatToRun: {kind: fs-path, value: ./step2-double-cli.js}
32
32
  transitions:
33
33
  success: success_state
34
34
  failure: failed_state
@@ -706,7 +706,7 @@ Before adding a card to a running board, agents should validate that each source
706
706
 
707
707
  ```bash
708
708
  node board-live-cards-cli.js probe-source \
709
- --card cards/card-market-prices.json \
709
+ --card cards/cardT-market-prices.json \
710
710
  --source-idx 0 \
711
711
  --rg <boardRuntimeDir> \
712
712
  --mock-projections '{"holdings":[{"ticker":"AAPL","quantity":10},{"ticker":"MSFT","quantity":5}]}'
@@ -29,11 +29,11 @@
29
29
  "compute": [
30
30
  {
31
31
  "bindTo": "normalizedQuotes",
32
- "expr": "{ \"quoteResponse\": { \"result\": $map(fetched_sources.quotes, function($r) { ($m := $r.chart.result[0].meta; $prev := $m.chartPreviousClose; $chg := $m.regularMarketPrice - $prev; { \"symbol\": $m.symbol, \"shortName\": $m.shortName ? $m.shortName : $m.longName, \"regularMarketPrice\": $m.regularMarketPrice, \"regularMarketChange\": $chg, \"regularMarketChangePercent\": $chg / $prev * 100 }) }), \"error\": null } }"
32
+ "expr": "{ \"quoteResponse\": { \"result\": $map(fetched_sources.quotes, function($r) { ($m := $r.chart.result[0].meta; $price := $sum($m.regularMarketPrice); $prev := $sum($m.chartPreviousClose); $chg := $price - $prev; { \"symbol\": $join($m.symbol, \"\"), \"shortName\": ($join($m.shortName, \"\") != \"\" ? $join($m.shortName, \"\") : $join($m.longName, \"\")), \"regularMarketPrice\": $price, \"regularMarketChange\": $chg, \"regularMarketChangePercent\": $prev = 0 ? 0 : ($chg / $prev * 100) }) }), \"error\": null } }"
33
33
  },
34
34
  {
35
35
  "bindTo": "prices",
36
- "expr": "$map(computed_values.normalizedQuotes.quoteResponse.result, function($q) { {\"ticker\": $q.symbol, \"name\": $q.shortName, \"price\": $round($q.regularMarketPrice, 2), \"change\": $round($q.regularMarketChange, 2), \"chg_pct\": $round($q.regularMarketChangePercent, 2)} })"
36
+ "expr": "$map((computed_values.normalizedQuotes.quoteResponse.result ? computed_values.normalizedQuotes.quoteResponse.result : []), function($q) { {\"ticker\": $q.symbol, \"name\": $q.shortName, \"price\": $round($q.regularMarketPrice, 2), \"change\": $round($q.regularMarketChange, 2), \"chg_pct\": $round($q.regularMarketChangePercent, 2)} })"
37
37
  }
38
38
  ],
39
39
  "provides": [
@@ -64,24 +64,14 @@
64
64
  "cost_basis": 150
65
65
  },
66
66
  {
67
- "ticker": "MSFT",
67
+ "ticker": "NVDA",
68
68
  "quantity": 5,
69
69
  "cost_basis": 310
70
70
  },
71
71
  {
72
- "ticker": "GOOGL",
73
- "quantity": 2,
74
- "cost_basis": 280
75
- },
76
- {
77
- "ticker": "TSLA",
72
+ "ticker": "GOOG",
78
73
  "quantity": 3,
79
- "cost_basis": 200
80
- },
81
- {
82
- "ticker": "NVDA",
83
- "quantity": 5,
84
- "cost_basis": 310
74
+ "cost_basis": 33
85
75
  }
86
76
  ]
87
77
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "port": 7799,
3
- "serverMetaStoreRef": "::fs-path::./.server-meta",
3
+ "serverMetaStoreRef": "b64:eyJraW5kIjoiZnMtcGF0aCIsInZhbHVlIjoiLi8uc2VydmVyLW1ldGEifQ",
4
4
  "cardsDir": "./cards",
5
5
  "taskExecutorPath": "./demo-task-executor.js",
6
6
  "chatHandlerPath": "./demo-chat-handler.js",