import { describe, expect, it, vi } from "vitest"; import { DemoMarketDataProvider, IPADS_BASE_URL, IPADS_WIRE_API, IpadsResponsesProvider } from "../src/agent/provider"; import { loadIpadsConfig } from "../src/server/ipadsConfig"; import type { WatchStock } from "../src/shared/types"; const stock: WatchStock = { id: "AAPL", symbol: "AAPL", display: "AAPL", market: "US", name: "Apple", tags: ["core"], addedAt: "2026-05-11T00:00:00.000Z" }; describe("IpadsResponsesProvider", () => { it("uses the fixed IPADS Responses API endpoint for agent quant analysis", async () => { const fetchImpl = vi.fn(async () => { return new Response( JSON.stringify({ output: [ { type: "message", role: "assistant", content: [ { type: "output_text", text: JSON.stringify({ action: "buy_watch", score: 72, confidence: 0.68, horizon: "1-3 months", summary: "Trend and valuation are supportive.", factors: [ { name: "trend", score: 75, weight: 0.4, evidence: "above MA20" }, { name: "valuation", score: 68, weight: 0.3, evidence: "reasonable" }, { name: "risk", score: 70, weight: 0.3, evidence: "controlled" } ], riskNotes: [] }) } ] } ] }), { status: 200, headers: { "content-type": "application/json" } } ); }); const provider = new IpadsResponsesProvider("secret", fetchImpl); const analysis = await provider.quantAnalysis({ stock, snapshot: { price: 100, currency: "USD", pe: 20, peers: [], news: [] }, candles: [{ date: "2026-05-11", open: 100, high: 102, low: 99, close: 101, volume: 1_000_000 }], indicators: [{ date: "2026-05-11", ma20: 98 }], fundamentals: [], relatedData: { riskFlags: [], sources: [] } }); expect(fetchImpl).toHaveBeenCalledWith( `${IPADS_BASE_URL}/${IPADS_WIRE_API}`, expect.objectContaining({ method: "POST", headers: expect.objectContaining({ Authorization: "Bearer secret", "Content-Type": "application/json" }), body: expect.stringContaining("\"model\":\"gpt-5.5\"") }) ); const calls = fetchImpl.mock.calls as unknown as [string, RequestInit][]; const requestBody = JSON.parse(String(calls[0]?.[1].body)); expect(requestBody.input).toContain("\"symbol\":\"AAPL\""); expect(typeof requestBody.input).toBe("string"); expect(requestBody).not.toHaveProperty("metadata"); expect(analysis.action).toBe("buy_watch"); }); it("falls back to finite local analysis when remote agent cannot analyze empty market data", async () => { const fetchImpl = vi.fn(async () => { return new Response( JSON.stringify({ output_text: JSON.stringify({ action: "hold", score: 50, confidence: 0.4, horizon: "1-3个月", summary: "invalid", factors: [ { name: "trend", score: 50, weight: 1, evidence: "empty" }, { name: "valuation", score: 50, weight: 0, evidence: "empty" }, { name: "risk", score: 50, weight: 0, evidence: "empty" } ], tradePlan: { direction: "wait", entryPrice: null, targetPrice: null, stopLoss: null, expectedReturnPct: null, rationale: "invalid null prices" }, riskNotes: [] }) }), { status: 200, headers: { "content-type": "application/json" } } ); }); const provider = new IpadsResponsesProvider("secret", fetchImpl); const analysis = await provider.quantAnalysis({ stock: { ...stock, symbol: "DRAM", display: "DRAM", id: "DRAM" }, snapshot: { price: 0, currency: "USD", peers: [], news: [] }, candles: [], indicators: [], fundamentals: [], relatedData: { riskFlags: [], sources: [] } }); expect(Number.isFinite(analysis.score)).toBe(true); expect(Number.isFinite(analysis.confidence)).toBe(true); expect(analysis.factors.every((factor) => Number.isFinite(factor.score))).toBe(true); expect(analysis.tradePlan?.entryPrice).toBeUndefined(); }); }); describe("loadIpadsConfig", () => { it("reads IPADS_API_KEY from an env object and keeps fixed URL and wire API", () => { expect(loadIpadsConfig({ IPADS_API_KEY: "secret" })).toEqual({ baseUrl: IPADS_BASE_URL, wireApi: IPADS_WIRE_API, apiKey: "secret", apiKeyConfigured: true }); }); it("reports an unconfigured API key when IPADS_API_KEY is blank", () => { expect(loadIpadsConfig({ IPADS_API_KEY: " " })).toEqual({ baseUrl: IPADS_BASE_URL, wireApi: IPADS_WIRE_API, apiKey: "", apiKeyConfigured: false }); }); }); describe("DemoMarketDataProvider", () => { it("returns deterministic snapshots when no provider URL is configured", async () => { const provider = new DemoMarketDataProvider(); await expect(provider.snapshot(stock)).resolves.toEqual( expect.objectContaining({ currency: "USD", peers: expect.any(Array), news: expect.any(Array) }) ); }); it("returns chart candles and financial statements for local cached views", async () => { const provider = new DemoMarketDataProvider(); await expect(provider.candles(stock)).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ date: expect.any(String), open: expect.any(Number), high: expect.any(Number), low: expect.any(Number), close: expect.any(Number), volume: expect.any(Number) }) ]) ); await expect(provider.fundamentals(stock)).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ period: expect.any(String), revenue: expect.any(Number), netIncome: expect.any(Number) }) ]) ); }); it("returns related data and a structured quantitative analysis", async () => { const provider = new DemoMarketDataProvider(); const candles = await provider.candles(stock); const relatedData = await provider.relatedData(stock); expect(relatedData).toEqual( expect.objectContaining({ sector: expect.any(String), avgVolume20: expect.any(Number), sentimentScore: expect.any(Number) }) ); await expect( provider.quantAnalysis({ stock, snapshot: await provider.snapshot(stock), candles, indicators: [], fundamentals: await provider.fundamentals(stock), relatedData }) ).resolves.toEqual( expect.objectContaining({ action: expect.stringMatching(/buy_watch|hold|avoid/), score: expect.any(Number), factors: expect.any(Array), tradePlan: expect.objectContaining({ rationale: expect.any(String) }) }) ); }); });