gloomberb 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.ts +10 -2
  5. package/src/api-client/research-activity.ts +61 -0
  6. package/src/api-client/types.ts +2 -1
  7. package/src/cli/commands/plugins.ts +4 -3
  8. package/src/cli/desktop-pane-shot.ts +79 -10
  9. package/src/cli/pane-functions/capabilities.ts +76 -23
  10. package/src/cli/pane-functions/dom.test.ts +41 -0
  11. package/src/cli/pane-functions/dom.ts +199 -0
  12. package/src/cli/pane-functions/index.test.ts +13 -2
  13. package/src/cli/pane-functions/index.ts +4 -4
  14. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  15. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  16. package/src/cli/pane-functions/report.test.ts +29 -1
  17. package/src/cli/pane-functions/report.ts +68 -7
  18. package/src/cli/pane-functions/screenshot.ts +193 -23
  19. package/src/cli/restore-plugins.ts +10 -3
  20. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  21. package/src/components/chart/composite/composite-chart.tsx +87 -21
  22. package/src/components/chart/composite/rasterizer.ts +33 -0
  23. package/src/components/chart/composite/renderers.test.ts +51 -4
  24. package/src/components/chart/composite/scene.test.ts +128 -2
  25. package/src/components/chart/composite/scene.ts +92 -16
  26. package/src/components/chart/composite/text-renderer.ts +13 -0
  27. package/src/components/chart/composite/time-scale.ts +30 -10
  28. package/src/components/chart/composite/types.ts +26 -0
  29. package/src/components/command-bar/surface/assist.test.tsx +20 -9
  30. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  31. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  32. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  33. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  34. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  35. package/src/data/config/store/index.test.ts +23 -0
  36. package/src/data/config/store/node.ts +7 -1
  37. package/src/data/config/store/normalize.ts +4 -0
  38. package/src/plugins/builtin/cds/client.test.ts +20 -0
  39. package/src/plugins/builtin/cds/client.ts +13 -3
  40. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  41. package/src/plugins/builtin/cds/headless.ts +113 -0
  42. package/src/plugins/builtin/cds/index.tsx +4 -0
  43. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  44. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  45. package/src/plugins/builtin/cloud/askg/protocol.test.ts +189 -0
  46. package/src/plugins/builtin/cloud/askg/protocol.ts +274 -0
  47. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  48. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  49. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  50. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  51. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  52. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  53. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  54. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  55. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +5 -2
  56. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  57. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  58. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  59. package/src/plugins/builtin/market-movers/client.ts +76 -0
  60. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  61. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  62. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  63. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  64. package/src/plugins/builtin/research/event-model.ts +245 -0
  65. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  66. package/src/plugins/builtin/research/events-headless.ts +87 -0
  67. package/src/plugins/builtin/research/index.tsx +4 -0
  68. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  69. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  70. package/src/plugins/builtin/scanner/headless.ts +125 -0
  71. package/src/plugins/builtin/scanner/index.ts +4 -0
  72. package/src/plugins/builtin/sectors/client.ts +54 -0
  73. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  74. package/src/plugins/builtin/sectors/headless.ts +80 -0
  75. package/src/plugins/builtin/sectors/index.tsx +6 -51
  76. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  77. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  78. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  79. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  80. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  81. package/src/plugins/builtin/volatility/client.ts +16 -3
  82. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  83. package/src/plugins/builtin/volatility/headless.ts +75 -0
  84. package/src/plugins/builtin/volatility/index.tsx +4 -0
  85. package/src/plugins/builtin/world-indices/client.ts +43 -0
  86. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  87. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  88. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  89. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  90. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  91. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  92. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  93. package/src/plugins/bundle.test.ts +58 -1
  94. package/src/plugins/bundle.ts +4 -2
  95. package/src/plugins/catalog-backend.test.ts +26 -0
  96. package/src/plugins/catalog-backend.ts +9 -2
  97. package/src/plugins/catalog-browser.test.ts +0 -13
  98. package/src/plugins/catalog-browser.ts +4 -0
  99. package/src/plugins/catalog-manifest.test.ts +15 -0
  100. package/src/plugins/catalog-manifest.ts +12 -5
  101. package/src/plugins/catalog-ui.ts +0 -6
  102. package/src/plugins/host-contract.ts +9 -0
  103. package/src/plugins/host-modules.ts +12 -0
  104. package/src/plugins/loader.ts +63 -15
  105. package/src/plugins/seed.ts +3 -0
  106. package/src/public/broker.ts +6 -0
  107. package/src/remote/schema.test.ts +65 -0
  108. package/src/remote/schema.ts +322 -39
  109. package/src/remote/types.ts +49 -2
  110. package/src/renderers/browser/app-services.ts +3 -0
  111. package/src/renderers/browser/config-host.ts +24 -8
  112. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  113. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  114. package/src/renderers/browser/main.tsx +3 -1
  115. package/src/renderers/browser/research-entry.test.ts +8 -0
  116. package/src/renderers/browser/research-entry.ts +7 -0
  117. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  118. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  119. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  120. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  121. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  122. package/src/sync/core-contributors.test.ts +111 -0
  123. package/src/sync/core-contributors.ts +155 -1
  124. package/src/time-series/resolve.test.ts +118 -1
  125. package/src/time-series/resolve.ts +14 -16
  126. package/src/types/config.ts +2 -0
  127. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  128. package/src/plugins/broker-sync/native-loader.ts +0 -10
  129. package/src/plugins/broker-sync/normalize.ts +0 -308
  130. package/src/plugins/broker-sync/public.ts +0 -122
  131. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  132. package/src/plugins/broker-sync/robinhood.ts +0 -114
  133. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  134. package/src/plugins/broker-sync/simplefin.ts +0 -70
  135. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -0,0 +1,199 @@
