feat(workbench): expand SCADA monitoring and investigation presentation

This commit is contained in:
2026-11-07 19:50:14 +08:00
parent 0801e161ec
commit ba71e2d529
133 changed files with 6754 additions and 1438 deletions
@@ -9,6 +9,77 @@ import {
} from "./project-client";
describe("workbench project client", () => {
it("explains project-list timeouts using the Server readiness result", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
if (new URL(String(input)).pathname === "/ready") return Response.json({
status: "not_ready", checks: { startup: "ready", metadata_database: "unavailable", mcp_transport: "ready" }
}, { status: 503 });
throw new DOMException("signal timed out", "TimeoutError");
});
vi.stubGlobal("fetch", fetchMock);
await expect(fetchWorkbenchProjects(async () => "token")).rejects.toThrow("读取授权项目超时(已等待 15 秒)");
expect(fetchMock).toHaveBeenCalledTimes(2);
const probe = fetchMock.mock.calls.find(([url]) => new URL(String(url)).pathname === "/ready");
expect(probe).toBeDefined();
expect(new Headers(probe?.[1]?.headers).has("Authorization")).toBe(false);
});
it("identifies metadata unavailability instead of exposing the raw abort message", async () => {
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
if (new URL(String(input)).pathname === "/ready") return Response.json({
status: "not_ready", checks: { startup: "ready", metadata_database: "unavailable", mcp_transport: "ready" }
}, { status: 503 });
throw new DOMException("signal timed out", "TimeoutError");
}));
await expect(fetchWorkbenchProjects(async () => "token")).rejects.toThrow("项目元数据库暂不可用");
});
it("names backend startup and an unavailable listener separately", async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
if (new URL(String(input)).pathname === "/ready") return Response.json({
status: "not_ready", checks: { startup: "not_ready", metadata_database: "not_checked", mcp_transport: "not_ready" }
}, { status: 503 });
throw new DOMException("signal timed out", "TimeoutError");
});
vi.stubGlobal("fetch", fetchMock);
await expect(fetchWorkbenchProjects(async () => "token")).rejects.toThrow("后端服务正在启动");
fetchMock.mockRejectedValue(new DOMException("signal timed out", "TimeoutError"));
await expect(fetchWorkbenchProjects(async () => "token")).rejects.toThrow("后端服务未响应");
});
it("preserves caller cancellation without probing readiness or presenting a timeout", async () => {
const controller = new AbortController();
controller.abort();
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
await expect(fetchWorkbenchProjects(undefined, controller.signal)).rejects.toMatchObject({ name: "AbortError" });
expect(fetchMock).not.toHaveBeenCalled();
});
it("retries a timed-out lookup after the backend recovers without keeping its cached failure", async () => {
let available = false;
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
if (new URL(String(input)).pathname === "/ready") return Response.json({
status: "ready", checks: { startup: "ready", metadata_database: "ready", mcp_transport: "ready" }
});
if (!available) throw new DOMException("signal timed out", "TimeoutError");
return Response.json({ items: [{ project_id: "supply", gs_workspace: "tjwater_next", status: "active" }],
total: 1, limit: 1000, offset: 0 });
}));
const provider = async () => "token";
await expect(findOwnedWorkbenchProject(provider)).rejects.toThrow("后端当前已就绪");
available = true;
await expect(findOwnedWorkbenchProject(provider)).resolves.toMatchObject({ project: { project_id: "supply" } });
});
it("keeps authorization failures distinct from service availability", async () => {
const fetchMock = vi.fn(async () => Response.json({ detail: "当前账号没有项目访问权限" }, { status: 403 }));
vi.stubGlobal("fetch", fetchMock);
await expect(fetchWorkbenchProjects(async () => "token")).rejects.toThrow("当前账号没有项目访问权限");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("caches complete authorized lists across repeated openings and refreshes explicitly or after expiry", async () => {
const clock = vi.spyOn(Date, "now").mockReturnValue(100);
const fetch = vi.fn(async () => Response.json({ items: [{ project_id: "supply", gs_workspace: "tjwater_next", status: "active" }], total: 1, limit: 1000, offset: 0 }));