178 lines
11 KiB
TypeScript
178 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { configDiffViewFromPreview } from "./ServerDetailPage";
|
|
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
|
import clientManagerLifecyclePanelSource from "../components/ClientManagerLifecyclePanel.tsx?raw";
|
|
import runtimeDLLExtensionsPanelSource from "../components/RuntimeDLLExtensionsPanel.tsx?raw";
|
|
import sourceRCONCommandPanelSource from "../components/SourceRCONCommandPanel.tsx?raw";
|
|
import artifactTransferSource from "../utils/artifactTransfer.ts?raw";
|
|
import type { ServerConfigDiffPreviewResponse } from "../api/types";
|
|
|
|
const preview: ServerConfigDiffPreviewResponse = {
|
|
serverInstanceId: "server-1",
|
|
configVersion: 7,
|
|
key: "server.properties",
|
|
currentContent: "max-players=20\npvp=true\n",
|
|
proposedContent: "max-players=40\npvp=true\n",
|
|
diff: [
|
|
{ kind: "removed", oldNumber: 1, content: "max-players=20" },
|
|
{ kind: "added", newNumber: 1, content: "max-players=40" },
|
|
{ kind: "context", oldNumber: 2, newNumber: 2, content: "pvp=true" }
|
|
],
|
|
hasChanges: true,
|
|
source: "platform-review",
|
|
reviewedAt: "2026-07-06T00:00:00Z"
|
|
};
|
|
|
|
describe("ServerDetailPage config write approval", () => {
|
|
it("maps platform diff preview responses into the display diff without losing approval metadata", () => {
|
|
const view = configDiffViewFromPreview(preview);
|
|
|
|
expect(view).toMatchObject({
|
|
serverInstanceId: "server-1",
|
|
configVersion: 7,
|
|
key: "server.properties",
|
|
source: "platform-review",
|
|
summary: "+1 / -1 行变更",
|
|
nextContent: "max-players=40\npvp=true\n"
|
|
});
|
|
expect(view.lines).toEqual([
|
|
{ kind: "removed", text: "max-players=20" },
|
|
{ kind: "added", text: "max-players=40" },
|
|
{ kind: "same", text: "pvp=true" }
|
|
]);
|
|
});
|
|
|
|
it("uses config preview and approval APIs instead of generic config.write job creation", () => {
|
|
expect(serverDetailPageSource).toContain("previewServerConfigDiff");
|
|
expect(serverDetailPageSource).toContain("approveServerConfigWrite");
|
|
expect(serverDetailPageSource).not.toContain('capability: "config.write"');
|
|
});
|
|
|
|
it("uses typed server metadata APIs and keeps destructive deletion out of detail metadata", () => {
|
|
expect(serverDetailPageSource).toContain("updateServerInstance(instance.id");
|
|
expect(serverDetailPageSource).toContain("serverMetadataUpdateRequestFromForm");
|
|
expect(serverDetailPageSource).not.toContain("deleteServerInstance");
|
|
expect(serverDetailPageSource).not.toContain("serverDeleteConfirmation");
|
|
expect(serverDetailPageSource).not.toContain("canDeleteServer(instance.state)");
|
|
expect(serverDetailPageSource).not.toContain("请输入当前登录密码");
|
|
expect(serverDetailPageSource).not.toContain("fallbackConfig");
|
|
});
|
|
|
|
it("does not locally mutate visible config after dispatching approval jobs", () => {
|
|
expect(serverDetailPageSource).not.toContain("setCurrentConfig(suggestion.diff.nextContent)");
|
|
expect(serverDetailPageSource).not.toContain("content: diff.nextContent");
|
|
});
|
|
|
|
it("uses platform-mediated artifact download and bridge references without backend internals", () => {
|
|
expect(serverDetailPageSource).toContain("openArtifactDownload");
|
|
expect(serverDetailPageSource).toContain("readArtifactContent");
|
|
expect(serverDetailPageSource).toContain("parsePluginArtifactReference");
|
|
expect(serverDetailPageSource).toContain("浏览器制品传输");
|
|
expect(serverDetailPageSource).not.toContain("storage://");
|
|
expect(serverDetailPageSource).not.toContain("unix://");
|
|
expect(serverDetailPageSource).not.toContain("Bearer ");
|
|
});
|
|
|
|
it("surfaces run distribution and client-manager workflows through platform-mediated APIs", () => {
|
|
expect(serverDetailPageSource).toContain('params.routeKey !== "run-builder"');
|
|
expect(serverDetailPageSource).toContain('id="run-builder"');
|
|
expect(serverDetailPageSource).toContain("scrollIntoView");
|
|
expect(serverDetailPageSource).toContain("getServerRuntimeActions");
|
|
expect(serverDetailPageSource).toContain("generateRunDistribution");
|
|
expect(serverDetailPageSource).toContain("downloadLatestRunDistribution");
|
|
expect(serverDetailPageSource).toContain("pushRunUpdate");
|
|
expect(serverDetailPageSource).toContain("generateClientManager");
|
|
expect(serverDetailPageSource).toContain("resetClientManagerKey");
|
|
expect(serverDetailPageSource).toContain("checkDependencies");
|
|
expect(serverDetailPageSource).toContain("installDependencies");
|
|
expect(serverDetailPageSource).toContain("listServerLiveLogs");
|
|
expect(serverDetailPageSource).toContain("requestLogBackfill");
|
|
expect(serverDetailPageSource).toContain("ClientManagerLifecyclePanel");
|
|
expect(clientManagerLifecyclePanelSource).toContain("listClientManagerLifecycles");
|
|
expect(clientManagerLifecyclePanelSource).toContain("deployClientManager");
|
|
expect(clientManagerLifecyclePanelSource).toContain("controlClientManager");
|
|
expect(clientManagerLifecyclePanelSource).toContain("updateClientManager");
|
|
expect(clientManagerLifecyclePanelSource).toContain("retryClientManagerLifecycle");
|
|
expect(clientManagerLifecyclePanelSource).toContain("revokeClientManagerSession");
|
|
expect(clientManagerLifecyclePanelSource).toContain("uninstallClientManager");
|
|
expect(clientManagerLifecyclePanelSource).toContain("expectedDeploymentGeneration");
|
|
expect(clientManagerLifecyclePanelSource).not.toContain("secretRef");
|
|
expect(clientManagerLifecyclePanelSource).not.toContain("hostPath");
|
|
expect(clientManagerLifecyclePanelSource).not.toContain("process.pid");
|
|
expect(serverDetailPageSource).not.toContain("authKey");
|
|
expect(serverDetailPageSource).not.toContain("password=");
|
|
expect(serverDetailPageSource).not.toContain("unix://");
|
|
expect(serverDetailPageSource).not.toContain("tcp://");
|
|
expect(serverDetailPageSource).not.toContain("mysql://");
|
|
expect(serverDetailPageSource).not.toContain("sqlite://");
|
|
});
|
|
|
|
it("renders declared UE4SS DLL update policy without local paths or RCON secrets", () => {
|
|
expect(serverDetailPageSource).toContain("RuntimeDLLExtensionsPanel");
|
|
expect(runtimeDLLExtensionsPanelSource).toContain("启动前自动校验和更新");
|
|
expect(runtimeDLLExtensionsPanelSource).toContain("运行:SCUM 服务器受监管启动");
|
|
expect(runtimeDLLExtensionsPanelSource).toContain("由 UE4SS 正常加载");
|
|
expect(runtimeDLLExtensionsPanelSource).toContain("Linux 启动前拒绝");
|
|
for (const forbidden of ["dllRef", "targetKey", "rconPort", "password="]) {
|
|
expect(runtimeDLLExtensionsPanelSource).not.toContain(forbidden);
|
|
}
|
|
});
|
|
|
|
it("adds direct SCUM chat and raw commands through the one-time typed RCON API", () => {
|
|
expect(serverDetailPageSource).toContain("SourceRCONCommandPanel");
|
|
expect(sourceRCONCommandPanelSource).toContain("sendSourceRCONCommand");
|
|
expect(sourceRCONCommandPanelSource).toContain("不保留聊天或指令记录");
|
|
expect(sourceRCONCommandPanelSource).toContain("不会显示执行回包");
|
|
expect(sourceRCONCommandPanelSource).not.toContain("ConfirmDialog");
|
|
expect(sourceRCONCommandPanelSource).not.toContain("operations.");
|
|
for (const forbidden of ["password", "host", "transcript", "history"]) {
|
|
expect(sourceRCONCommandPanelSource).not.toContain(forbidden);
|
|
}
|
|
});
|
|
|
|
it("loads the dependency catalog only after runtime actions expose dependency operations", () => {
|
|
const runtimeDistributionSectionSource = serverDetailPageSource.split("function RuntimeDistributionSection")[1]?.split("function RuntimeBindingFields")[0] ?? "";
|
|
expect(runtimeDistributionSectionSource).toContain('action.key === "dependencies-check" || action.key === "dependencies-install"');
|
|
expect(runtimeDistributionSectionSource).toContain("dependencyActions.some((action) => action.available)");
|
|
expect(runtimeDistributionSectionSource).toContain("getDependencyCatalog(instance.id)");
|
|
});
|
|
|
|
it("reviews and updates only redacted runtime binding metadata", () => {
|
|
const runtimeBindingSectionSource = serverDetailPageSource.split("function RuntimeBindingSection")[1]?.split("function RuntimeDistributionSection")[0] ?? "";
|
|
expect(serverDetailPageSource).toContain("getServerRuntimeBinding");
|
|
expect(serverDetailPageSource).toContain("updateServerRuntimeBinding");
|
|
expect(runtimeBindingSectionSource).toContain("missingKeys");
|
|
expect(runtimeBindingSectionSource).toContain('type={field.sensitive ? "password" : "text"}');
|
|
for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://", "mysql://", "sqlite://"]) {
|
|
expect(runtimeBindingSectionSource).not.toContain(forbidden);
|
|
}
|
|
});
|
|
|
|
it("routes plugin lifecycle controls through platform lifecycle APIs instead of generic jobs", () => {
|
|
expect(serverDetailPageSource).toContain('action === "install" || action === "restart"');
|
|
expect(serverDetailPageSource).toContain('action !== "start" && action !== "stop" && action !== "status"');
|
|
expect(serverDetailPageSource).toContain('control.lifecycleAction === "start" || control.lifecycleAction === "stop" || control.lifecycleAction === "status"');
|
|
expect(serverDetailPageSource).toContain("platformApiClient.startServerInstance(instance.id");
|
|
expect(serverDetailPageSource).toContain("platformApiClient.stopServerInstance(instance.id");
|
|
expect(serverDetailPageSource).toContain("platformApiClient.queryServerProcessStatus(instance.id");
|
|
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(instance, \"start\")");
|
|
expect(serverDetailPageSource).toContain("serverLifecycleCommandRequest(instance, \"stop\")");
|
|
expect(serverDetailPageSource).not.toContain('capability: "process.start"');
|
|
expect(serverDetailPageSource).not.toContain('capability: "process.stop"');
|
|
});
|
|
|
|
it("keeps plugin lifecycle and bridge-visible output on platform-owned logical references", () => {
|
|
expect(serverDetailPageSource).toContain("parsePluginArtifactReference(result)");
|
|
expect(serverDetailPageSource).toContain("platformApiClient.openArtifactDownload(artifact.id)");
|
|
expect(serverDetailPageSource).toContain("downloadArtifactReference(reference");
|
|
expect(artifactTransferSource).toContain("readContent(reference.artifactId");
|
|
expect(artifactTransferSource).toContain("replace(/Bearer\\s+[^\\s]+/gi, \"[token]\")");
|
|
expect(artifactTransferSource).toContain("replace(/sk-[A-Za-z0-9_-]+/g, \"[secret]\")");
|
|
expect(serverDetailPageSource).not.toContain("storage://bucket");
|
|
expect(serverDetailPageSource).not.toContain("runSocket");
|
|
expect(serverDetailPageSource).not.toContain("rawApiKey");
|
|
expect(serverDetailPageSource).not.toContain("apiKeyRef");
|
|
});
|
|
});
|