yaml-flow 6.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 (166) hide show
  1. package/board-live-cards-cli.js +4 -4
  2. package/browser/asset-integrity.json +3 -3
  3. package/browser/board-livecards-client.js +2 -0
  4. package/browser/board-livecards-client.js.map +1 -0
  5. package/browser/board-livecards-localstorage.js +10 -0
  6. package/browser/board-livecards-localstorage.js.map +1 -0
  7. package/browser/board-livegraph-engine.js +2 -2
  8. package/browser/board-livegraph-engine.js.map +1 -1
  9. package/browser/card-compute.js +28 -28
  10. package/browser/compute-jsonata.js +5 -0
  11. package/browser/compute-jsonata.js.map +1 -0
  12. package/browser/live-cards.js +264 -151
  13. package/card-store.js +4 -4
  14. package/dist/{board-live-cards-public-CltXYgaY.d.cts → board-live-cards-public-5n1-syA3.d.cts} +8 -5
  15. package/dist/{board-live-cards-public-f-E-FAyp.d.ts → board-live-cards-public-CK_J8uv0.d.ts} +8 -5
  16. package/dist/board-livegraph-runtime/index.cjs +2 -2
  17. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  18. package/dist/board-livegraph-runtime/index.d.cts +11 -9
  19. package/dist/board-livegraph-runtime/index.d.ts +11 -9
  20. package/dist/board-livegraph-runtime/index.js +2 -2
  21. package/dist/board-livegraph-runtime/index.js.map +1 -1
  22. package/dist/board-livegraph-runtime/jsonata-sync.cjs +37 -1
  23. package/dist/card-compute/index.cjs +4 -4
  24. package/dist/card-compute/index.cjs.map +1 -1
  25. package/dist/card-compute/index.d.cts +5 -1
  26. package/dist/card-compute/index.d.ts +5 -1
  27. package/dist/card-compute/index.js +4 -4
  28. package/dist/card-compute/index.js.map +1 -1
  29. package/dist/card-compute/jsonata-sync.cjs +37 -1
  30. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -1
  31. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -1
  32. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +27 -14
  33. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +27 -14
  34. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -1
  35. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -1
  36. package/dist/cli/browser-api/card-store-browser-api.cjs +1 -1
  37. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -1
  38. package/dist/cli/browser-api/card-store-browser-api.js +1 -1
  39. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -1
  40. package/dist/cli/browser-api/jsonata-sync.cjs +37 -1
  41. package/dist/cli/node/artifacts-store-cli.cjs +8 -8
  42. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -1
  43. package/dist/cli/node/artifacts-store-cli.js +8 -8
  44. package/dist/cli/node/artifacts-store-cli.js.map +1 -1
  45. package/dist/cli/node/board-live-cards-cli.cjs +7 -7
  46. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -1
  47. package/dist/cli/node/board-live-cards-cli.js +7 -7
  48. package/dist/cli/node/board-live-cards-cli.js.map +1 -1
  49. package/dist/cli/node/card-store-cli.cjs +5 -5
  50. package/dist/cli/node/card-store-cli.cjs.map +1 -1
  51. package/dist/cli/node/card-store-cli.js +5 -5
  52. package/dist/cli/node/card-store-cli.js.map +1 -1
  53. package/dist/cli/node/execution-adapter.cjs +3 -0
  54. package/dist/cli/node/execution-adapter.cjs.map +1 -0
  55. package/dist/cli/node/execution-adapter.d.cts +174 -0
  56. package/dist/cli/node/execution-adapter.d.ts +174 -0
  57. package/dist/cli/node/execution-adapter.js +3 -0
  58. package/dist/cli/node/execution-adapter.js.map +1 -0
  59. package/dist/cli/node/fs-board-adapter.cjs +7 -7
  60. package/dist/cli/node/fs-board-adapter.cjs.map +1 -1
  61. package/dist/cli/node/fs-board-adapter.d.cts +2 -2
  62. package/dist/cli/node/fs-board-adapter.d.ts +2 -2
  63. package/dist/cli/node/fs-board-adapter.js +7 -7
  64. package/dist/cli/node/fs-board-adapter.js.map +1 -1
  65. package/dist/cli/node/jsonata-sync.cjs +37 -1
  66. package/dist/cli/node/source-cli-task-executor.cjs +4 -4
  67. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -1
  68. package/dist/cli/node/source-cli-task-executor.js +4 -4
  69. package/dist/cli/node/source-cli-task-executor.js.map +1 -1
  70. package/dist/continuous-event-graph/index.cjs +2 -2
  71. package/dist/continuous-event-graph/index.cjs.map +1 -1
  72. package/dist/continuous-event-graph/index.js +2 -2
  73. package/dist/continuous-event-graph/index.js.map +1 -1
  74. package/dist/continuous-event-graph/jsonata-sync.cjs +37 -1
  75. package/dist/execution-refs.cjs +2 -1
  76. package/dist/execution-refs.cjs.map +1 -1
  77. package/dist/execution-refs.d.cts +55 -12
  78. package/dist/execution-refs.d.ts +55 -12
  79. package/dist/execution-refs.js +2 -1
  80. package/dist/execution-refs.js.map +1 -1
  81. package/dist/index.cjs +10 -10
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.js +10 -10
  84. package/dist/index.js.map +1 -1
  85. package/dist/jsonata-sync.cjs +37 -1
  86. package/dist/server-runtime/index.cjs +9 -0
  87. package/dist/server-runtime/index.cjs.map +1 -0
  88. package/dist/server-runtime/index.d.cts +31 -0
  89. package/dist/server-runtime/index.d.ts +31 -0
  90. package/dist/server-runtime/index.js +9 -0
  91. package/dist/server-runtime/index.js.map +1 -0
  92. package/dist/server-runtime/jsonata-sync.cjs +7623 -0
  93. package/dist/step-machine-public/index.cjs +3 -0
  94. package/dist/step-machine-public/index.cjs.map +1 -0
  95. package/dist/step-machine-public/index.d.cts +166 -0
  96. package/dist/step-machine-public/index.d.ts +166 -0
  97. package/dist/step-machine-public/index.js +3 -0
  98. package/dist/step-machine-public/index.js.map +1 -0
  99. package/dist/step-machine-public/jsonata-sync.cjs +7623 -0
  100. package/dist/storage-refs.cjs +2 -2
  101. package/dist/storage-refs.cjs.map +1 -1
  102. package/dist/storage-refs.d.cts +6 -6
  103. package/dist/storage-refs.d.ts +6 -6
  104. package/dist/storage-refs.js +2 -2
  105. package/dist/storage-refs.js.map +1 -1
  106. package/dist/types-CU3DjTKL.d.cts +147 -0
  107. package/dist/types-HGDTWIun.d.ts +147 -0
  108. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +9 -10
  109. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +370 -0
  110. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.py +398 -0
  111. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +9 -10
  112. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
  113. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
  114. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
  115. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +11 -10
  116. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +19 -4
  117. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +4 -8
  118. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -10
  119. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +8 -16
  120. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
  121. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
  122. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
  123. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +4 -8
  124. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +7 -16
  125. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
  126. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +13 -3
  127. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +2 -1
  128. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +2 -1
  129. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +2 -1
  130. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +20 -24
  131. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +0 -3
  132. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
  133. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
  134. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
  135. package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
  136. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
  137. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
  138. package/examples/example-board/agent-instructions.md +1 -1
  139. package/examples/example-board/cards/card-my-identity.json +30 -6
  140. package/examples/example-board/cards/card-portfolio-action.json +24 -6
  141. package/examples/example-board/cards/card-portfolio-intelligence.json +97 -0
  142. package/examples/example-board/cards/card-portfolio-risks.json +24 -6
  143. package/examples/example-board/cards/card-rebalance-impact.json +22 -6
  144. package/examples/example-board/cards/card-rebalance-sim.json +66 -15
  145. package/examples/example-board/cards/cardT-market-prices.json +80 -0
  146. package/examples/example-board/cards/{card-portfolio-value.json → cardT-portfolio-value.json} +38 -10
  147. package/examples/example-board/cards/cardT-portfolio.json +78 -0
  148. package/examples/example-board/demo-server-config.json +1 -1
  149. package/examples/example-board/demo-server.js +383 -69
  150. package/examples/example-board/demo-shell-localstorage.html +774 -0
  151. package/examples/example-board/demo-shell-with-server.html +18 -36
  152. package/examples/example-board/demo-shell.html +5 -4
  153. package/examples/example-board/demo-task-executor.js +213 -265
  154. package/package.json +15 -13
  155. package/step-machine-cli.js +43 -310
  156. package/board-livecards-server-runtime.js +0 -1513
  157. package/browser/board-livecards-runtime-client.js +0 -263
  158. package/dist/pycli/quickjs-board-runtime.global.js +0 -9
  159. package/dist/pycli/quickjs-board-runtime.global.js.map +0 -1
  160. package/dist/pycli/quickjs-step-machine-runtime.global.js +0 -5
  161. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +0 -1
  162. package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
  163. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
  164. package/examples/example-board/cards/card-market-prices.json +0 -56
  165. package/examples/example-board/cards/card-portfolio.json +0 -44
  166. package/examples/example-board/demo-shell-browser.html +0 -675