1
+ import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
2
+ import { renderSection, renderTable } from "../../utils/cli-output";
3
+ import type { MarketContext } from "../types";
4
+ import type { PaneFunctionReport } from "./report";
5
+ import type { ResolvedPaneFunction } from "./resolver";
6
+ import { collectShotSymbols } from "./data";
7
+ import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
8
+
9
+ const DOM_REPORT_WIDTH = 1280;
10
+ const DOM_REPORT_HEIGHT = 720;
11
+ const DOM_LIMITATION =
12
+ "Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
13
+
14
+ function rowsContainEllipsis(rows: DesktopPaneShotRenderedRow[]): boolean {
15
+ return rows.some((row) => row.cells.some((cell) => /\u2026|\.\.\./.test(cell.text)));
16
+ }
17
+
18
+ export function isDomReportTruncated(
19
+ render: Pick<PaneScreenshotResult["render"], "rows" | "truncated">,
20
+ ): boolean {
21
+ return render.truncated || rowsContainEllipsis(render.rows);
22
+ }
23
+
24
+ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
25
+ const byTable = new Map<number, DesktopPaneShotRenderedRow[]>();
26
+ for (const row of rows) {
27
+ const tableRows = byTable.get(row.tableIndex) ?? [];
28
+ tableRows.push(row);
29
+ byTable.set(row.tableIndex, tableRows);
30
+ }
31
+
32
+ return [...byTable.entries()].flatMap(([tableIndex, tableRows], index) => {
33
+ const columnCount = Math.max(0, ...tableRows.map((row) => row.cells.length));
34
+ const columns = Array.from({ length: columnCount }, (_, columnIndex) => ({
35
+ header: tableRows.find((row) => row.cells[columnIndex])?.cells[columnIndex]?.columnLabel
36
+ ?? `Column ${columnIndex + 1}`,
37
+ }));
38
+ const output = renderTable(
39
+ columns,
40
+ tableRows.map((row) => columns.map((_column, columnIndex) => row.cells[columnIndex]?.text ?? "")),
41
+ );
42
+ return [
43
+ ...(index > 0 ? [""] : []),
44
+ ...(byTable.size > 1 ? [renderSection(`Rendered table ${tableIndex + 1}`)] : []),
45
+ output,
46
+ ];
47
+ });
48
+ }
49
+
50
+ function renderedFailureReason(
51
+ result: PaneScreenshotResult,
52
+ rows: DesktopPaneShotRenderedRow[],
53
+ ): string | null {
54
+ if (result.render.loadingStateDetected) return "The rendered pane was still loading.";
55
+ if (result.render.errorStateDetected) {
56
+ const marker = result.render.errorStateMarkers[0];
57
+ return marker ? `The rendered pane reported: ${marker}` : "The rendered pane reported an error.";
58
+ }
59
+ if (result.render.emptyStateDetected) {
60
+ const marker = result.render.emptyStateMarkers[0];
61
+ return marker ? `The rendered pane reported: ${marker}` : "The rendered pane reported an empty state.";
62
+ }
63
+ if (rows.length === 0) return "The rendered pane exposed no structured rows or visible text.";
64
+ return null;
65
+ }
66
+
67
+ function reportRows(result: PaneScreenshotResult): DesktopPaneShotRenderedRow[] {
68
+ if (result.render.rows.length > 0) return result.render.rows;
69
+ if (
70
+ result.render.loadingStateDetected
71
+ || result.render.errorStateDetected
72
+ || result.render.emptyStateDetected
73
+ || !result.render.visibleText
74
+ ) {
75
+ return [];
76
+ }
77
+ return [{
78
+ tableIndex: 0,
79
+ rowIndex: 0,
80
+ selected: false,
81
+ cells: [{ columnLabel: "Rendered view", text: result.render.visibleText }],
82
+ }];
83
+ }
84
+
85
+ export function buildDomPaneReportFromRender(
86
+ resolved: ResolvedPaneFunction,
87
+ result: PaneScreenshotResult,
88
+ ): PaneFunctionReport {
89
+ const rows = reportRows(result);
90
+ const hasStructuredRows = result.render.rows.length > 0;
91
+ const truncated = isDomReportTruncated({ ...result.render, rows });
92
+ const truncationReasons = [...result.render.truncationReasons];
93
+ if (rowsContainEllipsis(rows) && !truncationReasons.includes("one or more cells are visibly clipped")) {
94
+ truncationReasons.push("one or more cells are visibly clipped");
95
+ }
96
+ const failureReason = renderedFailureReason(result, rows);
97
+ const unavailableSymbols = failureReason && result.symbols.length > 0 ? result.symbols : [];
98
+ const textLines = [resolved.label, ""];
99
+ if (hasStructuredRows) {
100
+ textLines.push(...renderDomTables(rows));
101
+ } else if (result.render.visibleText) {
102
+ textLines.push(result.render.visibleText);
103
+ } else {
104
+ textLines.push(failureReason ?? "No rendered values were available.");
105
+ }
106
+ if (failureReason && rows.length > 0) textLines.push("", failureReason);
107
+
108
+ return {
109
+ data: {
110
+ kind: "rendered-view",
111
+ target: resolved.token,
112
+ capabilityId: resolved.capability.id,
113
+ symbols: result.symbols,
114
+ options: resolved.options,
115
+ rowCount: rows.length,
116
+ empty: rows.length === 0,
117
+ complete: failureReason === null && !truncated,
118
+ unavailableSymbols,
119
+ rows,
120
+ visibleText: result.render.visibleText,
121
+ truncated,
122
+ truncationReasons,
123
+ limitation: DOM_LIMITATION,
124
+ ...(failureReason ? { reason: failureReason } : {}),
125
+ },
126
+ text: textLines.join("\n").trimEnd(),
127
+ };
128
+ }
129
+
130
+ export function appendDomReportFooter(
131
+ text: string,
132
+ elapsedMs: number,
133
+ truncated: boolean,
134
+ ): string {
135
+ const clipping = truncated ? "The rendered view is clipped." : "The rendered view may be clipped.";
136
+ return [
137
+ text,
138
+ "",
139
+ `Rendered view: values come from the visible pane. ${clipping} Render time: ${elapsedMs} ms.`,
140
+ ].join("\n");
141
+ }
142
+
143
+ function failedDomReport(
144
+ resolved: ResolvedPaneFunction,
145
+ rawArg: string,
146
+ error: unknown,
147
+ ): PaneFunctionReport {
148
+ const message = (error instanceof Error ? error.message : String(error))
149
+ .split("\n")[0]!
150
+ .replace(/^Error:\s*/, "")
151
+ .trim();
152
+ const reason = `The rendered view could not be read: ${message || "unknown renderer error"}`;
153
+ const symbols = collectShotSymbols(resolved, rawArg);
154
+ return {
155
+ data: {
156
+ kind: "rendered-view",
157
+ target: resolved.token,
158
+ capabilityId: resolved.capability.id,
159
+ symbols,
160
+ options: resolved.options,
161
+ rowCount: 0,
162
+ empty: true,
163
+ complete: false,
164
+ unavailableSymbols: symbols,
165
+ rows: [],
166
+ visibleText: "",
167
+ truncated: false,
168
+ truncationReasons: [],
169
+ limitation: DOM_LIMITATION,
170
+ reason,
171
+ },
172
+ text: [resolved.label, "", reason].join("\n"),
173
+ };
174
+ }
175
+
176
+ export async function buildDomFunctionReport(
177
+ resolved: ResolvedPaneFunction,
178
+ context: MarketContext,
179
+ rawArg: string,
180
+ ): Promise<PaneFunctionReport> {
181
+ try {
182
+ const result = await renderDesktopShot({
183
+ resolved,
184
+ context,
185
+ rawArg,
186
+ outputPath: "",
187
+ width: DOM_REPORT_WIDTH,
188
+ height: DOM_REPORT_HEIGHT,
189
+ theme: null,
190
+ scale: 1,
191
+ watermark: null,
192
+ options: {},
193
+ captureImage: false,
194
+ });
195
+ return buildDomPaneReportFromRender(resolved, result);
196
+ } catch (error) {
197
+ return failedDomReport(resolved, rawArg, error);
198
+ }
199
+ }
@@ -10,6 +10,7 @@ const {
10
10
  filterPaneCatalogEntries,
11
11
  renderPaneCatalogReport,
12
12
  getPaneFunctionCapability,
13
+ isDataPaneForDomFallback,
13
14
  normalizeCapabilityOptions,
14
15
  capabilityPluginState,
15
16
  } = paneFunctionTestInternals;
