Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { Dashboard, SystemHealth, SystemResources } from "@umt/contracts";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { MemoryRouter } from "react-router";
|
|
import { describe, expect, it } from "vitest";
|
|
import { qk } from "~/lib/query-keys.ts";
|
|
import { HomeRuntime } from "./home-runtime.tsx";
|
|
|
|
// 首页运行状态简版卡:整体健康、CPU、内存、磁盘,以及下载器 / 站点 / 媒体库目录摘要,点进去是运行状态页。
|
|
|
|
const health = (over: Partial<SystemHealth> = {}): SystemHealth =>
|
|
({
|
|
level: "warn",
|
|
checkedAt: new Date().toISOString(),
|
|
api: { version: "t", startedAt: new Date().toISOString(), uptimeSec: 10 },
|
|
processes: [],
|
|
retiredProcesses: [],
|
|
sse: { connections: 0 },
|
|
db: { level: "ok", latencyMs: 1, sizeBytes: 1, connections: null, error: null },
|
|
disks: [
|
|
{
|
|
scope: "library_root",
|
|
id: "r1",
|
|
name: "电影",
|
|
path: "/media/movies",
|
|
level: "warn",
|
|
totalBytes: 1000,
|
|
usedBytes: 870,
|
|
availableBytes: 130,
|
|
usedRatio: 0.87,
|
|
error: null,
|
|
},
|
|
],
|
|
diskThresholds: { warnPercent: 85, criticalPercent: 90 },
|
|
mounts: [
|
|
{
|
|
kind: "library_root",
|
|
id: "r1",
|
|
name: "电影",
|
|
path: "/m",
|
|
status: "online",
|
|
reason: null,
|
|
sentinelChecked: true,
|
|
},
|
|
{
|
|
kind: "library_root",
|
|
id: "r2",
|
|
name: "剧集",
|
|
path: "/t",
|
|
status: "offline",
|
|
reason: "root_missing",
|
|
sentinelChecked: true,
|
|
},
|
|
],
|
|
engine: {
|
|
level: "ok",
|
|
pending: 0,
|
|
running: 0,
|
|
oldestPendingAgeSec: null,
|
|
finished24h: 0,
|
|
failed24h: 0,
|
|
failureRate24h: null,
|
|
},
|
|
schedules: { level: "ok", total: 0, enabled: 0, running: 0, failing: [], failedRuns24h: 0 },
|
|
storage: { level: "ok", alerts: [] },
|
|
...over,
|
|
}) as SystemHealth;
|
|
|
|
const resources: SystemResources = {
|
|
at: new Date().toISOString(),
|
|
intervalSec: 5,
|
|
bucketSec: 5,
|
|
cpu: { scope: "cgroup", percent: 42.4, cores: 4, hostPercent: 30, load: [1, 1, 1] },
|
|
memory: {
|
|
scope: "cgroup",
|
|
usedBytes: 900,
|
|
limitBytes: 1000,
|
|
percent: 92,
|
|
hostTotalBytes: 2000,
|
|
hostAvailableBytes: 800,
|
|
swapUsedBytes: 0,
|
|
},
|
|
network: { scope: "cgroup", rxBps: 1, txBps: 1, rxTotalBytes: 1, txTotalBytes: 1 },
|
|
diskIo: { scope: "cgroup", readBps: 1, writeBps: 1 },
|
|
hostUptimeSec: 100,
|
|
samples: [],
|
|
};
|
|
|
|
function render(h: SystemHealth, downloaders: Dashboard["sources"]["downloaders"]) {
|
|
const client = new QueryClient();
|
|
client.setQueryData(qk.sub("system", "health"), h);
|
|
client.setQueryData(qk.sub("system", "resources", 30), resources);
|
|
client.setQueryData([...qk.dashboard, "sites-accounts"], {
|
|
items: [
|
|
{ id: "s1", enabled: true, accountStatus: "ok" },
|
|
{ id: "s2", enabled: true, accountStatus: "expired" },
|
|
{ id: "s3", enabled: false, accountStatus: "expired" },
|
|
],
|
|
nextCursor: null,
|
|
});
|
|
return renderToStaticMarkup(
|
|
createElement(
|
|
QueryClientProvider,
|
|
{ client },
|
|
createElement(MemoryRouter, null, createElement(HomeRuntime, { downloaders })),
|
|
),
|
|
);
|
|
}
|
|
|
|
describe("首页运行状态卡", () => {
|
|
it("整体健康、CPU / 内存、磁盘、下载器 / 站点 / 媒体库目录摘要,链到运行状态页", () => {
|
|
const html = render(health(), [
|
|
{ id: "d1", name: "qb", enabled: true, health: "healthy", lastSeenAt: null },
|
|
{ id: "d2", name: "qb2", enabled: true, health: "offline", lastSeenAt: null },
|
|
] as Dashboard["sources"]["downloaders"]);
|
|
expect(html).toContain("运行状态");
|
|
expect(html).toContain('href="/console/status"');
|
|
expect(html).toContain("需要关注");
|
|
expect(html).toContain("42%");
|
|
// 内存 ≥ 90% 标红
|
|
expect(html).toMatch(/text-destructive[^"]*">92%/);
|
|
expect(html).toContain("电影");
|
|
expect(html).toContain("87%");
|
|
// 摘要:正常数 / 总数(停用的不算)
|
|
expect(html).toMatch(/下载器<span[^>]*>1\/2/);
|
|
expect(html).toMatch(/站点<span[^>]*>1\/2/);
|
|
expect(html).toMatch(/媒体库目录<span[^>]*>1\/2/);
|
|
});
|
|
|
|
it("全部正常时写「全部正常」,没有配置的项不列", () => {
|
|
const html = render(health({ level: "ok", mounts: [] }), []);
|
|
expect(html).toContain("全部正常");
|
|
expect(html).not.toContain("媒体库目录<");
|
|
expect(html).not.toContain("下载器<span");
|
|
});
|
|
});
|