@@ -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)
@@ -31,6 +31,7 @@ const {
31
31
  createCardStorePublic,
32
32
  createCardStore,
33
33
  parseRef,
34
+ serializeRef,
34
35
  } = await import(pathToFileURL(_adapterPath).href);
35
36
 
36
37
  const FETCH_PRICES_JS = path.join(__dirname, 'portfolio-tracker-fetch-prices.js');
@@ -41,9 +42,9 @@ const CARDSTORE_DIR = path.join(_TMP_BASE, 'cardstore');
41
42
  const BOARDRUNTIME_DIR = path.join(_TMP_BASE, 'boardruntime');
42
43
  const OUTPUTS_DIR = path.join(_TMP_BASE, 'outputs');
43
44
 
44
- const CARDSTORE_REF = `::fs-path::${CARDSTORE_DIR}`;
45
- const BOARDRUNTIME_REF = `::fs-path::${BOARDRUNTIME_DIR}`;
46
- const OUTPUTS_REF = `::fs-path::${OUTPUTS_DIR}`;
45
+ const CARDSTORE_REF = serializeRef({ kind: 'fs-path', value: CARDSTORE_DIR });
46
+ const BOARDRUNTIME_REF = serializeRef({ kind: 'fs-path', value: BOARDRUNTIME_DIR });
47
+ const OUTPUTS_REF = serializeRef({ kind: 'fs-path', value: OUTPUTS_DIR });
47
48
  const NOTIFY_CHANNEL = 'yaml-flow-board-notify-portfolio-tracker-public';
