yaml-flow 5.4.2 → 7.0.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.
- package/board-live-cards-cli.js +6 -6
- package/browser/asset-integrity.json +10 -0
- package/browser/board-livecards-client.js +2 -0
- package/browser/board-livecards-client.js.map +1 -0
- package/browser/board-livecards-localstorage.js +10 -0
- package/browser/board-livecards-localstorage.js.map +1 -0
- package/browser/board-livegraph-engine.js +2 -1676
- package/browser/board-livegraph-engine.js.map +1 -1
- package/browser/card-compute.js +28 -28
- package/browser/compute-jsonata.js +5 -0
- package/browser/compute-jsonata.js.map +1 -0
- package/browser/live-cards.js +561 -129
- package/browser/live-cards.schema.json +418 -132
- package/card-store.js +37 -0
- package/dist/batch/index.cjs +1 -108
- package/dist/batch/index.cjs.map +1 -1
- package/dist/batch/index.js +1 -106
- package/dist/batch/index.js.map +1 -1
- package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
- package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
- package/dist/board-live-cards-public-CW5074xr.d.cts +318 -0
- package/dist/board-live-cards-public-hnZo0mAf.d.ts +318 -0
- package/dist/board-livegraph-runtime/index.cjs +2 -1671
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +12 -11
- package/dist/board-livegraph-runtime/index.d.ts +12 -11
- package/dist/board-livegraph-runtime/index.js +2 -1662
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/board-livegraph-runtime/jsonata-sync.cjs +7623 -0
- package/dist/card-compute/index.cjs +9 -7159
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +27 -1
- package/dist/card-compute/index.d.ts +27 -1
- package/dist/card-compute/index.js +9 -7145
- package/dist/card-compute/index.js.map +1 -1
- package/dist/card-compute/jsonata-sync.cjs +7623 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +3 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +37 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +37 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js +3 -0
- package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
- package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
- package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
- package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
- package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
- package/dist/cli/browser-api/card-store-browser-api.js +2 -0
- package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
- package/dist/cli/browser-api/jsonata-sync.cjs +7623 -0
- package/dist/cli/node/artifacts-store-cli.cjs +11 -0
- package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
- package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
- package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
- package/dist/cli/node/artifacts-store-cli.js +11 -0
- package/dist/cli/node/artifacts-store-cli.js.map +1 -0
- package/dist/cli/node/board-live-cards-cli.cjs +15 -0
- package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
- package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
- package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
- package/dist/cli/node/board-live-cards-cli.js +15 -0
- package/dist/cli/node/board-live-cards-cli.js.map +1 -0
- package/dist/cli/node/card-store-cli.cjs +8 -0
- package/dist/cli/node/card-store-cli.cjs.map +1 -0
- package/dist/cli/node/card-store-cli.d.cts +15 -0
- package/dist/cli/node/card-store-cli.d.ts +15 -0
- package/dist/cli/node/card-store-cli.js +8 -0
- package/dist/cli/node/card-store-cli.js.map +1 -0
- package/dist/cli/node/execution-adapter.cjs +3 -0
- package/dist/cli/node/execution-adapter.cjs.map +1 -0
- package/dist/cli/node/execution-adapter.d.cts +174 -0
- package/dist/cli/node/execution-adapter.d.ts +174 -0
- package/dist/cli/node/execution-adapter.js +3 -0
- package/dist/cli/node/execution-adapter.js.map +1 -0
- package/dist/cli/node/fs-board-adapter.cjs +14 -0
- package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
- package/dist/cli/node/fs-board-adapter.d.cts +204 -0
- package/dist/cli/node/fs-board-adapter.d.ts +204 -0
- package/dist/cli/node/fs-board-adapter.js +14 -0
- package/dist/cli/node/fs-board-adapter.js.map +1 -0
- package/dist/cli/node/jsonata-sync.cjs +7623 -0
- package/dist/cli/node/source-cli-task-executor.cjs +11 -0
- package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
- package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
- package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
- package/dist/cli/node/source-cli-task-executor.js +11 -0
- package/dist/cli/node/source-cli-task-executor.js.map +1 -0
- package/dist/config/index.cjs +1 -79
- package/dist/config/index.cjs.map +1 -1
- package/dist/config/index.js +1 -76
- package/dist/config/index.js.map +1 -1
- package/dist/continuous-event-graph/index.cjs +2 -2129
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +81 -5
- package/dist/continuous-event-graph/index.d.ts +81 -5
- package/dist/continuous-event-graph/index.js +2 -2088
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/continuous-event-graph/jsonata-sync.cjs +7623 -0
- package/dist/event-graph/index.cjs +22 -8292
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.js +22 -8237
- package/dist/event-graph/index.js.map +1 -1
- package/dist/execution-refs.cjs +3 -0
- package/dist/execution-refs.cjs.map +1 -0
- package/dist/execution-refs.d.cts +260 -0
- package/dist/execution-refs.d.ts +260 -0
- package/dist/execution-refs.js +3 -0
- package/dist/execution-refs.js.map +1 -0
- package/dist/index.cjs +29 -13221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +29 -13112
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +5 -617
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.js +5 -610
- package/dist/inference/index.js.map +1 -1
- package/dist/jsonata-sync.cjs +7623 -0
- package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
- package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
- package/dist/server-runtime/index.cjs +9 -0
- package/dist/server-runtime/index.cjs.map +1 -0
- package/dist/server-runtime/index.d.cts +31 -0
- package/dist/server-runtime/index.d.ts +31 -0
- package/dist/server-runtime/index.js +9 -0
- package/dist/server-runtime/index.js.map +1 -0
- package/dist/server-runtime/jsonata-sync.cjs +7623 -0
- package/dist/step-machine/index.cjs +11 -7129
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.js +11 -7113
- package/dist/step-machine/index.js.map +1 -1
- package/dist/step-machine-public/index.cjs +2 -0
- package/dist/step-machine-public/index.cjs.map +1 -0
- package/dist/step-machine-public/index.d.cts +159 -0
- package/dist/step-machine-public/index.d.ts +159 -0
- package/dist/step-machine-public/index.js +2 -0
- package/dist/step-machine-public/index.js.map +1 -0
- package/dist/step-machine-public/jsonata-sync.cjs +7623 -0
- package/dist/storage-refs.cjs +10 -0
- package/dist/storage-refs.cjs.map +1 -0
- package/dist/storage-refs.d.cts +93 -0
- package/dist/storage-refs.d.ts +93 -0
- package/dist/storage-refs.js +10 -0
- package/dist/storage-refs.js.map +1 -0
- package/dist/stores/file.cjs +1 -114
- package/dist/stores/file.cjs.map +1 -1
- package/dist/stores/file.js +1 -112
- package/dist/stores/file.js.map +1 -1
- package/dist/stores/index.cjs +1 -231
- package/dist/stores/index.cjs.map +1 -1
- package/dist/stores/index.js +1 -227
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/localStorage.cjs +1 -76
- package/dist/stores/localStorage.cjs.map +1 -1
- package/dist/stores/localStorage.js +1 -74
- package/dist/stores/localStorage.js.map +1 -1
- package/dist/stores/memory.cjs +1 -47
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.js +1 -45
- package/dist/stores/memory.js.map +1 -1
- package/dist/types-B1ZRa4aI.d.ts +147 -0
- package/dist/types-BxEFcVK9.d.cts +147 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +291 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +357 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +552 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +366 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +70 -3
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +16 -11
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +9 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +49 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +9 -8
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +12 -17
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +107 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +51 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +45 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +71 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +36 -187
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
- package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +43 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
- package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
- package/examples/example-board/agent-instructions.md +11 -5
- package/examples/example-board/cards/_index.json +47 -0
- package/examples/example-board/cards/card-market-prices.json +33 -9
- package/examples/example-board/cards/card-my-identity.json +30 -6
- package/examples/example-board/cards/card-portfolio-action.json +24 -6
- package/examples/example-board/cards/card-portfolio-intelligence.json +97 -0
- package/examples/example-board/cards/card-portfolio-risks.json +24 -6
- package/examples/example-board/cards/card-portfolio-value.json +38 -10
- package/examples/example-board/cards/card-portfolio.json +57 -13
- package/examples/example-board/cards/card-rebalance-impact.json +22 -6
- package/examples/example-board/cards/card-rebalance-sim.json +66 -15
- package/examples/example-board/demo-chat-handler.js +14 -4
- package/examples/example-board/demo-server-config.json +1 -0
- package/examples/example-board/demo-server.js +366 -68
- package/examples/example-board/demo-shell-localstorage.html +774 -0
- package/examples/example-board/demo-shell-with-server.html +20 -37
- package/examples/example-board/demo-shell.html +5 -4
- package/examples/example-board/demo-task-executor.js +273 -275
- package/examples/index.html +0 -14
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
- package/package.json +46 -8
- package/schema/live-cards.schema.json +418 -132
- package/step-machine-cli.js +43 -310
- package/board-livecards-server-runtime.js +0 -1574
- package/browser/board-livecards-runtime-client.js +0 -263
- package/dist/cli/board-live-cards-cli.cjs +0 -10650
- package/dist/cli/board-live-cards-cli.cjs.map +0 -1
- package/dist/cli/board-live-cards-cli.d.cts +0 -179
- package/dist/cli/board-live-cards-cli.d.ts +0 -179
- package/dist/cli/board-live-cards-cli.js +0 -10598
- package/dist/cli/board-live-cards-cli.js.map +0 -1
- package/dist/journal-9HEgs7dU.d.ts +0 -28
- package/dist/journal-B-JCfQnh.d.cts +0 -28
- package/dist/schedule-Cszq9LYY.d.ts +0 -21
- package/dist/schedule-qWNL0RQh.d.cts +0 -21
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
- package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
- package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +0 -351
- package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
- package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
- package/examples/example-board/demo-shell-browser.html +0 -674
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""portfolio-tracker-fetch-prices.py
|
|
3
|
+
|
|
4
|
+
Task executor for the portfolio board demo.
|
|
5
|
+
Handles run-source-fetch requests for source_defs with kind: "mock-quotes".
|
|
6
|
+
Generates random prices (2dp, 10.00-999.99) for each projected ticker.
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
run-source-fetch --in-ref <::kind::value> --out-ref <::kind::value> --err-ref <::kind::value>
|
|
10
|
+
validate-source-def --in <source.json>
|
|
11
|
+
describe-capabilities
|
|
12
|
+
|
|
13
|
+
Uses the public storage adapter for all storage and callback operations.
|
|
14
|
+
The executor does NOT contain transport-specific callback logic.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import random
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# Add pycli to path so we can import the public storage adapter.
|
|
28
|
+
_REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', '..'))
|
|
29
|
+
if _REPO_ROOT not in sys.path:
|
|
30
|
+
sys.path.insert(0, _REPO_ROOT)
|
|
31
|
+
|
|
32
|
+
from pycli.sub.public_storage_adapter import ( # noqa: E402
|
|
33
|
+
parse_ref,
|
|
34
|
+
serialize_ref,
|
|
35
|
+
blob_storage_for_ref,
|
|
36
|
+
report_complete,
|
|
37
|
+
report_failed,
|
|
38
|
+
KindValueRef,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_ref_str(ref: str) -> KindValueRef:
|
|
43
|
+
"""Convenience: parse a CLI ref string."""
|
|
44
|
+
return parse_ref(ref)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_source_def(source_def: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
errors: list[str] = []
|
|
49
|
+
|
|
50
|
+
if source_def.get("kind") != "mock-quotes":
|
|
51
|
+
errors.append(f"kind must be \"mock-quotes\"; got \"{source_def.get('kind')}\".")
|
|
52
|
+
if not isinstance(source_def.get("bindTo"), str) or not source_def.get("bindTo"):
|
|
53
|
+
errors.append("bindTo is required and must be a string.")
|
|
54
|
+
if not isinstance(source_def.get("outputFile"), str) or not source_def.get("outputFile"):
|
|
55
|
+
errors.append("outputFile is required and must be a string.")
|
|
56
|
+
projections = source_def.get("projections")
|
|
57
|
+
if not isinstance(projections, dict) or not isinstance(projections.get("tickers"), str):
|
|
58
|
+
errors.append("projections.tickers is required and must be a JSONata expression string.")
|
|
59
|
+
|
|
60
|
+
return {"ok": len(errors) == 0, "errors": errors}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_validate_source_def(args: argparse.Namespace) -> int:
|
|
64
|
+
if not os.path.exists(args.input):
|
|
65
|
+
print(json.dumps({"ok": False, "errors": [f"Input file not found: {args.input}"]}))
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(args.input, "r", encoding="utf-8") as f:
|
|
70
|
+
source_def = json.load(f)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(json.dumps({"ok": False, "errors": [f"Cannot parse source file: {e}"]}))
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
result = validate_source_def(source_def if isinstance(source_def, dict) else {})
|
|
76
|
+
print(json.dumps(result))
|
|
77
|
+
return 0 if result["ok"] else 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_describe_capabilities(_: argparse.Namespace) -> int:
|
|
81
|
+
capabilities = {
|
|
82
|
+
"version": "1.0",
|
|
83
|
+
"executor": "portfolio-tracker-fetch-prices",
|
|
84
|
+
"subcommands": ["run-source-fetch", "validate-source-def", "describe-capabilities"],
|
|
85
|
+
"sourceKinds": {
|
|
86
|
+
"mock-quotes": {
|
|
87
|
+
"description": "Generates random mock market prices (10.00-999.99) for each ticker in _projections.tickers.",
|
|
88
|
+
"inputSchema": {
|
|
89
|
+
"kind": {"type": "string", "required": True, "description": "Must be \"mock-quotes\"."},
|
|
90
|
+
"bindTo": {"type": "string", "required": True, "description": "Token name for the output binding."},
|
|
91
|
+
"outputFile": {"type": "string", "required": True, "description": "Relative path to write prices JSON."},
|
|
92
|
+
"projections": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": True,
|
|
95
|
+
"properties": {
|
|
96
|
+
"tickers": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"required": True,
|
|
99
|
+
"description": "JSONata expression resolving to a string[] of ticker symbols.",
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
"outputShape": "{ [ticker: string]: number }",
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
print(json.dumps(capabilities, indent=2, ensure_ascii=True))
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def cmd_run_source_fetch(args: argparse.Namespace) -> int:
|
|
113
|
+
in_ref = _parse_ref_str(args.in_ref)
|
|
114
|
+
out_ref = _parse_ref_str(args.out_ref)
|
|
115
|
+
err_ref = _parse_ref_str(args.err_ref)
|
|
116
|
+
|
|
117
|
+
in_storage = blob_storage_for_ref(in_ref)
|
|
118
|
+
out_storage = blob_storage_for_ref(out_ref)
|
|
119
|
+
err_storage = blob_storage_for_ref(err_ref)
|
|
120
|
+
|
|
121
|
+
raw_in = in_storage.read(in_ref.value)
|
|
122
|
+
if not raw_in:
|
|
123
|
+
print(f"[portfolio-tracker-fetch-prices] input envelope not found at: {args.in_ref}", file=sys.stderr)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
envelope = json.loads(raw_in)
|
|
127
|
+
callback = envelope.get("callback") if isinstance(envelope, dict) else None
|
|
128
|
+
|
|
129
|
+
def safe_fail(msg: str) -> int:
|
|
130
|
+
try:
|
|
131
|
+
err_storage.write(err_ref.value, msg)
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
if isinstance(callback, dict):
|
|
135
|
+
try:
|
|
136
|
+
report_failed(callback, msg)
|
|
137
|
+
return 0
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"[portfolio-tracker-fetch-prices] callback fail: {e}", file=sys.stderr)
|
|
140
|
+
return 1
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
source_def = envelope.get("source_def") if isinstance(envelope, dict) else None
|
|
145
|
+
if not isinstance(source_def, dict):
|
|
146
|
+
source_def = envelope if isinstance(envelope, dict) else {}
|
|
147
|
+
|
|
148
|
+
if source_def.get("kind") != "mock-quotes":
|
|
149
|
+
raise ValueError(f"Unsupported source kind: expected \"mock-quotes\", got \"{source_def.get('kind')}\"")
|
|
150
|
+
|
|
151
|
+
projections = source_def.get("_projections")
|
|
152
|
+
tickers = projections.get("tickers") if isinstance(projections, dict) else None
|
|
153
|
+
if not isinstance(tickers, list):
|
|
154
|
+
raise ValueError("sourceDef._projections.tickers is missing or not an array")
|
|
155
|
+
|
|
156
|
+
time.sleep(0.2 + random.random() * 0.1)
|
|
157
|
+
|
|
158
|
+
prices: dict[str, float] = {}
|
|
159
|
+
for ticker in tickers:
|
|
160
|
+
prices[str(ticker)] = round(10 + random.random() * 989.99, 2)
|
|
161
|
+
|
|
162
|
+
out_storage.write(out_ref.value, json.dumps(prices, ensure_ascii=True))
|
|
163
|
+
print(f"[portfolio-tracker-fetch-prices] wrote prices for: {', '.join([str(t) for t in tickers])}")
|
|
164
|
+
|
|
165
|
+
if isinstance(callback, dict):
|
|
166
|
+
report_complete(callback, out_ref)
|
|
167
|
+
return 0
|
|
168
|
+
except Exception as e:
|
|
169
|
+
msg = str(e)
|
|
170
|
+
print(f"[portfolio-tracker-fetch-prices] error: {msg}", file=sys.stderr)
|
|
171
|
+
return safe_fail(msg)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
175
|
+
parser = argparse.ArgumentParser(prog="portfolio-tracker-fetch-prices")
|
|
176
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
177
|
+
|
|
178
|
+
run_cmd = sub.add_parser("run-source-fetch")
|
|
179
|
+
run_cmd.add_argument("--in-ref", required=True)
|
|
180
|
+
run_cmd.add_argument("--out-ref", required=True)
|
|
181
|
+
run_cmd.add_argument("--err-ref", required=True)
|
|
182
|
+
run_cmd.set_defaults(handler=cmd_run_source_fetch)
|
|
183
|
+
|
|
184
|
+
val_cmd = sub.add_parser("validate-source-def")
|
|
185
|
+
val_cmd.add_argument("--in", dest="input", required=True)
|
|
186
|
+
val_cmd.set_defaults(handler=cmd_validate_source_def)
|
|
187
|
+
|
|
188
|
+
cap_cmd = sub.add_parser("describe-capabilities")
|
|
189
|
+
cap_cmd.set_defaults(handler=cmd_describe_capabilities)
|
|
190
|
+
|
|
191
|
+
return parser
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def main(argv: list[str] | None = None) -> int:
|
|
195
|
+
parser = build_parser()
|
|
196
|
+
args = parser.parse_args(argv)
|
|
197
|
+
return args.handler(args)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* portfolio-tracker-http-test.js
|
|
4
|
+
*
|
|
5
|
+
* E2E test for the portfolio-tracker board via HTTP + SSE.
|
|
6
|
+
*
|
|
7
|
+
* Two parallel tracks:
|
|
8
|
+
*
|
|
9
|
+
* Worker thread (portfolio-tracker-sse-worker.js) — SSE consumer
|
|
10
|
+
* Opens the board's /sse endpoint, parses every frame, and forwards it
|
|
11
|
+
* to the main thread via parentPort.postMessage({ type: 'frame', payload }).
|
|
12
|
+
*
|
|
13
|
+
* Main thread (this file) — Test driver
|
|
14
|
+
* Accumulates state from worker messages into NotificationState (NS).
|
|
15
|
+
* Drives sequential test steps (T1–T5) via HTTP PATCH/GET.
|
|
16
|
+
* All "wait for X" helpers poll NS with setInterval — no callbacks needed.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node portfolio-tracker-http-test.js [--port 7800] [--server node|py]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { Worker } from 'node:worker_threads';
|
|
23
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = path.dirname(__filename);
|
|
30
|
+
|
|
31
|
+
const cliArgs = process.argv.slice(2);
|
|
32
|
+
const portArg = cliArgs.indexOf('--port');
|
|
33
|
+
const serverArg = cliArgs.indexOf('--server');
|
|
34
|
+
const SERVER_TYPE = serverArg !== -1 ? cliArgs[serverArg + 1] : 'node'; // 'node' | 'py'
|
|
35
|
+
const PORT = portArg !== -1 ? parseInt(cliArgs[portArg + 1], 10) : (SERVER_TYPE === 'py' ? 7801 : 7800);
|
|
36
|
+
const BASE = `http://127.0.0.1:${PORT}/api/board`;
|
|
37
|
+
const SERVER_SCRIPT = path.join(__dirname, 'portfolio-tracker-server.js');
|
|
38
|
+
const PY_SERVER_SCRIPT = path.join(__dirname, 'portfolio-tracker-server.py');
|
|
39
|
+
const SSE_WORKER_SCRIPT = path.join(__dirname, 'portfolio-tracker-sse-worker.js');
|
|
40
|
+
|
|
41
|
+
/** Find a working Python interpreter. Returns null if none found. */
|
|
42
|
+
function findPython() {
|
|
43
|
+
const candidates = ['python3', 'python'];
|
|
44
|
+
for (const cmd of candidates) {
|
|
45
|
+
try {
|
|
46
|
+
const r = spawnSync(cmd, ['--version'], { stdio: 'pipe', timeout: 3000 });
|
|
47
|
+
if (r.status === 0 && r.stdout?.toString().startsWith('Python ')) return cmd;
|
|
48
|
+
} catch { /* next */ }
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// NOTIFICATION STATE — accumulated by the main thread from worker SSE frames
|
|
55
|
+
// =============================================================================
|
|
56
|
+
const NS = {
|
|
57
|
+
initialPayload: null, // first full snapshot frame
|
|
58
|
+
statusSummary: null, // latest { card_count, completed, failed, ... }
|
|
59
|
+
statusGeneration: 0, // bumped on every status notification received
|
|
60
|
+
dataObjects: {}, // token → payload (e.g. 'prices' → { AAPL: 142.5, ... })
|
|
61
|
+
computedValues: {}, // cardId → values (e.g. 'holdings-table' → { table: { rows: [...] } })
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Apply a parsed SSE frame into NS (called from worker message handler)
|
|
65
|
+
function applyFrame(payload) {
|
|
66
|
+
// Initial full snapshot — has cardDefinitions
|
|
67
|
+
if (payload.cardDefinitions) {
|
|
68
|
+
NS.initialPayload = payload;
|
|
69
|
+
if (payload.statusSnapshot?.summary) {
|
|
70
|
+
NS.statusSummary = payload.statusSnapshot.summary;
|
|
71
|
+
NS.statusGeneration++;
|
|
72
|
+
}
|
|
73
|
+
if (payload.dataObjectsByToken) {
|
|
74
|
+
Object.assign(NS.dataObjects, payload.dataObjectsByToken);
|
|
75
|
+
}
|
|
76
|
+
if (payload.cardRuntimeById) {
|
|
77
|
+
for (const [cardId, runtime] of Object.entries(payload.cardRuntimeById)) {
|
|
78
|
+
if (runtime?.computed_values && Object.keys(runtime.computed_values).length > 0) {
|
|
79
|
+
NS.computedValues[cardId] = runtime.computed_values;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Subsequent frames — notification-batch
|
|
86
|
+
if (payload.kind === 'notification-batch' && Array.isArray(payload.notifications)) {
|
|
87
|
+
for (const n of payload.notifications) {
|
|
88
|
+
if (n.kind === 'status' && n.status?.summary) {
|
|
89
|
+
NS.statusSummary = n.status.summary;
|
|
90
|
+
NS.statusGeneration++;
|
|
91
|
+
} else if (n.kind === 'data_object' && n.key) {
|
|
92
|
+
NS.dataObjects[n.key] = n.payload;
|
|
93
|
+
} else if (n.kind === 'computed_values' && n.cardId) {
|
|
94
|
+
NS.computedValues[n.cardId] = n.values;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Polling helpers (poll NS, never block the event loop) ───────────────────
|
|
101
|
+
|
|
102
|
+
function assert(condition, message) {
|
|
103
|
+
if (!condition) {
|
|
104
|
+
console.error(`\n[ASSERT FAILED] ${message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function waitUntil(predicate, timeoutMs, label) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const deadline = Date.now() + timeoutMs;
|
|
112
|
+
const interval = setInterval(() => {
|
|
113
|
+
let result;
|
|
114
|
+
try { result = predicate(); } catch { /* retry */ }
|
|
115
|
+
if (result !== undefined && result !== null && result !== false) {
|
|
116
|
+
clearInterval(interval);
|
|
117
|
+
resolve(result);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (Date.now() > deadline) {
|
|
121
|
+
clearInterval(interval);
|
|
122
|
+
reject(new Error(`Timeout (${timeoutMs}ms) waiting for: ${label}\n NS.statusSummary=${JSON.stringify(NS.statusSummary)}\n dataObjects=${JSON.stringify(Object.keys(NS.dataObjects))}`));
|
|
123
|
+
}
|
|
124
|
+
}, 150);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Waits for first full payload frame from SSE worker
|
|
129
|
+
const waitForInitialPayload = (ms = 15_000) =>
|
|
130
|
+
waitUntil(() => NS.initialPayload || false, ms, 'initial SSE payload');
|
|
131
|
+
|
|
132
|
+
// Waits for all cards to reach completed status
|
|
133
|
+
const waitForAllCompleted = (ms = 60_000, label = 'all completed') =>
|
|
134
|
+
waitUntil(() => {
|
|
135
|
+
const s = NS.statusSummary;
|
|
136
|
+
return (s && s.card_count > 0 && s.completed === s.card_count) ? s : false;
|
|
137
|
+
}, ms, label);
|
|
138
|
+
|
|
139
|
+
// Waits until prices data object has exactly the expected set of symbols
|
|
140
|
+
function waitForPriceSymbols(expectedSymbols, ms = 30_000, label = 'price symbols') {
|
|
141
|
+
const expected = [...expectedSymbols].sort().join(',');
|
|
142
|
+
return waitUntil(() => {
|
|
143
|
+
const prices = NS.dataObjects['prices'];
|
|
144
|
+
if (!prices || typeof prices !== 'object') return false;
|
|
145
|
+
const actual = Object.keys(prices).sort().join(',');
|
|
146
|
+
return actual === expected ? prices : false;
|
|
147
|
+
}, ms, `${label}: expected [${expected}]`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function httpGet(url) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
http.get(url, (res) => {
|
|
155
|
+
let body = '';
|
|
156
|
+
res.on('data', c => { body += c; });
|
|
157
|
+
res.on('end', () => {
|
|
158
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
|
|
159
|
+
catch { resolve({ status: res.statusCode, data: body }); }
|
|
160
|
+
});
|
|
161
|
+
}).on('error', reject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function httpPatch(url, payload) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const body = JSON.stringify(payload);
|
|
168
|
+
const req = http.request(url, {
|
|
169
|
+
method: 'PATCH',
|
|
170
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
171
|
+
}, (res) => {
|
|
172
|
+
let data = '';
|
|
173
|
+
res.on('data', c => { data += c; });
|
|
174
|
+
res.on('end', () => {
|
|
175
|
+
try { resolve({ status: res.statusCode, data: JSON.parse(data) }); }
|
|
176
|
+
catch { resolve({ status: res.statusCode, data }); }
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
req.on('error', reject);
|
|
180
|
+
req.write(body);
|
|
181
|
+
req.end();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function makeHoldingsPatch(holdingsMap) {
|
|
186
|
+
return {
|
|
187
|
+
card_data: {
|
|
188
|
+
holdings: Object.entries(holdingsMap).map(([symbol, qty]) => ({ symbol, qty })),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Server process ────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function startServer(port) {
|
|
196
|
+
const isPy = SERVER_TYPE === 'py';
|
|
197
|
+
let cmd, cmdArgs;
|
|
198
|
+
if (isPy) {
|
|
199
|
+
const python = findPython();
|
|
200
|
+
if (!python) throw new Error('Python interpreter not found on PATH');
|
|
201
|
+
cmd = python;
|
|
202
|
+
cmdArgs = [PY_SERVER_SCRIPT, '--port', String(port), '--reset'];
|
|
203
|
+
} else {
|
|
204
|
+
cmd = process.execPath;
|
|
205
|
+
cmdArgs = [SERVER_SCRIPT, '--port', String(port), '--reset'];
|
|
206
|
+
}
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const proc = spawn(cmd, cmdArgs, {
|
|
209
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
210
|
+
windowsHide: true,
|
|
211
|
+
});
|
|
212
|
+
let ready = false;
|
|
213
|
+
proc.stdout.on('data', (chunk) => {
|
|
214
|
+
const text = chunk.toString('utf-8');
|
|
215
|
+
process.stdout.write(`[server] ${text}`);
|
|
216
|
+
if (!ready && text.includes('listening on')) { ready = true; resolve(proc); }
|
|
217
|
+
});
|
|
218
|
+
proc.stderr.on('data', (chunk) => process.stderr.write(`[server:err] ${chunk}`));
|
|
219
|
+
proc.on('error', reject);
|
|
220
|
+
proc.on('exit', (code) => { if (!ready) reject(new Error(`Server exited early: code ${code}`)); });
|
|
221
|
+
setTimeout(() => { if (!ready) reject(new Error('Server startup timeout (15s)')); }, 15_000);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
console.log('\n=== portfolio-tracker HTTP E2E test ===');
|
|
228
|
+
console.log(`target: ${BASE} [server: ${SERVER_TYPE}]`);
|
|
229
|
+
console.log(`architecture: main-thread (test driver) + worker-thread (SSE consumer)\n`);
|
|
230
|
+
|
|
231
|
+
const serverProc = await startServer(PORT);
|
|
232
|
+
await new Promise(r => setTimeout(r, 300)); // brief settle
|
|
233
|
+
|
|
234
|
+
let sseWorker = null;
|
|
235
|
+
try {
|
|
236
|
+
// ── Step 1: init-board ──────────────────────────────────────────────────────
|
|
237
|
+
console.log('\n=== Step 1: init-board ===');
|
|
238
|
+
const initRes = await httpGet(`${BASE}/init-board`);
|
|
239
|
+
assert(initRes.status === 200, `init-board returned ${initRes.status}`);
|
|
240
|
+
console.log('[step1] ok');
|
|
241
|
+
|
|
242
|
+
// ── Step 2: Start SSE consumer worker ───────────────────────────────────────
|
|
243
|
+
// The worker opens /sse and forwards every parsed frame here via postMessage.
|
|
244
|
+
// Main thread accumulates frames into NS via applyFrame().
|
|
245
|
+
console.log('\n=== Step 2: Start SSE consumer worker ===');
|
|
246
|
+
sseWorker = new Worker(SSE_WORKER_SCRIPT, {
|
|
247
|
+
workerData: { sseUrl: `${BASE}/sse` },
|
|
248
|
+
});
|
|
249
|
+
sseWorker.on('message', (msg) => {
|
|
250
|
+
if (msg.type === 'frame') {
|
|
251
|
+
applyFrame(msg.payload);
|
|
252
|
+
} else if (msg.type === 'error') {
|
|
253
|
+
console.error(`[sse-worker] error: ${msg.message}`);
|
|
254
|
+
} else if (msg.type === 'closed') {
|
|
255
|
+
console.log('[sse-worker] SSE stream closed by server');
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
sseWorker.on('error', (err) => console.error(`[sse-worker] uncaught: ${err.message}`));
|
|
259
|
+
|
|
260
|
+
const initialPayload = await waitForInitialPayload();
|
|
261
|
+
console.log(`[step2] SSE worker online — initial payload (${initialPayload.cardDefinitions?.length ?? 0} cards)`);
|
|
262
|
+
console.log(` statusGen=${NS.statusGeneration}, dataObjects=${JSON.stringify(Object.keys(NS.dataObjects))}`);
|
|
263
|
+
|
|
264
|
+
// ── T1: Wait for initial drain ──────────────────────────────────────────────
|
|
265
|
+
console.log('\n=== T1: Wait for initial completion ===');
|
|
266
|
+
const t1Summary = await waitForAllCompleted(60_000, 'T1 initial drain');
|
|
267
|
+
console.log(`[T1] board completed — ${JSON.stringify(t1Summary)}`);
|
|
268
|
+
|
|
269
|
+
const t1Prices = await waitForPriceSymbols(['AAPL', 'MSFT'], 30_000, 'T1 prices');
|
|
270
|
+
assert(Object.values(t1Prices).every(v => typeof v === 'number'), 'T1: all prices must be numbers');
|
|
271
|
+
const t1Table = NS.computedValues['holdings-table']?.table;
|
|
272
|
+
assert(Array.isArray(t1Table?.rows) && t1Table.rows.length === 2, `T1: expected 2 rows, got ${t1Table?.rows?.length}`);
|
|
273
|
+
const t1Total = NS.computedValues['portfolio-value']?.totalValue;
|
|
274
|
+
assert(typeof t1Total === 'number' && t1Total > 0, `T1: totalValue must be positive, got ${t1Total}`);
|
|
275
|
+
console.log(`[T1] passed: prices=[AAPL,MSFT], rows=2, totalValue=${t1Total.toFixed(2)}`);
|
|
276
|
+
|
|
277
|
+
// ── T2a: Add GOOG to holdings ────────────────────────────────────────────────
|
|
278
|
+
console.log('\n=== T2a: Update holdings — add GOOG ===');
|
|
279
|
+
const t2Patch = await httpPatch(
|
|
280
|
+
`${BASE}/cards/portfolio-form`,
|
|
281
|
+
makeHoldingsPatch({ AAPL: 50, MSFT: 30, GOOG: 100 }),
|
|
282
|
+
);
|
|
283
|
+
assert(t2Patch.status === 200, `PATCH portfolio-form returned ${t2Patch.status}`);
|
|
284
|
+
console.log('[T2a] PATCH ok — worker will receive SSE notifications independently');
|
|
285
|
+
|
|
286
|
+
// ── T2b: Wait for 3-ticker completion ───────────────────────────────────────
|
|
287
|
+
console.log('\n=== T2b: Wait for 3-ticker completion ===');
|
|
288
|
+
const t2Summary = await waitForAllCompleted(60_000, 'T2b 3-ticker drain');
|
|
289
|
+
console.log(`[T2b] completed — ${JSON.stringify(t2Summary)}`);
|
|
290
|
+
|
|
291
|
+
const t2Prices = await waitForPriceSymbols(['AAPL', 'GOOG', 'MSFT'], 30_000, 'T2b prices');
|
|
292
|
+
const t2Table = NS.computedValues['holdings-table']?.table;
|
|
293
|
+
assert(Array.isArray(t2Table?.rows) && t2Table.rows.length === 3, `T2b: expected 3 rows, got ${t2Table?.rows?.length}`);
|
|
294
|
+
const t2Total = NS.computedValues['portfolio-value']?.totalValue;
|
|
295
|
+
assert(typeof t2Total === 'number' && t2Total > 0, 'T2b: totalValue must be positive');
|
|
296
|
+
console.log(`[T2b] passed: prices=[AAPL,GOOG,MSFT], rows=3, totalValue=${t2Total.toFixed(2)}`);
|
|
297
|
+
|
|
298
|
+
// ── T3: Rapid 3× holdings updates (queue stress) ─────────────────────────────
|
|
299
|
+
// The worker independently streams all SSE notifications while the driver
|
|
300
|
+
// fires rapid PATCHes. NS accumulates state continuously in both cases.
|
|
301
|
+
console.log('\n=== T3: Rapid 3× holdings updates ===');
|
|
302
|
+
const rapidUpdates = [
|
|
303
|
+
{ AAPL: 45, MSFT: 30, GOOG: 110, TSLA: 60 },
|
|
304
|
+
{ AAPL: 45, MSFT: 30, GOOG: 110, AMZN: 100 }, // intermediate — not expected to be final
|
|
305
|
+
{ AAPL: 40, MSFT: 35, GOOG: 120, TSLA: 70 }, // V5 — expected final state
|
|
306
|
+
];
|
|
307
|
+
for (const holdings of rapidUpdates) {
|
|
308
|
+
await httpPatch(`${BASE}/cards/portfolio-form`, makeHoldingsPatch(holdings));
|
|
309
|
+
}
|
|
310
|
+
console.log('[T3] rapid PATCHes sent — worker accumulates SSE state in parallel');
|
|
311
|
+
|
|
312
|
+
await waitForAllCompleted(60_000, 'T3 rapid-update drain');
|
|
313
|
+
const t3Prices = await waitForPriceSymbols(['AAPL', 'GOOG', 'MSFT', 'TSLA'], 30_000, 'T3 final prices');
|
|
314
|
+
const t3Table = NS.computedValues['holdings-table']?.table;
|
|
315
|
+
assert(Array.isArray(t3Table?.rows) && t3Table.rows.length === 4, `T3: expected 4 rows, got ${t3Table?.rows?.length}`);
|
|
316
|
+
assert(!Object.keys(t3Prices).includes('AMZN'), `T3: AMZN must not be present (got ${JSON.stringify(Object.keys(t3Prices))})`);
|
|
317
|
+
console.log(`[T3] passed: prices=${JSON.stringify(Object.keys(t3Prices).sort())}, rows=4, AMZN absent`);
|
|
318
|
+
|
|
319
|
+
// ── T4: Cross-verify portfolio-value totalValue ───────────────────────────────
|
|
320
|
+
console.log('\n=== T4: Cross-verify totalValue ===');
|
|
321
|
+
const t4Total = NS.computedValues['portfolio-value']?.totalValue;
|
|
322
|
+
assert(typeof t4Total === 'number' && t4Total > 0, `T4: totalValue must be positive, got ${t4Total}`);
|
|
323
|
+
const sumRows = t3Table.rows.reduce((acc, r) => acc + (r.value || 0), 0);
|
|
324
|
+
assert(Math.abs(sumRows - t4Total) < 0.01, `T4: mismatch: sumRows=${sumRows}, totalValue=${t4Total}`);
|
|
325
|
+
console.log(`[T4] passed: totalValue=${t4Total.toFixed(2)}, sumRows=${sumRows.toFixed(2)}`);
|
|
326
|
+
|
|
327
|
+
// ── T5: board-status HTTP cross-check ────────────────────────────────────────
|
|
328
|
+
// Compare the HTTP board-status endpoint response against what the worker
|
|
329
|
+
// accumulated via SSE — the two sources must agree.
|
|
330
|
+
console.log('\n=== T5: board-status HTTP cross-check ===');
|
|
331
|
+
const t5Res = await httpGet(`${BASE}/board-status`);
|
|
332
|
+
assert(t5Res.status === 200, `board-status returned ${t5Res.status}`);
|
|
333
|
+
const t5Summary = t5Res.data?.statusSnapshot?.summary;
|
|
334
|
+
assert(t5Summary, 'T5: statusSnapshot.summary missing from board-status');
|
|
335
|
+
assert(t5Summary.completed === t5Summary.card_count,
|
|
336
|
+
`T5: completed=${t5Summary.completed} !== card_count=${t5Summary.card_count}`);
|
|
337
|
+
assert(t5Summary.failed === 0, `T5: failed=${t5Summary.failed} (expected 0)`);
|
|
338
|
+
|
|
339
|
+
// Cross-check: dataObjects from HTTP response matches what worker accumulated
|
|
340
|
+
const httpDataObjKeys = Object.keys(t5Res.data.dataObjectsByToken || {}).sort().join(',');
|
|
341
|
+
const workerDataObjKeys = Object.keys(NS.dataObjects).sort().join(',');
|
|
342
|
+
assert(httpDataObjKeys === workerDataObjKeys,
|
|
343
|
+
`T5: HTTP dataObjects keys [${httpDataObjKeys}] differ from worker-accumulated [${workerDataObjKeys}]`);
|
|
344
|
+
|
|
345
|
+
console.log(`[T5] summary: ${JSON.stringify(t5Summary)}`);
|
|
346
|
+
console.log(`[T5] HTTP vs worker dataObjects agree: [${workerDataObjKeys}]`);
|
|
347
|
+
console.log(`[T5] statusGen at end: ${NS.statusGeneration}`);
|
|
348
|
+
console.log('[T5] all assertions passed');
|
|
349
|
+
|
|
350
|
+
console.log('\n=== All tests passed ✓ ===\n');
|
|
351
|
+
|
|
352
|
+
} finally {
|
|
353
|
+
sseWorker?.terminate();
|
|
354
|
+
serverProc.kill();
|
|
355
|
+
await new Promise(r => serverProc.on('exit', r));
|
|
356
|
+
console.log(`[portfolio-tracker-http-test] server stopped (${SERVER_TYPE})`);
|
|
357
|
+
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
3
|
import * as path from 'node:path';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { parseRef, blobStorageForRef } from 'yaml-flow/storage-refs';
|
|
5
6
|
|
|
6
7
|
function parseArgs(argv) {
|
|
7
|
-
const inIdx
|
|
8
|
-
const outIdx = argv.indexOf('--out');
|
|
9
|
-
const errIdx = argv.indexOf('--err');
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
14
|
-
console.error('Usage: <adapter> run-inference --in
|
|
8
|
+
const inIdx = argv.indexOf('--in-ref');
|
|
9
|
+
const outIdx = argv.indexOf('--out-ref');
|
|
10
|
+
const errIdx = argv.indexOf('--err-ref');
|
|
11
|
+
const inRefStr = inIdx !== -1 ? argv[inIdx + 1] : undefined;
|
|
12
|
+
const outRefStr = outIdx !== -1 ? argv[outIdx + 1] : undefined;
|
|
13
|
+
const errRefStr = errIdx !== -1 ? argv[errIdx + 1] : undefined;
|
|
14
|
+
if (!inRefStr || !outRefStr || !errRefStr) {
|
|
15
|
+
console.error('Usage: <adapter> run-inference --in-ref <::kind::value> --out-ref <::kind::value> --err-ref <::kind::value>');
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
const inRef = parseRef(inRefStr);
|
|
19
|
+
const outRef = parseRef(outRefStr);
|
|
20
|
+
const errRef = parseRef(errRefStr);
|
|
21
|
+
const inStorage = blobStorageForRef(inRef);
|
|
22
|
+
const outStorage = blobStorageForRef(outRef);
|
|
23
|
+
const errStorage = blobStorageForRef(errRef);
|
|
24
|
+
return { inRef, outRef, errRef, inStorage, outStorage, errStorage };
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
const envBoardDir = (process.env.BOARD_DIR ?? '').trim();
|
|
@@ -144,10 +151,12 @@ async function main() {
|
|
|
144
151
|
process.exit(1);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
const {
|
|
154
|
+
const { inRef, outRef, errRef, inStorage, outStorage, errStorage } = parseArgs(process.argv.slice(3));
|
|
148
155
|
|
|
149
156
|
try {
|
|
150
|
-
const
|
|
157
|
+
const rawIn = inStorage.read(inRef.value);
|
|
158
|
+
if (rawIn === null) throw new Error(`Input not found: ${inRef.value}`);
|
|
159
|
+
const payload = JSON.parse(rawIn);
|
|
151
160
|
const tmpCandidates = resolveSyncTmpFileCandidates(payload);
|
|
152
161
|
if (tmpCandidates.length > 0) {
|
|
153
162
|
await waitForTmpSyncInput(tmpCandidates);
|
|
@@ -174,12 +183,12 @@ async function main() {
|
|
|
174
183
|
};
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
outStorage.write(outRef.value, JSON.stringify(result));
|
|
187
|
+
errStorage.write(errRef.value, '');
|
|
179
188
|
} catch (err) {
|
|
180
189
|
const message = err instanceof Error ? err.message : String(err);
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
errStorage.write(errRef.value, message);
|
|
191
|
+
outStorage.write(outRef.value, JSON.stringify({ isTaskCompleted: false, reason: message, evidence: '' }));
|
|
183
192
|
process.exit(1);
|
|
184
193
|
}
|
|
185
194
|
}
|