@@ -208,13 +209,23 @@ describe("pane function CLI args", () => {
208
209
  });
209
210
  });
210
211
 
211
- test("enables live DOM screenshots without claiming report support for unmapped panes", () => {
212
+ test("maps data panes to rendered reports and keeps interactive panes unsupported", () => {
212
213
  expect(capabilityFor("new-api-pane")).toMatchObject({
213
- id: "unverified-pane",
214
+ id: "new-api-pane",
214
215
  botSafe: false,
216
+ outputKind: "rendered-view",
217
+ reportReadiness: "live-dom",
218
+ screenshotReadiness: "live-dom",
219
+ });
220
+
221
+ const helpPane = { ...dummyPane, id: "help" };
222
+ expect(getPaneFunctionCapability(undefined, helpPane)).toMatchObject({
223
+ id: "help",
215
224
  reportReadiness: "unsupported",
216
225
  screenshotReadiness: "live-dom",
217
226
  });
227
+ expect(isDataPaneForDomFallback(dummyPane)).toBe(true);
228
+ expect(isDataPaneForDomFallback(helpPane)).toBe(false);
218
229
  });
219
230
 
220
231
  test("rejects financial statement options on a price comparison", () => {
@@ -25,6 +25,7 @@ import {
25
25
  import {
26
26
  capabilityPluginState,
27
27
  getPaneFunctionCapability,
28
+ isDataPaneForDomFallback,
28
29
  normalizeCapabilityOptions,
29
30
  } from "./capabilities";
30
31
 
@@ -106,16 +107,14 @@ export async function runPaneScreenshot(args: string[], ctx: CliCommandContext)
106
107
  });
107
108
  if (parsed.requireBotSafe && !result.usable) {
108
109
  throw new Error(
109
- `${resolved.token} did not produce a usable bot-safe screenshot: `
110
- + `${result.empty ? "empty render" : ""}${result.empty && !result.complete ? ", " : ""}`
111
- + `${!result.complete ? `missing data for ${result.unavailableSymbols.join(", ")}` : ""}`
112
- + `${result.semanticMismatch ? `${(result.empty || !result.complete) ? ", " : ""}rendered content did not match the requested capability` : ""}`,
110
+ `${resolved.token} did not produce a usable bot-safe screenshot: ${result.unusableReason ?? "unknown reason"}`,
113
111
  );
114
112
  }
115
113
  ctx.printResult({ data: result }, {
116
114
  text: (data) => [
117
115
  `Saved screenshot to ${data.outputPath}`,
118
116
  `Result: ${data.rowCount} semantic rows; empty=${data.empty}; complete=${data.complete}; mismatch=${data.semanticMismatch}; usable=${data.usable}`,
117
+ ...(data.unusableReason ? [`Unusable reason: ${data.unusableReason}`] : []),
119
118
  ...(data.unavailableSymbols.length > 0
120
119
  ? [`Unavailable symbols: ${data.unavailableSymbols.join(", ")}`]
121
120
  : []),
@@ -180,6 +179,7 @@ export const paneFunctionTestInternals = {
180
179
  filterPaneCatalogEntries,
181
180
  renderPaneCatalogReport,
182
181
  getPaneFunctionCapability,
182
+ isDataPaneForDomFallback,
183
183
  normalizeCapabilityOptions,
184
184
  capabilityPluginState,
185
185
  };
@@ -0,0 +1,307 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHART_COMPOSER_PANE_ID, createDefaultConfig } from "../../types/config";
3
+ import type { DataProvider } from "../../types/data-provider";
4
+ import type { PricePoint, TickerFinancials } from "../../types/financials";
5
+ import {
6
+ buildIntradayPriceChartPreset,
7
+ buildPriceChartPreset,
8
+ } from "../../plugins/builtin/chart-composer/presets";
9
+ import type { MarketContext } from "../types";
10
+ import {
11
+ buildDesktopShotPayload,
12
+ intradayChartEvidenceMismatchesFor,
13
+ shotUnusableReasonFor,
14
+ } from "./screenshot";
15
+ import type { ResolvedPaneFunction } from "./resolver";
16
+ import {
17
+ resolveShotIntradaySessionWindow,
18
+ resolveShotIntradayRequest,
19
+ } from "./intraday-shot";
20
+
21
+ function sessionBars(date: string, base: number): PricePoint[] {
22
+ return [
23
+ {
24
+ date: new Date(`${date}T13:30:00.000Z`),
25
+ open: base,
26
+ high: base + 1,
27
+ low: base - 1,
28
+ close: base + 0.25,
29
+ volume: 100,
30
+ },
31
+ {
32
+ date: new Date(`${date}T13:35:00.000Z`),
33
+ open: base + 0.25,
34
+ high: base + 1.25,
35
+ low: base,
36
+ close: base + 0.5,
37
+ volume: 200,
38
+ },
39
+ ];
40
+ }
41
+
42
+ function financials(priceHistory: PricePoint[] = []): TickerFinancials {
43
+ return {
44
+ quote: {
45
+ symbol: "AAPL",
46
+ price: 108,
47
+ currency: "USD",
48
+ change: 1,
49
+ changePercent: 1,
50
+ lastUpdated: Date.parse("2026-09-03T20:00:00.000Z"),
51
+ listingExchangeName: "NASDAQ",
52
+ },
53
+ annualStatements: [],
54
+ quarterlyStatements: [],
55
+ priceHistory,
56
+ };
57
+ }
58
+
59
+ function contextWithProvider(provider: DataProvider): MarketContext {
60
+ return {
61
+ config: createDefaultConfig("/tmp/gloomberb-shot-test"),
62
+ dataDir: "/tmp/gloomberb-shot-test",
63
+ persistence: {
64
+ pluginState: { get: () => null },
65
+ },
66
+ store: {
67
+ loadTicker: async () => ({
68
+ metadata: {
69
+ ticker: "AAPL",
70
+ exchange: "NASDAQ",
71
+ currency: "USD",
72
+ name: "Apple",
73
+ portfolios: [],
74
+ watchlists: [],
75
+ positions: [],
76
+ custom: {},
77
+ tags: [],
78
+ },
79
+ }),
80
+ },
81
+ dataProvider: provider,
82
+ } as unknown as MarketContext;
83
+ }
84
+
85
+ function resolvedChart(
86
+ capabilityId: "intraday-price-chart" | "price-chart",
87
+ spec: ReturnType<typeof buildIntradayPriceChartPreset>,
88
+ options: Record<string, string>,
89
+ ): ResolvedPaneFunction {
90
+ return {
91
+ pane: { id: CHART_COMPOSER_PANE_ID },
92
+ capability: { id: capabilityId, options: [] },
93
+ options,
94
+ instance: {
95
+ instanceId: `${capabilityId}:test`,
96
+ paneId: CHART_COMPOSER_PANE_ID,
97
+ title: "AAPL",
98
+ binding: { kind: "fixed", symbol: "AAPL" },
99
+ settings: { chartSpec: spec },
100
+ },
101
+ createOptions: { symbol: "AAPL" },
102
+ } as unknown as ResolvedPaneFunction;
103
+ }
104
+
105
+ async function payloadFor(
106
+ resolved: ResolvedPaneFunction,
107
+ provider: DataProvider,
108
+ ) {
109
+ return buildDesktopShotPayload(
110
+ resolved,
111
+ contextWithProvider(provider),
112
+ "AAPL",
113
+ {},
114
+ 1200,
115
+ 675,
116
+ "tokyo",
117
+ 1.5,
118
+ null,
119
+ );
120
+ }
121
+
122
+ describe("GIP session windows", () => {
123
+ const history = [
124
+ ...sessionBars("2026-08-27", 100),
125
+ ...sessionBars("2026-08-28", 101),
126
+ ...sessionBars("2026-08-31", 102),
127
+ ...sessionBars("2026-09-01", 103),
128
+ ...sessionBars("2026-09-02", 104),
129
+ ...sessionBars("2026-09-03", 105),
130
+ ];
131
+
132
+ test("resolves the latest session, latest five sessions, and an explicit session", () => {
133
+ const latest = resolveShotIntradaySessionWindow(history, {
134
+ rangePreset: "1D",
135
+ timeZone: "America/New_York",
136
+ });
137
+ expect(latest.sessionDates).toEqual(["2026-09-03"]);
138
+ expect(latest.points).toHaveLength(2);
139
+
140
+ const week = resolveShotIntradaySessionWindow(history, {
141
+ rangePreset: "1W",
142
+ timeZone: "America/New_York",
143
+ });
144
+ expect(week.sessionDates).toEqual([
145
+ "2026-08-28",
146
+ "2026-08-31",
147
+ "2026-09-01",
148
+ "2026-09-02",
149
+ "2026-09-03",
150
+ ]);
151
+ expect(week.points).toHaveLength(10);
152
+
153
+ const explicit = resolveShotIntradaySessionWindow(history, {
154
+ rangePreset: "1W",
155
+ session: "2026-09-01",
156
+ timeZone: "America/New_York",
157
+ });
158
+ expect(explicit.sessionDates).toEqual(["2026-09-01"]);
159
+ expect(explicit.points.map((point) => point.close)).toEqual([103.25, 103.5]);
160
+ });
161
+
162
+ test("maps Auto to the preset interval and validates explicit dates", () => {
163
+ expect(resolveShotIntradayRequest({ rangePreset: "1D", chartResolution: "auto" }))
164
+ .toEqual({ rangePreset: "1D", resolution: "1m", session: null });
165
+ expect(resolveShotIntradayRequest({ rangePreset: "1W", chartResolution: "auto" }))
166
+ .toEqual({ rangePreset: "1W", resolution: "5m", session: null });
167
+ expect(resolveShotIntradayRequest({
168
+ rangePreset: "1W",
169
+ chartResolution: "15m",
170
+ session: "2026-09-02",
171
+ })).toEqual({ rangePreset: "1D", resolution: "15m", session: "2026-09-02" });
172
+ expect(() => resolveShotIntradayRequest({ session: "2026-02-30" }))
173
+ .toThrow("real calendar date");
174
+ });
175
+ });
176
+
177
+ describe("GIP screenshot payload", () => {
178
+ test("injects resolution-aware intraday bars for GIP", async () => {
179
+ const calls: Array<{ range: string; resolution: string }> = [];
180
+ const history = [
181
+ ...sessionBars("2026-09-02", 100),
182
+ ...sessionBars("2026-09-03", 105),
183
+ ];
184
+ const provider = {
185
+ id: "test",
186
+ name: "Test",
187
+ getTickerFinancials: async () => financials([
188
+ { date: new Date("2026-09-03T00:00:00.000Z"), close: 105 },
189
+ ]),
190
+ getPriceHistory: async () => [],
191
+ getPriceHistoryForResolution: async (_symbol, _exchange, range, resolution) => {
192
+ calls.push({ range, resolution });
193
+ return history;
194
+ },
195
+ getDetailedPriceHistory: async () => [],
196
+ } as DataProvider;
197
+ const spec = buildIntradayPriceChartPreset("AAPL");
198
+ spec.viewport.resolution = "auto";
199
+ const payload = await payloadFor(
200
+ resolvedChart("intraday-price-chart", spec, {
201
+ rangePreset: "1D",
202
+ chartResolution: "auto",
203
+ }),
204
+ provider,
205
+ );
206
+
207
+ expect(calls).toEqual([{ range: "1W", resolution: "1m" }]);
208
+ expect(payload.intradayHistories).toHaveLength(1);
209
+ expect(payload.intradayHistories[0]).toMatchObject({
210
+ symbol: "AAPL",
211
+ exchange: "NASDAQ",
212
+ rangePreset: "1D",
213
+ resolution: "1m",
214
+ requestedSession: null,
215
+ sessionDates: ["2026-09-03"],
216
+ unavailableReason: null,
217
+ });
218
+ expect(payload.intradayHistories[0]?.points).toHaveLength(2);
219
+ expect(payload.financials[0]?.[1].priceHistory).toHaveLength(2);
220
+
221
+ const renderedEvidence = [{
222
+ role: "chart-data",
223
+ metadata: {
224
+ kind: "chart-composer",
225
+ projectedPointCount: 2,
226
+ baseSeries: [{
227
+ pointCount: 2,
228
+ first: { date: "2026-09-03T13:30:00.000Z", value: 105.25 },
229
+ last: { date: "2026-09-03T13:35:00.000Z", value: 105.5 },
230
+ }],
231
+ },
232
+ }];
233
+ const resolved = resolvedChart("intraday-price-chart", spec, {
234
+ rangePreset: "1D",
235
+ chartResolution: "auto",
236
+ });
237
+ expect(intradayChartEvidenceMismatchesFor(resolved, payload, renderedEvidence as never))
238
+ .toEqual([]);
239
+ renderedEvidence[0]!.metadata.baseSeries[0]!.pointCount = 1;
240
+ expect(intradayChartEvidenceMismatchesFor(resolved, payload, renderedEvidence as never))
241
+ .toContain("rendered intraday point count does not match");
242
+ });
243
+
244
+ test("keeps GP on daily history without an intraday payload", async () => {
245
+ let intradayCalls = 0;
246
+ const daily = [
247
+ { date: new Date("2026-09-02T00:00:00.000Z"), close: 100 },
248
+ { date: new Date("2026-09-03T00:00:00.000Z"), close: 105 },
249
+ ];
250
+ const provider = {
251
+ id: "test",
252
+ name: "Test",
253
+ getTickerFinancials: async () => financials(),
254
+ getPriceHistory: async () => daily,
255
+ getPriceHistoryForResolution: async () => {
256
+ intradayCalls += 1;
257
+ return [];
258
+ },
259
+ } as DataProvider;
260
+ const payload = await payloadFor(
261
+ resolvedChart("price-chart", buildPriceChartPreset("AAPL"), { rangePreset: "3M" }),
262
+ provider,
263
+ );
264
+
265
+ expect(intradayCalls).toBe(0);
266
+ expect(payload.intradayHistories).toEqual([]);
267
+ expect(payload.financials[0]?.[1].priceHistory).toEqual(daily);
268
+ });
269
+
270
+ test("records an explicit unusable reason when no intraday bars exist", async () => {
271
+ const provider = {
272
+ id: "test",
273
+ name: "Test",
274
+ getTickerFinancials: async () => financials([
275
+ { date: new Date("2026-09-03T00:00:00.000Z"), close: 105 },
276
+ ]),
277
+ getPriceHistory: async () => [],
278
+ getPriceHistoryForResolution: async () => [],
279
+ getDetailedPriceHistory: async () => [],
280
+ } as DataProvider;
281
+ const spec = buildIntradayPriceChartPreset("AAPL");
282
+ spec.viewport.resolution = "auto";
283
+ const resolved = resolvedChart("intraday-price-chart", spec, {
284
+ rangePreset: "1D",
285
+ chartResolution: "auto",
286
+ session: "2020-01-02",
287
+ });
288
+ const payload = await payloadFor(resolved, provider);
289
+
290
+ expect(payload.intradayHistories[0]?.points).toEqual([]);
291
+ expect(payload.intradayHistories[0]?.start).toBe("2020-01-02T05:00:00.000Z");
292
+ expect(payload.intradayHistories[0]?.end).toBe("2020-01-03T05:00:00.000Z");
293
+ expect(payload.intradayHistories[0]?.unavailableReason)
294
+ .toBe("No intraday price history is available for AAPL for session 2020-01-02.");
295
+ expect(shotUnusableReasonFor(
296
+ resolved,
297
+ payload,
298
+ {
299
+ loadingStateDetected: false,
300
+ errorStateDetected: false,
301
+ emptyStateDetected: true,
302
+ },
303
+ ["AAPL"],
304
+ false,
305
+ )).toBe("No intraday price history is available for AAPL for session 2020-01-02.");
306
+ });
307
+ });