48
49
 
49
50
  // ── Card definitions ───────────────────────────────────────────────────────────
@@ -64,18 +65,16 @@ const CARD_PRICE_FETCH = {
64
65
  id: 'price-fetch',
65
66
  meta: { title: 'Fetch Market Prices' },
66
67
  requires: ['holdings'],
67
- provides: [{ bindTo: 'prices', ref: 'fetched_sources.prices' }],
68
+ provides: [{ bindTo: 'prices', ref: 'computed_values.prices' }],
68
69
  card_data: {},
69
- source_defs: [{
70
- kind: 'mock-quotes',
70
+ compute: [{
71
71
  bindTo: 'prices',
72
- outputFile: 'prices.json',
73
- projections: { tickers: '$append([], requires.holdings.symbol)' }
72
+ expr: '$merge($map(requires.holdings, function($h){ { $h.symbol: 100 } }))'
74
73
  }],
75
74
  view: {
76
75
  elements: [
77
76
  { kind: 'table', label: 'Market Prices',
78
- data: { bind: 'fetched_sources.prices' } }
77
+ data: { bind: 'computed_values.prices' } }
79
78
  ]
80
79
  }
81
80
  };
@@ -408,7 +407,7 @@ checkResult(
408
407
  'task-executor-ref': {
409
408
  meta: 'task-executor',
410
409
  howToRun: 'local-node',
411
- whatToRun: `::fs-path::${FETCH_PRICES_JS}`,
410
+ whatToRun: serializeRef({ kind: 'fs-path', value: FETCH_PRICES_JS }),
412
411
  },
413
412
  },
414
413
  }),