perf(web): load run previews instead of full runs in Agent chat
Loading a session fetched GET /runs/{run_id} one message at a time for
every assistant turn without stored metrics, and each run card without
an equity curve fetched it again on mount. Most turns are plain
replies, and backtest cards pulled the full chart payload just to draw
a sparkline.
History loading now requests GET /runs/{run_id}/preview for all those
turns at once, deduplicated by run id, and awaits them in message
order. Live completion and RunCompleteCard use the same endpoint.
lib/runCards.ts turns a preview into card fields for both the history
and live paths; a failed preview still shows the fallback card, as a
failed run fetch did before. report_worthy comes from the backend, so
the client-side rule in lib/runReports.ts and its test helper are
removed.
Signed-off-by: alex <alexwan02@163.com>
This commit is contained in:
1 parent
1cc1aaa97e
commit
ca113bb0c3
14 files changed
+200
-182
No files matched your search
@@ -37,8 +37,8 @@ export const RunCompleteCard = memo(function RunCompleteCard({ msg }: Props) {
|
||||
|
||||
let cancelled = false;
|
||||
setCurveLoading(true);
|
||||
api.getRun(msg.runId).then(r => {
|
||||
if (!cancelled && r.equity_curve) {
|
||||
api.getRunPreview(msg.runId).then(r => {
|
||||
if (!cancelled) {
|
||||
setCurve(r.equity_curve.map(e => ({ time: e.time, equity: e.equity })));
|
||||
}
|
||||
}).catch(() => {}).finally(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RunCompleteCard } from "../RunCompleteCard";
|
||||
import { makeMessage } from "@/tests/helpers/factories";
|
||||
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getRun: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getRunPine: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -12,7 +12,7 @@ vi.mock("@/lib/api", () => ({ api: apiMock }));
|
||||
|
||||
describe("RunCompleteCard", () => {
|
||||
beforeEach(() => {
|
||||
apiMock.getRun.mockResolvedValue({ status: "success", run_id: "strategy-run" });
|
||||
apiMock.getRunPreview.mockResolvedValue({ metrics: null, equity_curve: [], report_worthy: true });
|
||||
apiMock.getRunPine.mockResolvedValue({ exists: false, content: null });
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { hasStoredMetrics, runCardFields } from "../runCards";
|
||||
|
||||
describe("runCardFields", () => {
|
||||
it("skips plain replies", () => {
|
||||
expect(runCardFields({ metrics: null, equity_curve: [], report_worthy: false })).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps a card without data when the preview failed", () => {
|
||||
expect(runCardFields(null)).toEqual({ metrics: undefined, equityCurve: undefined });
|
||||
});
|
||||
|
||||
it("maps metrics and equity points for report runs", () => {
|
||||
const metrics = {
|
||||
final_value: 110, total_return: 0.1, annual_return: 0.05, max_drawdown: -0.02,
|
||||
sharpe: 1.1, win_rate: 0.5, trade_count: 3,
|
||||
};
|
||||
expect(runCardFields({
|
||||
metrics,
|
||||
equity_curve: [{ time: "2025-01-01", equity: "100", drawdown: "0" }],
|
||||
report_worthy: true,
|
||||
})).toEqual({ metrics, equityCurve: [{ time: "2025-01-01", equity: 100 }] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasStoredMetrics", () => {
|
||||
it("treats a missing or empty object as no metrics", () => {
|
||||
expect(hasStoredMetrics(undefined)).toBe(false);
|
||||
expect(hasStoredMetrics({})).toBe(false);
|
||||
expect(hasStoredMetrics({ sharpe: 1 })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { isReportWorthyRun } from "../runReports";
|
||||
import { makeRunData } from "@/tests/helpers/factories";
|
||||
|
||||
describe("isReportWorthyRun", () => {
|
||||
it("returns false for null/undefined", () => {
|
||||
expect(isReportWorthyRun(null)).toBe(false);
|
||||
expect(isReportWorthyRun(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty run with no data", () => {
|
||||
expect(isReportWorthyRun(makeRunData())).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when metrics has keys", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ metrics: { total_return: 0.1, final_value: 10000 } })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when run_card has keys", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ run_card: { schema_version: "1.0" } })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when equity_curve is non-empty", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ equity_curve: [{ time: "2024-01-01", equity: 10000 }] })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when trade_log is non-empty", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ trade_log: [{ date: "2024-01-01", action: "BUY" }] })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when trade_markers is non-empty", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ trade_markers: [{ time: "2024-01-01", side: "buy", price: 100 }] })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when validation has keys", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ validation: { passed: true } })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when price_series has non-empty array", () => {
|
||||
expect(
|
||||
isReportWorthyRun(
|
||||
makeRunData({
|
||||
price_series: {
|
||||
AAPL: [{ time: "2024-01-01", open: 100, high: 105, low: 99, close: 103, volume: 1000 }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when price_series values are all empty arrays", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ price_series: { AAPL: [] } })),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when artifacts contain matching filenames", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ artifacts: [{ name: "metrics.json", path: "/tmp/metrics.json" }] })),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ artifacts: [{ name: "trades.csv", path: "/tmp/t.csv" }] })),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ artifacts: [{ name: "strategy.pine", path: "/tmp/s.pine" }] })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for artifacts with non-matching names", () => {
|
||||
expect(
|
||||
isReportWorthyRun(makeRunData({ artifacts: [{ name: "readme.txt", path: "/tmp/r.txt" }] })),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("handles empty artifacts array", () => {
|
||||
expect(isReportWorthyRun(makeRunData({ artifacts: [] }))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -524,6 +524,7 @@ export const api = {
|
||||
const qs = q.toString();
|
||||
return request<RunData>(`/runs/${id}${qs ? `?${qs}` : ""}`);
|
||||
},
|
||||
getRunPreview: (id: string) => request<RunPreview>(`/runs/${id}/preview`),
|
||||
getRunCode: (id: string) => request<Record<string, string>>(`/runs/${id}/code`),
|
||||
getRunFactor: (id: string) => request<FactorReportPayload>(`/runs/${id}/factor`),
|
||||
getRunAttribution: (id: string) => request<AttributionResponse>(`/runs/${encodeURIComponent(id)}/attribution`),
|
||||
@@ -1166,6 +1167,14 @@ export interface EquityPoint {
|
||||
drawdown: string | number;
|
||||
}
|
||||
|
||||
/** What a chat run card needs from `GET /runs/{id}/preview`. */
|
||||
export interface RunPreview {
|
||||
metrics: BacktestMetrics | null;
|
||||
equity_curve: EquityPoint[];
|
||||
/** False for plain chat replies, which own a run directory but no report. */
|
||||
report_worthy: boolean;
|
||||
}
|
||||
|
||||
/** Monte Carlo fan-chart payload: percentile envelope + sampled paths over trade order. */
|
||||
export interface MonteCarloEquityPaths {
|
||||
steps: number[];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RunPreview } from "@/lib/api";
|
||||
import type { AgentMessage } from "@/types/agent";
|
||||
|
||||
/** Whether a stored reply already carries backtest metrics, so its card needs no preview. */
|
||||
export function hasStoredMetrics(metrics: Record<string, number> | undefined): boolean {
|
||||
return !!metrics && Object.keys(metrics).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Card fields for a finished run, or `null` when the run has nothing to report.
|
||||
* A failed preview still gets a card so the report link stays reachable.
|
||||
*/
|
||||
export function runCardFields(
|
||||
preview: RunPreview | null,
|
||||
): Pick<AgentMessage, "metrics" | "equityCurve"> | null {
|
||||
if (preview && !preview.report_worthy) return null;
|
||||
return {
|
||||
metrics: preview?.metrics ?? undefined,
|
||||
equityCurve: preview?.equity_curve.map((e) => ({ time: e.time, equity: Number(e.equity) })),
|
||||
};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { RunData } from "@/lib/api";
|
||||
|
||||
function hasItems(value: unknown): boolean {
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
}
|
||||
|
||||
function hasObjectKeys(value: unknown): boolean {
|
||||
return !!value && typeof value === "object" && Object.keys(value).length > 0;
|
||||
}
|
||||
|
||||
export function isReportWorthyRun(run: Pick<
|
||||
RunData,
|
||||
"metrics" | "run_card" | "equity_curve" | "trade_log" | "price_series" | "trade_markers" | "validation" | "artifacts"
|
||||
> | null | undefined): boolean {
|
||||
if (!run) return false;
|
||||
if (hasObjectKeys(run.metrics)) return true;
|
||||
if (hasObjectKeys(run.run_card)) return true;
|
||||
if (hasItems(run.equity_curve)) return true;
|
||||
if (hasItems(run.trade_log)) return true;
|
||||
if (hasItems(run.trade_markers)) return true;
|
||||
if (hasObjectKeys(run.validation)) return true;
|
||||
if (run.price_series && Object.values(run.price_series).some(hasItems)) return true;
|
||||
return (run.artifacts || []).some((artifact) =>
|
||||
/(?:metrics|equity|trades|positions|ohlcv|validation|strategy)\.(?:csv|json|pine|py)$/i.test(artifact.name),
|
||||
);
|
||||
}
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
type StoredAgentMessage,
|
||||
} from "@/stores/agent";
|
||||
import { useSSE } from "@/hooks/useSSE";
|
||||
import { ApiError, AUTH_REQUIRED_MESSAGE, api, isAuthRequiredError, type GoalSnapshot, type MandateProposal, type MandateCommitted, type ScheduledResearchProposal, type LiveAction, type LiveHalted, type LLMSettings } from "@/lib/api";
|
||||
import { ApiError, AUTH_REQUIRED_MESSAGE, api, isAuthRequiredError, type GoalSnapshot, type MandateProposal, type MandateCommitted, type ScheduledResearchProposal, type LiveAction, type LiveHalted, type LLMSettings, type RunPreview } from "@/lib/api";
|
||||
import {
|
||||
extractUploadedAttachments,
|
||||
} from "@/lib/attachments";
|
||||
import { hasStoredMetrics, runCardFields } from "@/lib/runCards";
|
||||
import { buildChatPrompt, MAX_GOAL_CHARS, MAX_MESSAGE_CHARS, promptExceedsLimit, SWARM_PROMPT_PREFIX } from "@/lib/chatPrompt";
|
||||
import { isReportWorthyRun } from "@/lib/runReports";
|
||||
import type { AgentMessage, SwarmRunStatus, ToolCallEntry } from "@/types/agent";
|
||||
import { AgentAvatar } from "@/components/chat/AgentAvatar";
|
||||
import { WelcomeScreen } from "@/components/chat/WelcomeScreen";
|
||||
@@ -526,6 +526,17 @@ export function Agent() {
|
||||
try {
|
||||
const msgs = await api.getSessionMessages(sid);
|
||||
if (genRef.current !== gen) return;
|
||||
// Every assistant turn owns a run directory, so most of these are plain
|
||||
// replies. Request all previews up front rather than one per message.
|
||||
const previews = new Map<string, Promise<RunPreview | null>>();
|
||||
for (const m of msgs) {
|
||||
const meta = m.metadata as Record<string, unknown> | undefined;
|
||||
const runId = meta?.run_id as string | undefined;
|
||||
const metrics = meta?.metrics as Record<string, number> | undefined;
|
||||
if (m.role !== "user" && runId && !hasStoredMetrics(metrics) && !previews.has(runId)) {
|
||||
previews.set(runId, api.getRunPreview(runId).catch(() => null));
|
||||
}
|
||||
}
|
||||
const agentMsgs: StoredAgentMessage[] = [];
|
||||
let latestRuntimeIdentity: RuntimeIdentity | null = null;
|
||||
for (const m of msgs) {
|
||||
@@ -594,35 +605,12 @@ export function Agent() {
|
||||
if (m.content && m.content !== "Strategy execution completed.") {
|
||||
agentMsgs.push({ id: m.message_id + "_ans", type: "answer", content: m.content, elapsed_ms: elapsedMs, timestamp: assistantTs });
|
||||
}
|
||||
if (metrics && Object.keys(metrics).length > 0) {
|
||||
if (hasStoredMetrics(metrics)) {
|
||||
agentMsgs.push({ id: m.message_id, type: "run_complete", content: "", runId, metrics, timestamp: assistantTs + 1 });
|
||||
} else {
|
||||
// Fetch run data to check report-worthiness; show fallback card if fetch fails
|
||||
let fetchedMetrics: Record<string, number> | undefined;
|
||||
let fetchedCurve: Array<{ time: string; equity: number }> | undefined;
|
||||
let showCard = false;
|
||||
try {
|
||||
const runData = await api.getRun(runId);
|
||||
if (isReportWorthyRun(runData)) {
|
||||
fetchedMetrics = runData.metrics;
|
||||
fetchedCurve = runData.equity_curve?.map((e) => ({ time: e.time, equity: Number(e.equity) }));
|
||||
showCard = true;
|
||||
}
|
||||
// succeeded but not report-worthy (plain chat turn) → skip card
|
||||
} catch {
|
||||
// fetch failed (auth/404/network) → can't tell, show link as fallback
|
||||
showCard = true;
|
||||
}
|
||||
if (showCard) {
|
||||
agentMsgs.push({
|
||||
id: m.message_id,
|
||||
type: "run_complete",
|
||||
content: "",
|
||||
runId,
|
||||
metrics: fetchedMetrics,
|
||||
equityCurve: fetchedCurve,
|
||||
timestamp: assistantTs + 1,
|
||||
});
|
||||
const card = runCardFields((await previews.get(runId)) ?? null);
|
||||
if (card) {
|
||||
agentMsgs.push({ id: m.message_id, type: "run_complete", content: "", runId, ...card, timestamp: assistantTs + 1 });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1044,24 +1032,12 @@ export function Agent() {
|
||||
|
||||
// Show RunCompleteCard when the turn produced backtest metrics or a shadow report
|
||||
if (runId) {
|
||||
let runMetrics: Record<string, number> | undefined;
|
||||
let runCurve: Array<{ time: string; equity: number }> | undefined;
|
||||
let showCard = false;
|
||||
try {
|
||||
const runData = await api.getRun(runId);
|
||||
if (isReportWorthyRun(runData)) {
|
||||
runMetrics = runData.metrics;
|
||||
runCurve = runData.equity_curve?.map(e => ({ time: e.time, equity: Number(e.equity) }));
|
||||
showCard = true;
|
||||
}
|
||||
} catch {
|
||||
showCard = true; // fetch failed → show link as fallback
|
||||
}
|
||||
if (showCard || shadowId) {
|
||||
const card = runCardFields(await api.getRunPreview(runId).catch(() => null));
|
||||
if (card || shadowId) {
|
||||
s.addMessage({
|
||||
id: "", type: "run_complete", content: "", runId,
|
||||
metrics: showCard ? runMetrics : undefined,
|
||||
equityCurve: showCard ? runCurve : undefined,
|
||||
metrics: card?.metrics,
|
||||
equityCurve: card?.equityCurve,
|
||||
shadowId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useAgentStore } from "@/stores/agent";
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getGoal: vi.fn(),
|
||||
getLLMSettings: vi.fn(),
|
||||
getRun: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
sseUrl: vi.fn((sid: string) => `/sessions/${sid}/events`),
|
||||
}));
|
||||
@@ -184,7 +184,7 @@ describe("Agent activity finished while the page was away", () => {
|
||||
useAgentStore.getState().reset();
|
||||
sseMock.connect.mockClear();
|
||||
apiMock.getGoal.mockResolvedValue(null);
|
||||
apiMock.getRun.mockResolvedValue({});
|
||||
apiMock.getRunPreview.mockResolvedValue({ metrics: null, equity_curve: [], report_worthy: false });
|
||||
apiMock.getLLMSettings.mockResolvedValue({
|
||||
provider: "deepseek",
|
||||
model_name: "deepseek-v4-pro",
|
||||
@@ -285,7 +285,7 @@ describe("Agent completion trusts the backend's attempt start", () => {
|
||||
useAgentStore.getState().reset();
|
||||
sseMock.connect.mockClear();
|
||||
apiMock.getGoal.mockResolvedValue(null);
|
||||
apiMock.getRun.mockResolvedValue({});
|
||||
apiMock.getRunPreview.mockResolvedValue({ metrics: null, equity_curve: [], report_worthy: false });
|
||||
apiMock.getSessionMessages.mockResolvedValue([]);
|
||||
apiMock.getLLMSettings.mockResolvedValue({
|
||||
provider: "deepseek",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useAgentStore } from "@/stores/agent";
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getGoal: vi.fn(),
|
||||
getLLMSettings: vi.fn(),
|
||||
getRun: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
sseUrl: vi.fn((sid: string) => `/sessions/${sid}/events`),
|
||||
}));
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useAgentStore } from "@/stores/agent";
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getGoal: vi.fn(),
|
||||
getLLMSettings: vi.fn(),
|
||||
getRun: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
sseUrl: vi.fn((sid: string) => `/sessions/${sid}/events`),
|
||||
}));
|
||||
@@ -59,7 +59,7 @@ describe("Agent grounding status line", () => {
|
||||
useAgentStore.getState().reset();
|
||||
sseMock.connect.mockClear();
|
||||
apiMock.getGoal.mockResolvedValue(null);
|
||||
apiMock.getRun.mockResolvedValue({});
|
||||
apiMock.getRunPreview.mockResolvedValue({ metrics: null, equity_curve: [], report_worthy: false });
|
||||
apiMock.getSessionMessages.mockResolvedValue([]);
|
||||
apiMock.getLLMSettings.mockResolvedValue({
|
||||
provider: "deepseek",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { createMemoryRouter, RouterProvider } from "react-router";
|
||||
import { Agent } from "../Agent";
|
||||
import { useAgentStore } from "@/stores/agent";
|
||||
|
||||
// Every assistant turn owns a run directory, so history loading asks the
|
||||
// preview endpoint which of them deserve a run card.
|
||||
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getGoal: vi.fn(),
|
||||
getLLMSettings: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
sseUrl: vi.fn((sid: string) => `/sessions/${sid}/events`),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/api")>("@/lib/api");
|
||||
return { ...actual, api: apiMock };
|
||||
});
|
||||
|
||||
const sseMock = vi.hoisted(() => ({
|
||||
connect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
onStatusChange: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useSSE", () => ({
|
||||
useSSE: () => sseMock,
|
||||
}));
|
||||
|
||||
function assistant(id: string, runId: string, minute: number) {
|
||||
return {
|
||||
message_id: id,
|
||||
role: "assistant",
|
||||
content: `reply ${id}`,
|
||||
created_at: `2025-01-01T00:${String(minute).padStart(2, "0")}:00Z`,
|
||||
metadata: { run_id: runId, status: "completed" },
|
||||
};
|
||||
}
|
||||
|
||||
function runCards() {
|
||||
return useAgentStore.getState().messages.filter((m) => m.type === "run_complete");
|
||||
}
|
||||
|
||||
// Each case uses its own session: the agent store keeps a module-level cache
|
||||
// of past sessions that reset() does not clear.
|
||||
function renderAgentAt(sessionId: string) {
|
||||
const router = createMemoryRouter(
|
||||
[{ path: "/", element: <Agent /> }],
|
||||
{ initialEntries: [`/?session=${sessionId}`] },
|
||||
);
|
||||
render(<RouterProvider router={router} />);
|
||||
}
|
||||
|
||||
describe("Agent history run previews", () => {
|
||||
beforeEach(() => {
|
||||
useAgentStore.getState().reset();
|
||||
apiMock.getGoal.mockResolvedValue(null);
|
||||
apiMock.getLLMSettings.mockResolvedValue({ sse_timeout_seconds: 90 });
|
||||
apiMock.getRunPreview.mockReset();
|
||||
Object.defineProperty(HTMLElement.prototype, "scrollTo", { configurable: true, value: vi.fn() });
|
||||
});
|
||||
|
||||
it("cards only report-worthy or unknown runs and asks once per run", async () => {
|
||||
apiMock.getSessionMessages.mockResolvedValue([
|
||||
assistant("m1", "run-chat", 1),
|
||||
assistant("m2", "run-backtest", 2),
|
||||
assistant("m3", "run-backtest", 3),
|
||||
assistant("m4", "run-broken", 4),
|
||||
]);
|
||||
apiMock.getRunPreview.mockImplementation(async (runId: string) => {
|
||||
if (runId === "run-chat") return { metrics: null, equity_curve: [], report_worthy: false };
|
||||
if (runId === "run-backtest") {
|
||||
return {
|
||||
metrics: { final_value: 110 },
|
||||
equity_curve: [{ time: "2025-01-01", equity: "100", drawdown: "0" }],
|
||||
report_worthy: true,
|
||||
};
|
||||
}
|
||||
throw new Error("network");
|
||||
});
|
||||
|
||||
renderAgentAt("session-one");
|
||||
|
||||
await waitFor(() => expect(runCards()).toHaveLength(3));
|
||||
expect(runCards().map((m) => m.runId)).toEqual(["run-backtest", "run-backtest", "run-broken"]);
|
||||
expect(runCards()[0].equityCurve).toEqual([{ time: "2025-01-01", equity: 100 }]);
|
||||
expect(runCards()[2].metrics).toBeUndefined();
|
||||
expect(apiMock.getRunPreview.mock.calls.map(([id]) => id).sort()).toEqual(
|
||||
["run-backtest", "run-broken", "run-chat"],
|
||||
);
|
||||
});
|
||||
|
||||
it("requests every preview before any of them answers", async () => {
|
||||
apiMock.getSessionMessages.mockResolvedValue([
|
||||
assistant("m1", "run-a", 1),
|
||||
assistant("m2", "run-b", 2),
|
||||
assistant("m3", "run-c", 3),
|
||||
]);
|
||||
apiMock.getRunPreview.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
renderAgentAt("session-two");
|
||||
|
||||
await waitFor(() => expect(apiMock.getRunPreview).toHaveBeenCalledTimes(3));
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { useAgentStore } from "@/stores/agent";
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
getGoal: vi.fn(),
|
||||
getLLMSettings: vi.fn(),
|
||||
getRun: vi.fn(),
|
||||
getRunPreview: vi.fn(),
|
||||
getSessionMessages: vi.fn(),
|
||||
sseUrl: vi.fn((sid: string) => `/sessions/${sid}/events`),
|
||||
}));
|
||||
|
||||
@@ -23,17 +23,6 @@ export function makeToolCall(overrides: Partial<ToolCallEntry> = {}): ToolCallEn
|
||||
};
|
||||
}
|
||||
|
||||
/** Minimal RunData shape for isReportWorthyRun tests */
|
||||
type ReportWorthyInput = Parameters<typeof import("@/lib/runReports").isReportWorthyRun>[0];
|
||||
|
||||
export function makeRunData<T extends object = object>(overrides: T = {} as T): ReportWorthyInput {
|
||||
return {
|
||||
status: "done",
|
||||
run_id: `run-${++_seq}`,
|
||||
...overrides,
|
||||
} as ReportWorthyInput;
|
||||
}
|
||||
|
||||
/** Reset factory counters between test files */
|
||||
export function resetFactories() {
|
||||
_seq = 0;
|
||||
|
||||
Reference in new issue
Block a user