Files
browser/platform_web/components/ScumOperationsPanel.test.tsx
T

102 lines
8.0 KiB
TypeScript

import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import type { GameClientBridgeSnapshotResponse } from "../api/types";
import type { ScumOperationsPageContract } from "../contracts/scumOperations";
import { ScumOperationsPanel, type ScumOperationsPanelData } from "./ScumOperationsPanel";
import scumOperationsPanelSource from "./ScumOperationsPanel.tsx?raw";
const now = "2026-07-20T08:00:00Z";
const contract: ScumOperationsPageContract = {
pluginId: "game.scum",
routeKey: "files-config",
serverInstanceId: "server-1",
title: "SCUM 运维",
permissions: ["server.read", "server.logs.read", "server.game-client.read", "server.game-client.command", "server.game-client.maintenance"],
bridgeActions: ["server.instances.read", "logs.query", "client-manager.request"],
commands: [
{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.json", timeoutSeconds: 30, maxPayloadBytes: 4096 },
{ type: "companion.diagnostics", title: "Collect diagnostics", permission: "server.game-client.read", approvalLevel: "none", payloadSchemaRef: "schemas/bridge/diagnostics.json", timeoutSeconds: 30, maxPayloadBytes: 2048 },
{ type: "restart.prepare", title: "Prepare restart", permission: "server.game-client.maintenance", approvalLevel: "platform-admin", payloadSchemaRef: "schemas/bridge/restart.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }
],
snapshots: ["companion.health", "online.sessions", "players", "squads", "vehicles", "flags"].map((type) => ({ type, schemaVersion: "1", schemaRef: `schemas/bridge/${type}.json`, keepForSeconds: 3600, maxRecords: 24 })),
queryTemplates: [],
logSources: [{ key: "scum-chat-events", kind: "file.tail", streamKey: "scum.chat", retentionDays: 30 }],
logEvents: [
{ key: "scum-chat", title: "SCUM chat", sourceKey: "scum-chat-events", eventType: "scum.chat", permission: "server.logs.read", schemaRef: "schemas/log-events/chat.json", retentionDays: 30, severity: "info" },
{ key: "scum-kill", title: "SCUM kill", sourceKey: "scum-chat-events", eventType: "scum.kill", permission: "server.logs.read", schemaRef: "schemas/log-events/kill.json", retentionDays: 30, severity: "warning" }
],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] }
};
function snapshot(type: string, payload: GameClientBridgeSnapshotResponse["payload"]): GameClientBridgeSnapshotResponse {
return { id: `snapshot-${type}`, serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client", type, schemaVersion: "1", streamKey: "current", sequence: 1, observedAt: now, payload, retention: { keepForSeconds: 3600, maxRecords: 24 }, createdAt: now, expiresAt: "2026-07-20T09:00:00Z" };
}
const data: ScumOperationsPanelData = {
status: { serverInstanceId: "server-1", pluginId: "game.scum", available: true, profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["announcement.send", "companion.diagnostics", "restart.prepare"], snapshotTypes: contract.snapshots.map((item) => item.type), queryTemplateKeys: [] }] },
commands: [{ id: "command-1", serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client", commandType: "announcement.send", priority: 50, state: "succeeded", approvalState: "approved", resultSummary: "announcement delivered", result: { status: "succeeded", summary: "announcement delivered", payload: { delivered: true }, completedAt: now }, expiresAt: now, createdAt: now, updatedAt: now, completedAt: now }],
snapshots: [
snapshot("companion.health", { status: "online", observedAt: now, version: "1.0.0", latencyMs: 20 }),
snapshot("online.sessions", { observedAt: now, onlineCount: 1, sessions: [{ sessionId: "game-session-1", playerName: "Moonlight" }] }),
snapshot("players", { observedAt: now, players: [{ playerId: "player-1", playerName: "Moonlight", status: "online" }] }),
snapshot("squads", { observedAt: now, squads: [{ squadId: "squad-1", name: "Lunar", memberCount: 4 }] }),
snapshot("vehicles", { observedAt: now, vehicles: [{ vehicleId: "vehicle-1", vehicleType: "truck", status: "parked" }] }),
snapshot("flags", { observedAt: now, flags: [{ flagId: "flag-1", status: "active" }] })
],
logs: [{ streamKey: "scum.chat", eventType: "scum.chat", entry: { seq: 1, timestamp: now, level: "info", line: "token=raw-secret /Users/operator/scum.log", redacted: true } }],
backups: [{ id: "backup-1", serverInstanceId: "server-1", artifactId: "artifact-1", checksum: "sha256:safe", sizeBytes: 2048, state: "available", recoveryStatus: "verified", retentionUntil: "2026-07-27T08:00:00Z", createdAt: now, updatedAt: now }],
errors: []
};
describe("ScumOperationsPanel", () => {
it("renders the first safe SCUM operations surface", () => {
const html = renderToStaticMarkup(<ScumOperationsPanel contract={contract} initialData={data} />);
for (const label of ["Companion", "命令队列与结果", "玩家与世界状态快照", "玩家 1", "会话 1", "小队 1", "载具 1", "旗帜 1", "语义日志", "scum.chat", "维护与备份策略", "backup-1", "已批准"]) {
expect(html).toContain(label);
}
expect(html).toContain("不触发自动封禁或惩罚");
expect(html).not.toContain("raw-secret");
expect(html).not.toContain("/Users/");
expect(html).not.toMatch(/sessionToken|componentKey|secretRef|hostPath|dsn|runSocket|credential/i);
});
it("keeps commands disabled with a visible bridge availability reason", () => {
const html = renderToStaticMarkup(<ScumOperationsPanel contract={contract} initialData={{ ...data, status: { ...data.status!, available: false, reason: "compatible companion is offline", profiles: data.status!.profiles.map((profile) => ({ ...profile, available: false, reason: "component heartbeat is unavailable" })) } }} />);
expect(html).toContain("compatible companion is offline");
expect(html).toContain("disabled");
const profileReasonHtml = renderToStaticMarkup(<ScumOperationsPanel contract={contract} initialData={{ ...data, status: { ...data.status!, available: true, reason: undefined, profiles: data.status!.profiles.map((profile) => ({ ...profile, available: false, reason: "component heartbeat is unavailable" })) } }} />);
expect(profileReasonHtml).toContain("component heartbeat is unavailable");
expect(profileReasonHtml).toContain("disabled");
});
it("renders every command approval state without leaking unsafe result details", () => {
const approvalStates = ["not_required", "pending", "approved", "rejected"] as const;
const unsafeSummary = "token=raw-command-secret /Users/operator/result.json unix:///var/run/scum.sock";
const commands = approvalStates.map((approvalState, index) => ({
...data.commands[0]!,
id: `command-${index + 1}`,
approvalState,
resultSummary: approvalState === "rejected" ? unsafeSummary : `approval ${approvalState}`,
result: approvalState === "rejected" ? { ...data.commands[0]!.result!, summary: unsafeSummary } : data.commands[0]!.result
}));
const html = renderToStaticMarkup(<ScumOperationsPanel contract={contract} initialData={{ ...data, commands }} />);
for (const label of ["无需审批", "待审批", "已批准", "已拒绝"]) {
expect(html).toContain(label);
}
expect(html).not.toContain("raw-command-secret");
expect(html).not.toContain("/Users/operator");
expect(html).not.toContain("unix:///var/run");
});
it("uses shared console surfaces without page-local ambient decoration", () => {
expect(scumOperationsPanelSource).toContain('className="console-panel"');
expect(scumOperationsPanelSource).toContain('className="resource-table-wrap"');
expect(scumOperationsPanelSource).not.toMatch(/position:\s*fixed|sparkle|snowflake|magic-circle|backdrop-layer/i);
expect(scumOperationsPanelSource).not.toContain("InsecureSkipVerify");
});
});