Files
browser/plugins/tests/scum-feature-module.test.ts
T

175 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { migrateConfigurationRecord, migrateGiftGrantRecord, migratePlayerProfileRecord, migratePlayerRecord, migrateStatePatchRecord, migrateTrajectoryHistoryRecord, migrateTrajectoryRecord, migrationStatus } from "../examples/scum-server-plugin/features/migration.js";
import { renderPluginPage } from "../examples/scum-server-plugin/page-bundle/index.js";
import { configurationCatalog, validateConfigPatch, validateStatePatch, validateVehicleSpawn, vehicleSpawnCatalog } from "../examples/scum-server-plugin/features/schemas.js";
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 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", () => {
it("owns runtime allowlists without a version gate", () => {
expect(configurationCatalog.map((field) => field.key)).toContain("welcome-message");
expect(validateConfigPatch({ reason: "adjust capacity", idempotencyKey: "cfg-1", changes: [{ key: "max-players", value: "129" }] })).toContain("超出允许范围");
expect(validateStatePatch([{ fieldKey: "skills.running", before: 1, after: 2 }])).toBeNull();
expect(validateStatePatch([{ fieldKey: "unknown", before: 1, after: 2 }])).toContain("白名单");
expect(vehicleSpawnCatalog.map((vehicle) => vehicle.code)).toEqual(["BPC_Laika_C", "BPC_WolfsWagen_C"]);
expect(validateVehicleSpawn({ vehicleCode: "BPC_Laika_C" })).toBeNull();
expect(validateVehicleSpawn({ vehicleCode: "#spawnvehicle BPC_Laika_C" })).toContain("格式无效");
expect(validateVehicleSpawn({ vehicleCode: "BPC_Unknown_C" })).toContain("受控目录");
});
it("maps transitional records only as read-only provenance", () => {
expect(migratePlayerRecord({ id: "p-1", gamePlayerId: "steam-1", displayName: "Mira", updatedAt: "2026-07-29T00:00:00Z" })).toMatchObject({ provenance: "transitional-read-only", readOnly: true, payload: { gamePlayerId: "steam-1" } });
expect(migrateTrajectoryRecord({ playerRecordId: "p-1", points: [{ recordedAt: "2026-07-29T00:00:00Z", mapX: 10, mapY: 20 }] })).toMatchObject({ provenance: "transitional-read-only", points: [{ x: 10, y: 20 }] });
});
it("preserves only allowlisted transitional history for every feature area", () => {
expect(migrateConfigurationRecord({ id: "cfg-1", version: "0.9.700.90357", fields: { MaxPlayers: 64 }, observedAt: "2026-07-29T00:00:00Z", hostPath: "C:/secret" })).toMatchObject({ readOnly: true, payload: { fields: { MaxPlayers: "64" } } });
expect(migratePlayerProfileRecord({ player: { id: "p-1", gamePlayerId: "steam-1", displayName: "Mira", updatedAt: "2026-07-29T00:00:00Z" }, sessions: [{ id: "s-1", gamePlayerRecordId: "p-1", startedAt: "2026-07-29T00:00:00Z", networkFingerprint: "never-copy" }], accessAttempts: [{ occurredAt: "2026-07-29T00:01:00Z", outcome: "review", reason: "manual" }] })).toMatchObject({ payload: { sessions: [{ kind: "login" }], risks: [{ summary: "manual" }] } });
expect(migrateGiftGrantRecord({ id: "gift-1", revisionId: "r-1", gamePlayerRecordId: "p-1", status: "unknown", createdAt: "2026-07-29T00:00:00Z" })).toMatchObject({ payload: { status: "unknown" }, readOnly: true });
expect(migrateStatePatchRecord({ id: "patch-1", gamePlayerRecordId: "p-1", expectedStateVersion: "state-1", safetyWindow: "maintenance", status: "confirmed", createdAt: "2026-07-29T00:00:00Z", changes: [{ fieldKey: "skills.running", before: 1, after: 2 }] })).toMatchObject({ payload: { status: "succeeded" }, readOnly: true });
expect(migrateTrajectoryHistoryRecord({ id: "track-1", playerRecordId: "p-1", points: [{ recordedAt: "2026-07-29T00:00:00Z", mapX: 10, mapY: 20 }] })).toMatchObject({ sourceRecordId: "track-1", readOnly: true });
});
it("matches controlled transitional fixtures without carrying sensitive fields into plugin history", () => {
expect(migrateConfigurationRecord(scumMigrationParityFixtures.configuration.source)).toEqual(scumMigrationParityFixtures.configuration.expected);
expect(migratePlayerProfileRecord(scumMigrationParityFixtures.playerHistory.source)).toEqual(scumMigrationParityFixtures.playerHistory.expected);
expect(migrateGiftGrantRecord(scumMigrationParityFixtures.gift.source)).toEqual(scumMigrationParityFixtures.gift.expected);
expect(migrateStatePatchRecord(scumMigrationParityFixtures.statePatch.source)).toEqual(scumMigrationParityFixtures.statePatch.expected);
expect(migrateTrajectoryHistoryRecord(scumMigrationParityFixtures.trajectory.source)).toEqual(scumMigrationParityFixtures.trajectory.expected);
expect(migrateConfigurationRecord({ version: "0.9.700.90357", observedAt: "2026-07-29T00:00:00Z", fields: { hostPath: "/srv/scum" } })).toBeNull();
});
it("enables plugin authority only for one exact server-feature flag", () => {
const flags = [{ serverInstanceId: "server-1", feature: "configuration" as const, authority: "plugin" as const }];
expect(migrationStatus(flags, "server-1", "configuration")).toMatchObject({ authority: "plugin", pluginWritesEnabled: true, readOnlyHistory: true });
expect(migrationStatus(flags, "server-2", "configuration")).toMatchObject({ authority: "transitional-read-only", pluginWritesEnabled: false });
expect(migrationStatus([...flags, flags[0]], "server-1", "configuration")).toMatchObject({ authority: "transitional-read-only", pluginWritesEnabled: false });
});
it("renders projection-backed user management without raw file/config panels", () => {
const view = renderAndCollect();
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 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("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 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("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: { data?: typeof projectionData; permissions?: string[]; pageKey?: string; pageTitle?: string } = {}) {
const nodes: string[] = [];
const texts: string[] = [];
const buttons: Array<{ label: string; disabled: boolean }> = [];
const collectText = (value: unknown): void => { if (typeof value === "string") texts.push(value); else if (Array.isArray(value)) value.forEach(collectText); else if (value && typeof value === "object" && "children" in value) collectText((value as { children?: unknown }).children); };
let stateCall = 0;
const react = {
createElement: (type: unknown, props: Record<string, unknown> | null, ...children: unknown[]) => {
if (typeof type === "string") nodes.push(`${type}:${String(props?.["aria-label"] ?? "")}`);
children.forEach(collectText);
if (type === "button") buttons.push({ label: String(children[0]), disabled: Boolean(props?.disabled) });
return { type, props, children };
},
useEffect: () => undefined,
useState: <T,>(initial: T | (() => T)): [T, (next: T | ((previous: T) => T)) => void] => {
stateCall += 1;
if (stateCall === 1) return [{ status: "ready", data: options.data ?? projectionData } as T, () => undefined];
return [typeof initial === "function" ? (initial as () => T)() : initial, () => undefined];
}
};
renderPluginPage(react, {
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: {
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 };
}