141 lines
9.2 KiB
TypeScript
141 lines
9.2 KiB
TypeScript
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<Parameters<typeof useAgentPromptSuggestions>[0]["request"]>().mockRejectedValue(new Error("unavailable"));
|
|
const empty: never[] = [];
|
|
let result: ReturnType<typeof useAgentPromptSuggestions>;
|
|
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<Parameters<typeof useAgentPromptSuggestions>[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);
|
|
});
|
|
});
|