Integrate SCUM real ops workflows
This commit is contained in:
@@ -9,18 +9,16 @@ import { configurationCatalog, validateConfigPatch, validateStatePatch, validate
|
||||
import { scumMigrationParityFixtures } from "./fixtures/scum-migration-parity.js";
|
||||
|
||||
const pageSource = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../examples/scum-server-plugin/features/page.ts"), "utf8");
|
||||
const declaredWorkspace = {
|
||||
defaultDirectoryKey: "scum-config",
|
||||
directories: [
|
||||
{ key: "scum-config", label: "服务器配置", scope: "config" },
|
||||
{ key: "scum-logs", label: "日志文件", scope: "logs" }
|
||||
],
|
||||
files: [
|
||||
{ key: "scum-server-settings", directoryKey: "scum-config", label: "ServerSettings.ini", kind: "config", editable: true },
|
||||
{ key: "scum-game-config", directoryKey: "scum-config", label: "Game.ini", kind: "config" },
|
||||
{ key: "scum-admin-log", directoryKey: "scum-logs", label: "Admin.log", kind: "log", streamKey: "scum.admin" }
|
||||
],
|
||||
configFields: configurationCatalog
|
||||
const projectionData = {
|
||||
players: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", userProfileId: "profile-1", displayName: "Mira", squadName: "Wolves", online: true, famePoints: 42, normalBalance: 1000, goldBalance: 3, position: { x: 10, y: 20, z: 3, hasCoordinates: true }, freshness: { status: "fresh" }, unknownFields: { "855": 100 } }],
|
||||
squads: [{ squadId: "squad-1", name: "Wolves", memberCount: 3, leaderProfileId: "profile-1", freshness: { status: "fresh" } }],
|
||||
members: [{ gamePlayerId: "steam-1", displayName: "Mira", squadId: "squad-1", rank: "Leader", freshness: { status: "fresh" } }],
|
||||
vehicles: [{ vehicleId: "veh-1", label: "Laika", position: { subjectType: "vehicle", subjectId: "veh-1", x: 400, y: 200, z: 0, hasCoordinates: true }, freshness: { status: "fresh" } }],
|
||||
flags: [{ flagId: "flag-1", ownerSquadId: "squad-1", ownershipConfidence: "verified", position: { subjectType: "flag", subjectId: "flag-1", x: 100, y: 80, z: 0, hasCoordinates: true }, freshness: { status: "fresh" } }],
|
||||
positions: [{ subjectType: "player", subjectId: "steam-1", gamePlayerId: "steam-1", x: 10, y: 20, z: 3, hasCoordinates: true, freshness: { status: "fresh" } }],
|
||||
operations: [{ id: "op-1", templateKey: "player.fame.set", status: "waiting", safeSummary: { message: "awaiting approval" } }],
|
||||
workflows: [{ id: "wf-1", templateKey: "scum.world-refresh", status: "queued", currentStepKey: "read-positions", createdAt: "2026-08-10T00:00:00Z", safeSummary: { message: "world refresh queued" } }],
|
||||
steps: [{ stepKey: "read-positions", status: "queued", capability: "remote.run.db.sqlite.query", safeSummary: { message: "queued safely" } }]
|
||||
};
|
||||
|
||||
describe("SCUM plugin feature module", () => {
|
||||
@@ -64,64 +62,77 @@ describe("SCUM plugin feature module", () => {
|
||||
expect(migrationStatus([...flags, flags[0]], "server-1", "configuration")).toMatchObject({ authority: "transitional-read-only", pluginWritesEnabled: false });
|
||||
});
|
||||
|
||||
it("renders the compact two-level file management workbench without legacy stacked panels", () => {
|
||||
it("renders projection-backed user management without raw file/config panels", () => {
|
||||
const view = renderAndCollect();
|
||||
expect(view.nodes).toContain("section:SCUM 文件管理");
|
||||
expect(view.nodes).toContain("aside:SCUM 文件两级菜单");
|
||||
expect(view.nodes).toContain("article:文件 ServerSettings.ini");
|
||||
expect(view.texts.join("\n")).toContain("scum-config · 配置声明");
|
||||
expect(view.texts).toContain("刷新文件列表");
|
||||
expect(view.texts).toContain("ServerSettings.ini");
|
||||
expect(view.texts).toContain("Game.ini");
|
||||
expect(view.texts).toContain("配置表单");
|
||||
expect(view.texts).toContain("键值视图");
|
||||
expect(view.texts).toContain("原文模式");
|
||||
expect(view.buttons.find((button) => button.label === "刷新文件列表")?.disabled).toBe(false);
|
||||
expect(view.buttons.find((button) => button.label === "读取文件")?.disabled).toBe(false);
|
||||
expect(view.buttons.find((button) => button.label === "刷新结果")?.disabled).toBe(false);
|
||||
for (const legacyText of ["玩家档案", "礼物", "受控状态", "载具", "地图轨迹", "查询玩家"]) expect(view.texts.join("\n")).not.toContain(legacyText);
|
||||
expect(view.nodes).toContain("section:用户管理");
|
||||
expect(view.texts.join("\n")).toContain("登录日志和 SCUM.db typed observations");
|
||||
expect(view.texts).toContain("投影/Companion 可用");
|
||||
expect(view.texts).toContain("刷新投影");
|
||||
expect(view.texts).toContain("刷新真实数据");
|
||||
expect(view.texts).toContain("Mira");
|
||||
expect(view.texts.join("\n")).toContain("Steam 76561198000000001");
|
||||
expect(view.texts.join("\n")).toContain("Profile profile-1");
|
||||
expect(view.texts.join("\n")).toContain("Fame 42");
|
||||
expect(view.buttons.find((button) => button.label === "Fame +100")?.disabled).toBe(false);
|
||||
expect(view.buttons.find((button) => button.label === "现金 +1000")?.disabled).toBe(false);
|
||||
expect(view.buttons.find((button) => button.label === "855 审批")?.disabled).toBe(false);
|
||||
for (const removedText of ["ServerSettings.ini", "Game.ini", "配置表单", "键值视图", "原文模式", "读取文件", "提交写入"]) expect(view.texts.join("\n")).not.toContain(removedText);
|
||||
});
|
||||
|
||||
it("does not invent a hardcoded SCUM file list when the platform workspace is missing", () => {
|
||||
const view = renderAndCollect({ workspace: {} });
|
||||
expect(view.texts.join("\n")).toContain("当前插件没有可展示的声明文件。");
|
||||
expect(view.texts).not.toContain("ServerSettings.ini");
|
||||
expect(view.texts).not.toContain("Game.ini");
|
||||
it("does not invent fake players when projections are empty", () => {
|
||||
const view = renderAndCollect({ data: { ...projectionData, players: [], positions: [] } });
|
||||
expect(view.texts.join("\n")).toContain("暂无玩家投影");
|
||||
expect(view.texts.join("\n")).toContain("不会显示假玩家");
|
||||
expect(view.texts).not.toContain("Mira");
|
||||
expect(pageSource).not.toContain("fallbackFiles");
|
||||
expect(pageSource).not.toContain("samplePlayers");
|
||||
});
|
||||
|
||||
it("keeps declared log files in read-only raw view with encoding controls", () => {
|
||||
const view = renderAndCollect({ directoryKey: "scum-logs", fileKey: "scum-admin-log" });
|
||||
expect(view.nodes).toContain("article:文件 Admin.log");
|
||||
expect(view.texts).toContain("UTF-8");
|
||||
expect(view.texts).toContain("UTF-16 LE");
|
||||
expect(view.texts.join("\n")).toContain("尚未读取此日志文件的受控内容。");
|
||||
expect(view.nodes.some((node) => node.startsWith("textarea:"))).toBe(false);
|
||||
expect(view.texts).not.toContain("配置表单");
|
||||
expect(view.texts).not.toContain("提交写入");
|
||||
it("renders squad and flag governance from projections", () => {
|
||||
const view = renderAndCollect({ pageKey: "squads", pageTitle: "队伍管理" });
|
||||
expect(view.nodes).toContain("section:队伍管理");
|
||||
expect(view.texts).toContain("队伍");
|
||||
expect(view.texts).toContain("成员 / 旗帜");
|
||||
expect(view.texts).toContain("Wolves");
|
||||
expect(view.texts.join("\n")).toContain("成员 3");
|
||||
expect(view.texts.join("\n")).toContain("verified");
|
||||
});
|
||||
|
||||
it("renders current config values, unknown fields, encoding switch, and guarded write actions after a read", () => {
|
||||
const view = renderAndCollect({ snapshot: { serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "ready", content: "ServerName=Qinghuo\nMaxPlayers=96\nCustomKey=keep\n", version: 3, checksum: "sha256:cfg", sizeBytes: 48 } });
|
||||
expect(view.texts).toContain("UTF-8");
|
||||
expect(view.texts).toContain("UTF-16 LE");
|
||||
expect(view.texts).toContain("未建模配置项");
|
||||
expect(view.texts).toContain("CustomKey");
|
||||
expect(pageSource).toContain('e("option", { value: "true" }, "是")');
|
||||
expect(pageSource).toContain('type: "range"');
|
||||
expect(view.buttons.find((button) => button.label === "预览改动")?.disabled).toBe(true);
|
||||
expect(view.buttons.find((button) => button.label === "提交写入")?.disabled).toBe(true);
|
||||
it("renders realtime map overlays without sample coordinates", () => {
|
||||
const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" });
|
||||
expect(view.nodes).toContain("section:实时地图");
|
||||
expect(view.texts).toContain("地图覆盖物");
|
||||
expect(view.texts.join("\n")).toContain("坐标点");
|
||||
expect(view.texts.join("\n")).toContain("X 10 / Y 20 / Z 3");
|
||||
expect(pageSource).toContain("map-projection-board");
|
||||
expect(pageSource).not.toContain("sampleCoordinates");
|
||||
});
|
||||
|
||||
it("loads snapshots on selection or manual refresh and bounded post-request refresh", () => {
|
||||
expect(pageSource).toContain("getFileSnapshot(selectedFile.key)");
|
||||
expect(pageSource).toContain("刷新结果");
|
||||
expect(pageSource).toContain("loadFileSnapshot(selectedFile.key, true)");
|
||||
it("renders gift and workflow typed status surfaces", () => {
|
||||
const gifts = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" });
|
||||
expect(gifts.nodes).toContain("section:礼包管理");
|
||||
expect(gifts.texts.join("\n")).toContain("typed delivery workflow");
|
||||
expect(gifts.buttons.find((button) => button.label === "创建礼包发放")?.disabled).toBe(false);
|
||||
expect(gifts.buttons.find((button) => button.label === "发送通知")?.disabled).toBe(false);
|
||||
|
||||
const workflows = renderAndCollect({ pageKey: "workflows", pageTitle: "Workflow 状态" });
|
||||
expect(workflows.nodes).toContain("section:Workflow 状态");
|
||||
expect(workflows.texts.join("\n")).toContain("scum.world-refresh");
|
||||
expect(workflows.texts.join("\n")).toContain("read-positions");
|
||||
});
|
||||
|
||||
it("loads projections through typed workspace actions instead of file snapshots", () => {
|
||||
expect(pageSource).toContain("listSCUMPlayers");
|
||||
expect(pageSource).toContain("createSCUMOperation");
|
||||
expect(pageSource).toContain("createSCUMWorkflow");
|
||||
expect(pageSource).not.toContain("getFileSnapshot");
|
||||
expect(pageSource).not.toContain("requestFile");
|
||||
expect(pageSource).not.toContain("writeFile");
|
||||
expect(pageSource).not.toContain("setInterval");
|
||||
});
|
||||
});
|
||||
|
||||
function renderAndCollect(options: { snapshot?: Record<string, unknown>; permissions?: string[]; directoryKey?: string; fileKey?: string; workspace?: Record<string, unknown> } = {}) {
|
||||
function renderAndCollect(options: { data?: typeof projectionData; permissions?: string[]; pageKey?: string; pageTitle?: string } = {}) {
|
||||
const nodes: string[] = [];
|
||||
const texts: string[] = [];
|
||||
const buttons: Array<{ label: string; disabled: boolean }> = [];
|
||||
@@ -137,21 +148,26 @@ function renderAndCollect(options: { snapshot?: Record<string, unknown>; permiss
|
||||
useEffect: () => undefined,
|
||||
useState: <T,>(initial: T | (() => T)): [T, (next: T | ((previous: T) => T)) => void] => {
|
||||
stateCall += 1;
|
||||
if (stateCall === 1 && options.directoryKey) return [options.directoryKey as T, () => undefined];
|
||||
if (stateCall === 2 && options.fileKey) return [options.fileKey as T, () => undefined];
|
||||
if (stateCall === 8 && options.snapshot) return [options.snapshot as T, () => undefined];
|
||||
if (stateCall === 1) return [{ status: "ready", data: options.data ?? projectionData } as T, () => undefined];
|
||||
return [typeof initial === "function" ? (initial as () => T)() : initial, () => undefined];
|
||||
}
|
||||
};
|
||||
renderPluginPage(react, {
|
||||
context: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.files.read", "server.files.write", "server.logs.read"] },
|
||||
availability: { available: true, features: [{ key: "config.manage", available: true }] },
|
||||
workspace: options.workspace ?? declaredWorkspace,
|
||||
page: { key: options.pageKey ?? "players", title: options.pageTitle ?? "用户管理" },
|
||||
context: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"] },
|
||||
availability: { available: true, features: [{ key: "player.intelligence", available: true }] },
|
||||
workspaceActions: {
|
||||
refreshWorkspace: async () => declaredWorkspace,
|
||||
requestFile: async (fileKey: string) => ({ status: "queued", message: fileKey }),
|
||||
getFileSnapshot: async (fileKey: string) => ({ serverInstanceId: "server-1", pluginId: "game.scum", key: fileKey, state: "not-read" }),
|
||||
writeFile: async (fileKey: string) => ({ status: "queued", message: fileKey })
|
||||
listSCUMPlayers: async () => ({ items: projectionData.players, count: projectionData.players.length }),
|
||||
listSCUMSquads: async () => ({ items: projectionData.squads, count: projectionData.squads.length }),
|
||||
listSCUMSquadMembers: async () => ({ items: projectionData.members, count: projectionData.members.length }),
|
||||
listSCUMVehicles: async () => ({ items: projectionData.vehicles, count: projectionData.vehicles.length }),
|
||||
listSCUMFlags: async () => ({ items: projectionData.flags, count: projectionData.flags.length }),
|
||||
listSCUMPositions: async () => ({ items: projectionData.positions, count: projectionData.positions.length }),
|
||||
listSCUMOperations: async () => ({ items: projectionData.operations, count: projectionData.operations.length }),
|
||||
listSCUMWorkflows: async () => ({ items: projectionData.workflows, count: projectionData.workflows.length }),
|
||||
listSCUMWorkflowSteps: async () => ({ items: projectionData.steps, count: projectionData.steps.length }),
|
||||
createSCUMOperation: async () => ({ id: "op-new", status: "waiting" }),
|
||||
createSCUMWorkflow: async () => ({ id: "wf-new", status: "queued" })
|
||||
}
|
||||
});
|
||||
return { nodes, texts, buttons };
|
||||
|
||||
Reference in New Issue
Block a user