@openfinclaw/findoo-datahub-plugin 2026.3.10 → 2026.3.12

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.
@@ -2,7 +2,7 @@
2
2
  * L3 — Gateway Bootstrap E2E
3
3
  *
4
4
  * Verifies findoo-datahub-plugin loads correctly in a gateway-like environment:
5
- * 1. Plugin registers all 12 tools
5
+ * 1. Plugin registers all 13 tools
6
6
  * 2. Plugin registers both services (fin-data-provider, fin-regime-detector)
7
7
  * 3. Tools are callable and return well-formed responses
8
8
  * 4. Services are consumable by other extensions
@@ -87,253 +87,268 @@ function parseResult(result: unknown): Record<string, unknown> {
87
87
 
88
88
  /* ---------- tests ---------- */
89
89
 
90
- describe.skipIf(SKIP)(
91
- "L3 Gateway Bootstrap E2E",
92
- { timeout: 120_000 },
93
- () => {
94
- let tempDir: string;
95
- let tools: Map<string, ToolDef>;
96
- let services: Map<string, unknown>;
97
- let logs: Array<{ level: string; msg: string }>;
98
-
99
- beforeAll(async () => {
100
- tempDir = mkdtempSync(join(tmpdir(), "l3-gateway-"));
101
- const ctx = createGatewayApi(tempDir);
102
- tools = ctx.tools;
103
- services = ctx.services;
104
- logs = ctx.logs;
105
- // Simulate gateway calling plugin.register()
106
- await findooDatahubPlugin.register(ctx.api);
90
+ describe.skipIf(SKIP)("L3 — Gateway Bootstrap E2E", { timeout: 120_000 }, () => {
91
+ let tempDir: string;
92
+ let tools: Map<string, ToolDef>;
93
+ let services: Map<string, unknown>;
94
+ let logs: Array<{ level: string; msg: string }>;
95
+
96
+ beforeAll(async () => {
97
+ tempDir = mkdtempSync(join(tmpdir(), "l3-gateway-"));
98
+ const ctx = createGatewayApi(tempDir);
99
+ tools = ctx.tools;
100
+ services = ctx.services;
101
+ logs = ctx.logs;
102
+ // Simulate gateway calling plugin.register()
103
+ await findooDatahubPlugin.register(ctx.api);
104
+ });
105
+
106
+ afterAll(() => {
107
+ rmSync(tempDir, { recursive: true, force: true });
108
+ });
109
+
110
+ /* === Section 1: Plugin registration contract === */
111
+
112
+ describe("1. Registration contract", () => {
113
+ it("1.1 plugin has correct metadata", () => {
114
+ expect(findooDatahubPlugin.id).toBe("findoo-datahub-plugin");
115
+ expect(findooDatahubPlugin.name).toBe("Findoo DataHub");
116
+ expect(findooDatahubPlugin.kind).toBe("financial");
107
117
  });
108
118
 
109
- afterAll(() => {
110
- rmSync(tempDir, { recursive: true, force: true });
119
+ it("1.2 registers exactly 13 tools", () => {
120
+ expect(tools.size).toBe(13);
111
121
  });
112
122
 
113
- /* === Section 1: Plugin registration contract === */
123
+ it("1.3 all 13 tool names match specification", () => {
124
+ const expected = [
125
+ "fin_stock",
126
+ "fin_index",
127
+ "fin_macro",
128
+ "fin_derivatives",
129
+ "fin_crypto",
130
+ "fin_market",
131
+ "fin_query",
132
+ "fin_data_ohlcv",
133
+ "fin_data_regime",
134
+ "fin_ta",
135
+ "fin_etf",
136
+ "fin_data_markets",
137
+ "fin_currency",
138
+ ];
139
+ for (const name of expected) {
140
+ expect(tools.has(name), `Missing tool: ${name}`).toBe(true);
141
+ }
142
+ // No extra tools
143
+ for (const name of tools.keys()) {
144
+ expect(expected.includes(name), `Unexpected tool: ${name}`).toBe(true);
145
+ }
146
+ });
114
147
 
115
- describe("1. Registration contract", () => {
116
- it("1.1 plugin has correct metadata", () => {
117
- expect(findooDatahubPlugin.id).toBe("findoo-datahub-plugin");
118
- expect(findooDatahubPlugin.name).toBe("Findoo DataHub");
119
- expect(findooDatahubPlugin.kind).toBe("financial");
120
- });
148
+ it("1.4 each tool has name, description, and execute function", () => {
149
+ for (const [name, tool] of tools) {
150
+ expect(typeof tool.name, `${name}.name`).toBe("string");
151
+ expect(typeof tool.description, `${name}.description`).toBe("string");
152
+ expect(tool.description!.length, `${name}.description length`).toBeGreaterThan(10);
153
+ expect(typeof tool.execute, `${name}.execute`).toBe("function");
154
+ }
155
+ });
121
156
 
122
- it("1.2 registers exactly 12 tools", () => {
123
- expect(tools.size).toBe(12);
124
- });
157
+ it("1.5 registers both required services", () => {
158
+ expect(services.has("fin-data-provider")).toBe(true);
159
+ expect(services.has("fin-regime-detector")).toBe(true);
160
+ });
125
161
 
126
- it("1.3 all 12 tool names match specification", () => {
127
- const expected = [
128
- "fin_stock",
129
- "fin_index",
130
- "fin_macro",
131
- "fin_derivatives",
132
- "fin_crypto",
133
- "fin_market",
134
- "fin_query",
135
- "fin_data_ohlcv",
136
- "fin_data_regime",
137
- "fin_ta",
138
- "fin_etf",
139
- "fin_data_markets",
140
- ];
141
- for (const name of expected) {
142
- expect(tools.has(name), `Missing tool: ${name}`).toBe(true);
143
- }
144
- // No extra tools
145
- for (const name of tools.keys()) {
146
- expect(expected.includes(name), `Unexpected tool: ${name}`).toBe(true);
147
- }
148
- });
162
+ it("1.6 fin-data-provider service has required methods", () => {
163
+ const provider = services.get("fin-data-provider") as Record<string, unknown>;
164
+ expect(typeof provider.getOHLCV).toBe("function");
165
+ expect(typeof provider.getTicker).toBe("function");
166
+ expect(typeof provider.detectRegime).toBe("function");
167
+ expect(typeof provider.getSupportedMarkets).toBe("function");
168
+ });
149
169
 
150
- it("1.4 each tool has name, description, and execute function", () => {
151
- for (const [name, tool] of tools) {
152
- expect(typeof tool.name, `${name}.name`).toBe("string");
153
- expect(typeof tool.description, `${name}.description`).toBe("string");
154
- expect(tool.description!.length, `${name}.description length`).toBeGreaterThan(10);
155
- expect(typeof tool.execute, `${name}.execute`).toBe("function");
156
- }
157
- });
170
+ it("1.7 fin-regime-detector service has detect method", () => {
171
+ const detector = services.get("fin-regime-detector") as Record<string, unknown>;
172
+ expect(typeof detector.detect).toBe("function");
173
+ });
174
+ });
158
175
 
159
- it("1.5 registers both required services", () => {
160
- expect(services.has("fin-data-provider")).toBe(true);
161
- expect(services.has("fin-regime-detector")).toBe(true);
162
- });
176
+ /* === Section 2: Tool response format contract === */
163
177
 
164
- it("1.6 fin-data-provider service has required methods", () => {
165
- const provider = services.get("fin-data-provider") as Record<string, unknown>;
166
- expect(typeof provider.getOHLCV).toBe("function");
167
- expect(typeof provider.getTicker).toBe("function");
168
- expect(typeof provider.detectRegime).toBe("function");
169
- expect(typeof provider.getSupportedMarkets).toBe("function");
178
+ describe("2. Tool response format", () => {
179
+ it("2.1 category tools return {success, endpoint, count, results}", async () => {
180
+ const tool = tools.get("fin_stock")!;
181
+ const raw = await tool.execute("fmt-1", {
182
+ symbol: "600519.SH",
183
+ endpoint: "price/historical",
184
+ limit: 3,
170
185
  });
171
-
172
- it("1.7 fin-regime-detector service has detect method", () => {
173
- const detector = services.get("fin-regime-detector") as Record<string, unknown>;
174
- expect(typeof detector.detect).toBe("function");
186
+ const res = parseResult(raw);
187
+ expect(res).toHaveProperty("success");
188
+ expect(res).toHaveProperty("endpoint");
189
+ expect(res).toHaveProperty("count");
190
+ expect(res).toHaveProperty("results");
191
+ expect(res.success).toBe(true);
192
+ expect(typeof res.count).toBe("number");
193
+ expect(Array.isArray(res.results)).toBe(true);
194
+ }, 30_000);
195
+
196
+ it("2.2 fin_data_ohlcv returns {symbol, market, timeframe, count, candles}", async () => {
197
+ const tool = tools.get("fin_data_ohlcv")!;
198
+ const raw = await tool.execute("fmt-2", {
199
+ symbol: "600519.SH",
200
+ market: "equity",
201
+ timeframe: "1d",
202
+ limit: 5,
175
203
  });
176
- });
177
-
178
- /* === Section 2: Tool response format contract === */
179
-
180
- describe("2. Tool response format", () => {
181
- it("2.1 category tools return {success, endpoint, count, results}", async () => {
182
- const tool = tools.get("fin_stock")!;
183
- const raw = await tool.execute("fmt-1", {
184
- symbol: "600519.SH",
185
- endpoint: "price/historical",
186
- limit: 3,
187
- });
188
- const res = parseResult(raw);
189
- expect(res).toHaveProperty("success");
190
- expect(res).toHaveProperty("endpoint");
191
- expect(res).toHaveProperty("count");
192
- expect(res).toHaveProperty("results");
193
- expect(res.success).toBe(true);
194
- expect(typeof res.count).toBe("number");
195
- expect(Array.isArray(res.results)).toBe(true);
196
- }, 30_000);
197
-
198
- it("2.2 fin_data_ohlcv returns {symbol, market, timeframe, count, candles}", async () => {
199
- const tool = tools.get("fin_data_ohlcv")!;
200
- const raw = await tool.execute("fmt-2", {
201
- symbol: "600519.SH",
202
- market: "equity",
203
- timeframe: "1d",
204
- limit: 5,
205
- });
206
- const res = parseResult(raw);
207
- expect(res.symbol).toBe("600519.SH");
208
- expect(res.market).toBe("equity");
209
- expect(res.timeframe).toBe("1d");
210
- expect(typeof res.count).toBe("number");
211
- expect(Array.isArray(res.candles)).toBe(true);
212
- const candle = (res.candles as Array<Record<string, unknown>>)[0]!;
213
- expect(candle).toHaveProperty("timestamp");
214
- expect(candle).toHaveProperty("open");
215
- expect(candle).toHaveProperty("high");
216
- expect(candle).toHaveProperty("low");
217
- expect(candle).toHaveProperty("close");
218
- expect(candle).toHaveProperty("volume");
219
- }, 30_000);
220
-
221
- it("2.3 fin_data_regime returns {symbol, market, timeframe, regime}", async () => {
222
- const tool = tools.get("fin_data_regime")!;
223
- const raw = await tool.execute("fmt-3", {
224
- symbol: "600519.SH",
225
- market: "equity",
226
- timeframe: "1d",
227
- });
228
- const res = parseResult(raw);
229
- expect(res.symbol).toBe("600519.SH");
230
- expect(res.market).toBe("equity");
231
- expect(res.timeframe).toBe("1d");
232
- expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
233
- }, 30_000);
234
-
235
- it("2.4 fin_data_markets returns {datahub, markets, categories, endpoints}", async () => {
236
- const tool = tools.get("fin_data_markets")!;
237
- const raw = await tool.execute("fmt-4", {});
238
- const res = parseResult(raw);
239
- expect(res).toHaveProperty("datahub");
240
- expect(res).toHaveProperty("markets");
241
- expect(res).toHaveProperty("categories");
242
- expect(res.endpoints).toBe(172);
204
+ const res = parseResult(raw);
205
+ expect(res.symbol).toBe("600519.SH");
206
+ expect(res.market).toBe("equity");
207
+ expect(res.timeframe).toBe("1d");
208
+ expect(typeof res.count).toBe("number");
209
+ expect(Array.isArray(res.candles)).toBe(true);
210
+ const candle = (res.candles as Array<Record<string, unknown>>)[0]!;
211
+ expect(candle).toHaveProperty("timestamp");
212
+ expect(candle).toHaveProperty("open");
213
+ expect(candle).toHaveProperty("high");
214
+ expect(candle).toHaveProperty("low");
215
+ expect(candle).toHaveProperty("close");
216
+ expect(candle).toHaveProperty("volume");
217
+ }, 30_000);
218
+
219
+ it("2.3 fin_data_regime returns {symbol, market, timeframe, regime}", async () => {
220
+ const tool = tools.get("fin_data_regime")!;
221
+ const raw = await tool.execute("fmt-3", {
222
+ symbol: "600519.SH",
223
+ market: "equity",
224
+ timeframe: "1d",
243
225
  });
226
+ const res = parseResult(raw);
227
+ expect(res.symbol).toBe("600519.SH");
228
+ expect(res.market).toBe("equity");
229
+ expect(res.timeframe).toBe("1d");
230
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(res.regime);
231
+ }, 30_000);
232
+
233
+ it("2.4 fin_data_markets returns {datahub, markets, categories, endpoints}", async () => {
234
+ const tool = tools.get("fin_data_markets")!;
235
+ const raw = await tool.execute("fmt-4", {});
236
+ const res = parseResult(raw);
237
+ expect(res).toHaveProperty("connected");
238
+ expect(res).toHaveProperty("markets");
239
+ expect(res).toHaveProperty("categories");
240
+ expect(res.endpoints).toBe(172);
241
+ });
244
242
 
245
- it("2.5 error responses return {error: string}", async () => {
246
- const tool = tools.get("fin_query")!;
247
- const raw = await tool.execute("fmt-5", { path: "" });
248
- const res = parseResult(raw);
249
- expect(typeof res.error).toBe("string");
250
- expect(res.success).toBeUndefined();
251
- });
243
+ it("2.5 error responses return {error: string}", async () => {
244
+ const tool = tools.get("fin_query")!;
245
+ const raw = await tool.execute("fmt-5", { path: "" });
246
+ const res = parseResult(raw);
247
+ expect(typeof res.error).toBe("string");
248
+ expect(res.success).toBeUndefined();
252
249
  });
250
+ });
253
251
 
254
- /* === Section 3: Config resolution === */
255
-
256
- describe("3. Config edge cases", () => {
257
- it("3.1 plugin warns when no API key configured", async () => {
258
- const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey-"));
259
- const saved = { ...process.env };
260
- delete process.env.DATAHUB_API_KEY;
261
- delete process.env.DATAHUB_PASSWORD;
262
- delete process.env.OPENFINCLAW_DATAHUB_PASSWORD;
263
-
264
- const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
265
- // Clear the pluginConfig entirely so resolveConfig can't find a key
266
- (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
267
- await findooDatahubPlugin.register(ctx.api);
268
-
269
- const warnLog = ctx.logs.find(
270
- (l) => l.level === "warn" && l.msg.includes("no API key"),
271
- );
272
- expect(warnLog).toBeDefined();
273
-
274
- // Restore
275
- Object.assign(process.env, saved);
276
- rmSync(tmpDir2, { recursive: true, force: true });
277
- });
252
+ /* === Section 3: Config resolution === */
278
253
 
279
- it("3.2 plugin still registers all tools even without API key", async () => {
280
- const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey2-"));
281
- const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
282
- (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
283
- await findooDatahubPlugin.register(ctx.api);
284
- expect(ctx.tools.size).toBe(12);
285
- expect(ctx.services.size).toBe(2);
286
- rmSync(tmpDir2, { recursive: true, force: true });
287
- });
254
+ describe("3. Config edge cases", () => {
255
+ it("3.1 plugin warns when no API key configured", async () => {
256
+ const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey-"));
257
+ const saved = { ...process.env };
258
+ delete process.env.DATAHUB_API_KEY;
259
+ delete process.env.DATAHUB_PASSWORD;
260
+ delete process.env.OPENFINCLAW_DATAHUB_PASSWORD;
261
+
262
+ const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
263
+ // Clear the pluginConfig entirely so resolveConfig can't find a key
264
+ (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
265
+ await findooDatahubPlugin.register(ctx.api);
266
+
267
+ const errorLog = ctx.logs.find(
268
+ (l) => l.level === "error" && l.msg.includes("API key is required"),
269
+ );
270
+ expect(errorLog).toBeDefined();
271
+
272
+ // Restore
273
+ Object.assign(process.env, saved);
274
+ rmSync(tmpDir2, { recursive: true, force: true });
288
275
  });
289
276
 
290
- /* === Section 4: Cross-extension service consumption === */
291
-
292
- describe("4. Cross-extension service consumption", () => {
293
- it("4.1 another extension can resolve fin-data-provider and call getOHLCV", async () => {
294
- // Simulate another extension calling the service
295
- const provider = services.get("fin-data-provider") as {
296
- getOHLCV: (p: {
297
- symbol: string;
298
- market: string;
299
- timeframe: string;
300
- limit: number;
301
- }) => Promise<Array<{ timestamp: number; close: number }>>;
302
- };
303
-
304
- const data = await provider.getOHLCV({
305
- symbol: "600519.SH",
306
- market: "equity",
307
- timeframe: "1d",
308
- limit: 10,
309
- });
310
- expect(data.length).toBeGreaterThan(0);
311
- expect(typeof data[0]!.timestamp).toBe("number");
312
- expect(typeof data[0]!.close).toBe("number");
313
- }, 30_000);
314
-
315
- it("4.2 another extension can resolve fin-regime-detector and detect regime", async () => {
316
- const provider = services.get("fin-data-provider") as {
317
- getOHLCV: (p: {
318
- symbol: string;
319
- market: string;
320
- timeframe: string;
321
- limit: number;
322
- }) => Promise<Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume: number }>>;
323
- };
324
- const detector = services.get("fin-regime-detector") as {
325
- detect: (bars: Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume: number }>) => string;
326
- };
327
-
328
- const bars = await provider.getOHLCV({
329
- symbol: "600519.SH",
330
- market: "equity",
331
- timeframe: "1d",
332
- limit: 300,
333
- });
334
- const regime = detector.detect(bars);
335
- expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(regime);
336
- }, 30_000);
277
+ it("3.2 plugin still registers all tools even without API key", async () => {
278
+ const tmpDir2 = mkdtempSync(join(tmpdir(), "l3-nokey2-"));
279
+ const ctx = createGatewayApi(tmpDir2, { datahubApiKey: undefined });
280
+ (ctx.api as unknown as { pluginConfig: Record<string, unknown> }).pluginConfig = {};
281
+ await findooDatahubPlugin.register(ctx.api);
282
+ expect(ctx.tools.size).toBe(13);
283
+ expect(ctx.services.size).toBe(2);
284
+ rmSync(tmpDir2, { recursive: true, force: true });
337
285
  });
338
- },
339
- );
286
+ });
287
+
288
+ /* === Section 4: Cross-extension service consumption === */
289
+
290
+ describe("4. Cross-extension service consumption", () => {
291
+ it("4.1 another extension can resolve fin-data-provider and call getOHLCV", async () => {
292
+ // Simulate another extension calling the service
293
+ const provider = services.get("fin-data-provider") as {
294
+ getOHLCV: (p: {
295
+ symbol: string;
296
+ market: string;
297
+ timeframe: string;
298
+ limit: number;
299
+ }) => Promise<Array<{ timestamp: number; close: number }>>;
300
+ };
301
+
302
+ const data = await provider.getOHLCV({
303
+ symbol: "600519.SH",
304
+ market: "equity",
305
+ timeframe: "1d",
306
+ limit: 10,
307
+ });
308
+ expect(data.length).toBeGreaterThan(0);
309
+ expect(typeof data[0]!.timestamp).toBe("number");
310
+ expect(typeof data[0]!.close).toBe("number");
311
+ }, 30_000);
312
+
313
+ it("4.2 another extension can resolve fin-regime-detector and detect regime", async () => {
314
+ const provider = services.get("fin-data-provider") as {
315
+ getOHLCV: (p: {
316
+ symbol: string;
317
+ market: string;
318
+ timeframe: string;
319
+ limit: number;
320
+ }) => Promise<
321
+ Array<{
322
+ timestamp: number;
323
+ open: number;
324
+ high: number;
325
+ low: number;
326
+ close: number;
327
+ volume: number;
328
+ }>
329
+ >;
330
+ };
331
+ const detector = services.get("fin-regime-detector") as {
332
+ detect: (
333
+ bars: Array<{
334
+ timestamp: number;
335
+ open: number;
336
+ high: number;
337
+ low: number;
338
+ close: number;
339
+ volume: number;
340
+ }>,
341
+ ) => string;
342
+ };
343
+
344
+ const bars = await provider.getOHLCV({
345
+ symbol: "600519.SH",
346
+ market: "equity",
347
+ timeframe: "1d",
348
+ limit: 300,
349
+ });
350
+ const regime = detector.detect(bars);
351
+ expect(["bull", "bear", "sideways", "volatile", "crisis"]).toContain(regime);
352
+ }, 30_000);
353
+ });
354
+ });