import { afterEach, describe, expect, it, vi } from "vitest"; import { act, createElement } from "react"; import { createRoot, type Root } from "react-dom/client"; import { collectRecentAgentActivities, useAgentPromptSuggestions } from "./use-agent-prompt-suggestions"; import { WorkbenchSessionProvider } from "./workbench-session-provider"; import type { ScheduledConditionItem } from "../types"; import type { AgentChatSessionSummary } from "@/features/agent"; describe("recent activities for Agent suggestions", () => { it("uses updated ordinary conversations, excludes stale/private task sessions, and preserves real versus demo origins", () => { const now = Date.parse("2026-10-09T08:00:00Z"); const session = (id: string, updatedAt: number, purpose: AgentChatSessionSummary["purpose"]): AgentChatSessionSummary => ({ id, title: id, purpose, createdAt: now - 10 * 24 * 60 * 60_000, updatedAt, }); const activities = collectRecentAgentActivities({ alerts: [ { id: "demo", title: "演示压力告警", description: "演示", time: "15:00", occurredAt: "2026-10-09T07:00:00Z" }, { id: "operational-task-notification:real", title: "现场核查待办", description: "真实后台任务", time: "15:05", occurredAt: "2026-10-09T07:05:00Z" }, ], alertDataSource: "mock", conditions: [], runtimeDataSource: "mock", artifact: null, sessions: [session("updated-old-conversation", now - 60_000, "ordinary_chat"), session("stale", now - 8 * 24 * 60 * 60_000, "ordinary_chat"), session("task-private", now, "task_collaboration")], }, now); expect(activities.find((item) => item.id === "alert:demo")?.origin).toBe("demo"); expect(activities.find((item) => item.id === "alert:operational-task-notification:real")?.origin).toBe("backend"); expect(activities.filter((item) => item.kind === "session").map((item) => item.title)).toEqual(["updated-old-conversation"]); }); }); describe("recommendation cache lifecycle", () => { let root: Root | undefined; afterEach(async () => { if (root) await act(async () => root!.unmount()); root = undefined; vi.useRealTimers(); vi.restoreAllMocks(); }); it("retries failures after cache expiry with a bounded budget and supports manual retry", async () => { vi.useFakeTimers(); root = createRoot(document.createElement("div")); const request = vi.fn[0]["request"]>().mockRejectedValue(new Error("unavailable")); const empty: never[] = []; let result: ReturnType; function Probe() { result = useAgentPromptSuggestions({ enabled: true, model: "fixture/model", cacheScope: "a", sessionId: null, request, alerts: empty, alertDataSource: "backend", conditions: empty, runtimeDataSource: "backend", artifact: null, sessions: empty }); return null; } await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe)))); expect(result!.unavailable).toBe(true); expect(request).toHaveBeenCalledTimes(1); await act(async () => { await vi.advanceTimersByTimeAsync(30_000); }); expect(request).toHaveBeenCalledTimes(2); await act(async () => { await vi.advanceTimersByTimeAsync(30_000); }); expect(request).toHaveBeenCalledTimes(3); await act(async () => { await vi.advanceTimersByTimeAsync(300_000); }); expect(request).toHaveBeenCalledTimes(3); request.mockResolvedValueOnce([{ question: "核查压力数据", activityId: null }]); await act(async () => result!.retry()); expect(request).toHaveBeenCalledTimes(4); expect(result!.suggestions[0]?.question).toBe("核查压力数据"); expect(result!.unavailable).toBe(false); }); it("retries cached failures on service recovery while preserving successful recommendations", async () => { root = createRoot(document.createElement("div")); const request = vi.fn[0]["request"]>() .mockRejectedValueOnce(new Error("offline")).mockResolvedValue([{ question: "核查压力数据", activityId: null }]); const empty: never[] = []; function Probe({ recoveryVersion }: { recoveryVersion: number }) { useAgentPromptSuggestions({ enabled: true, model: "fixture/model", cacheScope: "a", recoveryVersion, sessionId: null, request, alerts: empty, alertDataSource: "backend", conditions: empty, runtimeDataSource: "backend", artifact: null, sessions: empty }); return null; } for (const recoveryVersion of [0, 1, 2]) { await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { recoveryVersion })))); } expect(request).toHaveBeenCalledTimes(2); }); it("waits for a model and cancels automatic retries when the panel is disabled", async () => { vi.useFakeTimers(); root = createRoot(document.createElement("div")); const request = vi.fn(async () => { throw new Error("unavailable"); }); const empty: never[] = []; function Probe({ model, enabled }: { model: string; enabled: boolean }) { useAgentPromptSuggestions({ enabled, model, cacheScope: "a", sessionId: null, request, alerts: empty, alertDataSource: "backend", conditions: empty, runtimeDataSource: "backend", artifact: null, sessions: empty }); return null; } const render = async (model: string, enabled: boolean) => act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { model, enabled })))); await render("", true); expect(request).not.toHaveBeenCalled(); await render("fixture/model", true); expect(request).toHaveBeenCalledTimes(1); await render("fixture/model", false); await act(async () => { await vi.advanceTimersByTimeAsync(90_000); }); expect(request).toHaveBeenCalledTimes(1); }); it("reuses recommendations across history refresh, panel reopen and a new empty conversation", async () => { const container = document.createElement("div"); root = createRoot(container); const request = vi.fn(async () => [{ question: "核查当前压力", activityId: null }]); const empty: never[] = []; function Probe({ enabled, sessionId }: { enabled: boolean; sessionId: string | null }) { const result = useAgentPromptSuggestions({ enabled, model: "fixture/model", cacheScope: "project:a", sessionId, request, alerts: empty, alertDataSource: "backend", conditions: empty, runtimeDataSource: "backend", artifact: null, sessions: empty }); return createElement("p", null, result.suggestions[0]?.question ?? "loading"); } for (let i = 0; i < 10; i++) { await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { enabled: false, sessionId: null })))); await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { enabled: true, sessionId: `empty-${i}` })))); } expect(request).toHaveBeenCalledTimes(1); expect(container.textContent).toBe("核查当前压力"); }); it("ignores task heartbeat timestamps but regenerates when an activity changes status", async () => { root = createRoot(document.createElement("div")); const request = vi.fn(async () => [{ question: "复核任务证据", activityId: "task:task-a" }]); const empty: never[] = []; function Probe({ time, status }: { time: number; status: "running" | "completed" }) { const condition: ScheduledConditionItem = { id: "task-a", kind: "condition", taskId: "scada-diagnosis", sessionId: "native-a", title: "SCADA 证据复核", summary: "检查压力监测", riskLevel: "normal", scheduledAt: "2026-10-22T01:00:00Z", updatedAt: time, status }; useAgentPromptSuggestions({ enabled: true, model: "fixture/model", cacheScope: "project:a", sessionId: null, request, alerts: empty, alertDataSource: "backend", conditions: [condition], runtimeDataSource: "backend", artifact: null, sessions: empty }); return null; } for (let i = 0; i < 10; i++) await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { time: 1000 + i * 5000, status: "running" })))); expect(request).toHaveBeenCalledTimes(1); await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { time: 60000, status: "completed" })))); expect(request).toHaveBeenCalledTimes(2); }); it("isolates projects and caches generation errors instead of retrying each reopen", async () => { root = createRoot(document.createElement("div")); const request = vi.fn(async () => { throw new Error("unavailable"); }); const empty: never[] = []; function Probe({ scope, enabled }: { scope: string; enabled: boolean }) { useAgentPromptSuggestions({ enabled, cacheScope: scope, model: "fixture/model", sessionId: null, request, alerts: empty, alertDataSource: "backend", conditions: empty, runtimeDataSource: "backend", artifact: null, sessions: empty }); return null; } for (const [scope, enabled] of [["a", true], ["a", false], ["a", true], ["b", true]] as const) { await act(async () => root!.render(createElement(WorkbenchSessionProvider, null, createElement(Probe, { scope, enabled })))); } expect(request).toHaveBeenCalledTimes(2); }); });