Files
next-tjwater-frontend/src/features/workbench/hooks/use-agent-prompt-suggestions.test.ts
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

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);
});
});