import { describe, expect, it, vi } 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 { buildPlayerAttributeMutation, createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, mergePlayerSnapshots, parseGiftCommands, parseGiftItems, playerAttributeDrafts, playerAttributeSqlPreview, projectSCUMLoginLogs, queueGiftDelivery, queuePlayerAttributePatch, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js"; import { collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.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 featureRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../examples/scum-server-plugin/features"); const pageSource = readFileSync(resolve(featureRoot, "page.ts"), "utf8"); const dataClientSource = readFileSync(resolve(featureRoot, "page-data.ts"), "utf8"); const surfaceData: SCUMSurfaceData = { players: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", userProfileId: "profile-1", displayName: "Mira", squadName: "Wolves", squadId: "squad-1", online: true, famePoints: 42, normalBalance: 1000, goldBalance: 3, lastLoginIp: "203.0.113.7", position: { x: 10, y: 20, z: 3 }, freshness: { status: "fresh" } }], squads: [{ squadId: "squad-1", name: "Wolves", memberCount: 1, memberLimit: 12, leaderProfileId: "profile-1", score: 88, message: "Hold the north", freshness: { status: "fresh" } }], members: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", displayName: "Mira", squadId: "squad-1", rank: "Leader", score: 42, lastLoginAt: "2026-08-10T00:00:00Z", freshness: { status: "fresh" } }], events: [{ id: "event-1", name: "Friday Range", eventType: "range", class: 1, corn: "0 20 * * 5", rconCommand: "#start_event event-1", placard: "Event starting", percent: 75, npc: 1, item: 3, zombie: 12, animal: 2, status: "enabled" }], eventProduces: [{ _recordKey: "event-1:produce-1", id: "produce-1", eventId: "event-1", tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 }], eventRuns: [{ id: "run-1", eventId: "event-1", status: "running", startedAt: "2026-08-10T00:00:00Z", summary: "Round 1" }], nativeEventRounds: [{ eventRecordId: "native-1", eventId: "native-event", state: "active", startTime: "2026-08-10T00:00:00Z", enemyKills: 2 }], tasks: [{ taskRecordId: "task-1", taskKind: "active-task", state: "active", userProfileId: "profile-1" }], activityEvents: [{ id: "activity-1", type: "reward", subjectName: "Mira", status: "delivered", occurredAt: "2026-08-10T00:02:00Z" }], tradeGoods: [ { code: "goods-1", name: "Cargo Drop", catalogType: "item", lastSeenAt: "2026-08-10T00:01:00Z" }, { code: "#spawnvehicle BPC_Laika_C", className: "BPC_Laika_C", catalogType: "vehicle", type: "21", typeName: "其他载具", imagePath: "/original/BPC_Laika_C.webp", lastSeenAt: "2026-08-10T00:00:03Z" } ], tradeEvents: [{ steamId: "76561198000000001", itemCode: "goods-1", tradeVerb: "purchased", quantity: "2", price: "120", observedAt: "2026-08-10T00:01:00Z" }], gifts: [{ code: "starter-pack", name: "Starter Pack", class: 5, audience: "all", number: 1, achievement: 2, achievementNumber: 10, status: "active", items: [{ catalogCode: "BP_Cash_01", quantity: 2 }], commands: [{ command: "#announce Starter pack" }] }], giftClaims: [{ id: "claim-1", playerId: "steam-1", giftCode: "starter-pack", status: "claimed", claimedAt: "2026-08-10T00:03:00Z" }], pendingGifts: [{ id: "pending-1", playerId: "steam-1", giftCode: "starter-pack", status: "pending", createdAt: "2026-08-10T00:03:30Z" }], giftDeliveries: [{ id: "delivery-1", playerId: "steam-1", giftCode: "starter-pack", status: "delivered", deliveredAt: "2026-08-10T00:04:00Z" }], timedGiftEvents: [{ timedGiftId: "timed-1", userProfileId: "profile-1", mapId: "map-1", spawnTime: 1, spawnAt: "2026-08-10T00:05:00Z" }], mapPoints: [{ id: "poi-1", name: "Airfield", layer: "other", x: 800, y: 900, z: 10, source: "plugin-map" }], mapRegions: [{ id: "region-1", name: "Safe Zone", x: 500, y: 600, z: 0, source: "server-config" }], mapSettings: [], vehicles: [{ vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", position: { x: 400, y: 200, z: 0 }, freshness: { status: "fresh" } }], trajectories: [ { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.positions" }, { subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z", source: "run.sqlite.scum.vehicles" } ], flags: [{ flagId: "flag-1", name: "Wolves Flag", ownerSquadId: "squad-1", ownershipConfidence: "verified", position: { x: 100, y: 80, z: 0 }, freshness: { status: "fresh" } }] }; describe("SCUM plugin feature module", () => { it("owns runtime catalogs 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 }])).toBeNull(); 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 declared 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 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("loads page data only through scoped plugin collections", async () => { const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 })); const data = await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "gifts"); expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents, scumCollections.players, scumCollections.tradeGoods]); expect(data.gifts[0]).toMatchObject({ collection: scumCollections.gifts, _recordKey: `${scumCollections.gifts}-1` }); }); it("reads player records only from plugin-owned collections", async () => { const pluginData = pluginDataActions({ list: async (collection) => collection === scumCollections.players ? { items: [{ key: "steam-1", value: { gamePlayerId: "steam-1", displayName: "Mira", online: false } }] } : { items: [] } }); const data = await loadSCUMSurface({ pluginData }, "players"); expect(data.players[0]).toMatchObject({ gamePlayerId: "steam-1", displayName: "Mira", online: false }); const sameName = mergePlayerSnapshots([{ steamId: "steam-2", displayName: "Noah", online: false }], { items: [{ observedAt: "2026-08-10T00:02:00Z", payload: { players: [{ playerId: "steam-3", playerName: "Noah", status: "online" }] } }] }); expect(sameName).toHaveLength(2); expect(sameName.find((player) => player.steamId === "steam-2")).toMatchObject({ online: false }); expect(dataClientSource).not.toContain("scum-client-manager"); }); it("projects SCUM login log lines inside the plugin-owned page module", async () => { const pluginData = pluginDataActions({ list: async (collection, key) => { if (collection === scumCollections.players) return { items: [{ key: "76561198000000001", value: { steamId: "76561198000000001", displayName: "Old", normalBalance: 100 } }] }; if (collection === scumCollections.logCursors && key) return { items: [{ key, value: { nextSeq: 1 } }] }; return { items: [], count: 0 }; } }); const logs = { listStreams: vi.fn(async () => ({ items: [{ id: "stream-login", streamKey: "scum.login", latestSeq: 3 }], count: 1 })), query: vi.fn(async () => ({ logStreamId: "stream-login", entries: [ { seq: 2, timestamp: "2026-08-10T00:01:00Z", line: "Login: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" }, { seq: 3, timestamp: "2026-08-10T00:04:00Z", line: "Logout: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" } ], nextSeq: 3, latestSeq: 3 })) }; await expect(projectSCUMLoginLogs({ pluginData, logs })).resolves.toBe(2); expect(logs.query).toHaveBeenCalledWith({ logStreamId: "stream-login", afterSeq: 1, limit: 200 }); expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.players, [expect.objectContaining({ key: "76561198000000001", value: expect.objectContaining({ displayName: "Mira", online: false, status: "offline", normalBalance: 100, lastLoginIp: "203.0.113.7", lastLogoutObservedAt: "2026-08-10T00:04:00Z", source: "plugin.log.scum.login" }) })]); expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.activityEvents, expect.arrayContaining([expect.objectContaining({ key: "stream-login:2", value: expect.objectContaining({ eventType: "login", rawLine: expect.stringContaining("Login:") }) }), expect.objectContaining({ key: "stream-login:3", value: expect.objectContaining({ eventType: "logout", rawLine: expect.stringContaining("Logout:") }) })])); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.logCursors, "scum.login:stream-login", expect.objectContaining({ nextSeq: 3 })); expect(dataClientSource).toContain("projectSCUMLoginLogs"); }); it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => { const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 })); await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "workflows"); expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents, scumCollections.tradeGoods, scumCollections.tradeEvents]); await loadSCUMSurface({ pluginData: pluginDataActions({ list }) }, "activity"); expect(list.mock.calls.slice(-8).map(([collection]) => collection)).toEqual([scumCollections.events, scumCollections.eventProduces, scumCollections.eventRuns, scumCollections.nativeEventRounds, scumCollections.tasks, scumCollections.activityEvents, scumCollections.tradeGoods, scumCollections.tradeEvents]); }); it("uses transaction, put, and delete for plugin-owned gift data", async () => { const pluginData = pluginDataActions(); const actions = { pluginData }; expect(parseGiftItems("BP_Cash_01:2, Water-Bottle.01:1")).toEqual([{ catalogCode: "BP_Cash_01", quantity: 2 }, { catalogCode: "Water-Bottle.01", quantity: 1 }]); expect(parseGiftCommands("#announce Hello\n#spawnitem BP_Cash_01 2")).toEqual([{ command: "#announce Hello" }, { command: "#spawnitem BP_Cash_01 2" }]); expect(() => parseGiftItems("cash:0")).toThrow("格式无效"); expect(parseGiftItems("a:101,b:1,c:1,d:1,e:1,f:1,g:1,h:1,i:1")).toHaveLength(9); await saveGiftDefinition(actions, { code: "starter", name: "Starter", items: [] }); await createGiftDelivery(actions, { id: "delivery-1", giftCode: "starter", playerId: "steam-1" }); await deleteGiftDefinition(actions, "starter"); expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.gifts, [{ operation: "put", key: "starter", value: { code: "starter", name: "Starter", items: [] } }]); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.giftDeliveries, "delivery-1", expect.objectContaining({ giftCode: "starter", playerId: "steam-1" })); expect(pluginData.delete).toHaveBeenCalledWith(scumCollections.gifts, "starter"); }); it("persists event produces, event runs, and gift resets in plugin-owned collections", async () => { const pluginData = pluginDataActions(); const dispatch = vi.fn>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } })); const actions: SCUMWorkspaceActions = { pluginData, dispatch }; await saveEventProduce(actions, { id: "produce-1", eventId: "event-1", tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 }); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.eventProduces, "event-1:produce-1", expect.objectContaining({ eventId: "event-1", tradeGoodsId: "goods-1" })); await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces); expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ action: "remote.access.request", payload: expect.objectContaining({ capability: "remote.run.rcon.command", "input.command": "#start_event event-1" }) })); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.eventRuns, expect.any(String), expect.objectContaining({ eventId: "event-1", status: "queued", produces: surfaceData.eventProduces })); await resetGiftClaim(actions, { _recordKey: "claim-1" }); await resetPendingGift(actions, { _recordKey: "pending-1", status: "received", receivedAt: "now" }); expect(pluginData.delete).toHaveBeenCalledWith(scumCollections.giftClaims, "claim-1"); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.pendingGifts, "pending-1", expect.objectContaining({ status: "pending", receivedAt: null })); }); it("queues gift and event commands through platform-mediated SCUM RCON", async () => { const pluginData = pluginDataActions(); const dispatch = vi.fn>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } })); const actions: SCUMWorkspaceActions = { pluginData, dispatch }; await queueGiftDelivery(actions, { ...surfaceData.gifts[0], operations: ["#announce Starter pack", "#SetFamePoints 250"] }, surfaceData.players[0]); expect(dispatch.mock.calls.map(([request]) => request.payload?.["input.command"])).toEqual(["#SpawnItem BP_Cash_01 2", "#announce Starter pack", "#SetFamePoints 250"]); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.giftDeliveries, expect.any(String), expect.objectContaining({ giftCode: "starter-pack", playerId: "steam-1", status: "queued" })); await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces); expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-1" }) })); }); it("defaults activity class to range and strips collection metadata from queued produces", async () => { const pluginData = pluginDataActions(); const dispatch = vi.fn>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } })); await startEvent({ pluginData, dispatch }, { id: "event-default", name: "Default Event", rconCommand: "#start_event event-default" }, [{ _recordKey: "event-default:produce-1", id: "produce-1", eventId: "event-default", updatedAt: "2026-08-10T00:00:00Z", tradeGoodsId: "cargo-drop", percent: 80, value: 2, r: 500, x: 1000, y: 2000, z: 300 }]); expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-default" }) })); }); it("keeps positive event duration and counts above the removed arbitrary limits", async () => { const dispatch = vi.fn>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } })); await startEvent({ pluginData: pluginDataActions(), dispatch }, { id: "event-large", name: "Large Event", rconCommand: "#start_event event-large", durationSeconds: 86401, npc: 10001, item: 10002, zombie: 10003, animal: 10004 }, [{ tradeGoodsId: "cargo-drop", percent: 80, value: 10001, r: 2000001, x: 3000000, y: -3000000, z: 0 }]); expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-large" }) })); }); it("renders searchable user management from real collection values", () => { const view = renderAndCollect(); expect(view.nodes).toContain("section:用户管理"); expect(view.texts).not.toContain("SCUM 用户真实记录"); expect(view.texts).toEqual(expect.arrayContaining(["在线用户 ", "1 / 1"])); expect(view.texts).toEqual(expect.arrayContaining(["用户名", "Steam ID", "渣币", "登录 IP", "队伍", "状态", "最后活动", "操作"])); expect(view.texts).not.toContain("筛选结果"); expect(view.texts).not.toContain("通用数据/机器动作可用"); expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["同步 SCUM.db", "重新读取"])); expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["编辑属性", "礼包状态", "他的物品", "登录历史", "用户轨迹"])); expect(view.inputs.map((input) => input.label)).toContain("搜索用户"); expect(pageSource).toContain("console-panel scum-workbench"); expect(pageSource).toContain("resource-filter-bar scum-filter-bar"); expect(pageSource).toContain("provider-table-wrap scum-table-wrap"); expect(view.texts).toContain("Mira"); expect(view.texts).toEqual(expect.arrayContaining(["76561198000000001", "1000", "203.0.113.7"])); expect(view.texts).not.toEqual(expect.arrayContaining(["UUID / Steam", "Gold 3"])); const ipSearch = renderAndCollect({ playerSearch: "203.0.113.7" }); expect(ipSearch.texts).toContain("Mira"); const uuidSearch = renderAndCollect({ playerSearch: "profile-1" }); expect(uuidSearch.texts).toContain("Mira"); }); it("expires online users after five minutes without activity", () => { const staleAt = new Date(Date.now() - 5 * 60 * 1000 - 1).toISOString(); const view = renderAndCollect({ data: { ...surfaceData, players: [{ ...surfaceData.players[0], lastSeenAt: staleAt }] } }); expect(view.texts).toEqual(expect.arrayContaining(["在线用户 ", "0 / 1"])); expect(view.texts).toContain("离线/未知"); }); it("prepares player attribute SQL execution through platform-to-Run remote access", async () => { const player = { steamId: "76561198000000001", displayName: "Mira", stateVersion: "state-1", stamina: 12, dexterity: 4, intelligence: 8 }; const drafts = playerAttributeDrafts(player).map((draft) => draft.fieldKey === "stamina" ? { ...draft, after: "855" } : draft); expect(playerAttributeSqlPreview(drafts)).toContain("UPDATE prisoner SET stamina = 855 WHERE id = :playerId;"); expect(buildPlayerAttributeMutation(player, drafts)).toMatchObject({ playerId: "76561198000000001", sqlText: expect.stringContaining("UPDATE prisoner SET stamina = 855"), changes: [{ fieldKey: "stamina", before: 12, after: 855 }] }); const actions = { dispatch: vi.fn>(async () => ({ status: "queued", result: { jobId: "job-1" } })) }; await queuePlayerAttributePatch(actions, player, drafts); expect(actions.dispatch).toHaveBeenCalledWith(expect.objectContaining({ action: "remote.access.request", payload: expect.objectContaining({ capability: "remote.run.db.sqlite.execute", declarationKey: "scum-database", "input.sqlText": expect.stringContaining("UPDATE prisoner SET stamina = 855") }) })); }); it("does not invent users when the collection is empty", () => { const view = renderAndCollect({ data: { ...surfaceData, players: [] } }); expect(view.texts.join("\n")).toContain("没有符合筛选条件的真实用户记录"); expect(view.texts).not.toContain("Mira"); expect(pageSource).not.toContain("samplePlayers"); expect(pageSource).not.toContain("fallbackFiles"); }); it("renders squad filtering, roster, and flag details", () => { const view = renderAndCollect({ pageKey: "squads", pageTitle: "队伍管理" }); expect(view.inputs.map((input) => input.label)).toContain("搜索队伍"); expect(view.texts).toContain("Wolves"); expect(view.texts).toContain("队伍成员"); expect(view.texts).toContain("Mira"); expect(view.texts.join("\n")).toContain("verified"); }); it("renders activity definitions, status filters, runs, and records", () => { const view = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理" }); expect(view.inputs.map((input) => input.label)).toContain("活动状态"); expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["生成类型", "RCON 命令", "活动公告", "活动概率", "活动持续秒数", "生成物品编号", "生成半径", "生成 X", "生成 Y", "生成 Z"])); expect(view.texts).toContain("Friday Range"); expect(view.texts).toContain("running"); expect(view.texts).toContain("最近活动记录"); expect(view.texts).toContain("Mira"); expect(view.texts).toContain("活动生成项"); expect(view.texts).toEqual(expect.arrayContaining(["物品列表", "最近商人交易", "Cargo Drop", "买入 × 2"])); expect(pageSource).toContain("setEventEditorOpen(detailOpen(event))"); expect(pageSource).not.toContain("open: Boolean(view.eventId || view.eventName)"); }); it("renders gift definitions, claims, and delivery records", () => { const definitions = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" }); expect(definitions.texts).toContain("礼包定义"); expect(definitions.texts).toContain("物品列表"); expect(definitions.texts).toContain("Starter Pack"); expect(definitions.texts).toContain("Cargo Drop"); expect(definitions.buttons.find((button) => button.label === "保存礼包")?.disabled).toBe(false); expect(definitions.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["礼包周期", "适用玩家", "发放次数", "成就类型", "成就值", "礼包物品", "礼包命令"])); const claims = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理", giftTab: "claims" }); expect(claims.texts).toContain("领取记录"); expect(claims.texts).toContain("claimed"); const deliveries = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理", giftTab: "deliveries" }); expect(deliveries.texts).toContain("发放记录"); expect(deliveries.texts).toContain("delivered"); expect(deliveries.buttons.find((button) => button.label === "立即发放")?.disabled).toBe(false); }); it("renders map layers, filter controls, points, and selected-point details", () => { const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" }); expect(view.nodes).toContain("div:SCUM 地图图层"); expect(view.inputs.map((input) => input.label)).toContain("筛选地图点"); for (const layer of ["用户", "载具", "旗帜", "区域", "其他"]) expect(view.texts).toContain(layer); expect(view.texts).toContain("地图点详情"); expect(view.texts).toContain("Airfield"); expect(view.texts.join("\n")).toContain("X 800 / Y 900 / Z 10"); expect(pageSource).toContain('new URL("../assets/map/scum-map-overview.jpg", import.meta.url).href'); expect(view.elements.find((element) => element.label === "SCUM 地图图层")?.style?.backgroundImage).toContain("scum-map-overview.jpg"); expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["启用自定义地图", "地图中心 X", "地图中心 Y", "地图宽度公里", "地图高度公里"])); }); it("deduplicates map entities, keeps every point, and computes custom map bounds", async () => { const duplicateData: SCUMSurfaceData = { ...surfaceData, mapPoints: [{ id: "direct-player", subjectType: "player", subjectId: "76561198000000001", name: "Mira", x: 10, y: 20, z: 3 }, ...Array.from({ length: 260 }, (_, index) => ({ id: `poi-${index}`, name: `POI ${index}`, layer: "other", x: index * 10, y: index * 10 }))] }; expect(collectMapPoints(duplicateData)).toHaveLength(264); expect(collectMapPoints(surfaceData).find((point) => point.vehicleId === "veh-1")).toMatchObject({ imagePath: "/original/BPC_Laika_C.webp", spawnCommand: "#spawnvehicle BPC_Laika_C" }); const bounds = resolveMapBounds({ customMapEnabled: true, centerX: 100000, centerY: 200000, widthKm: 4, heightKm: 2 }); expect(bounds).toEqual({ worldMinX: -100000, worldMinY: 100000, worldMaxX: 300000, worldMaxY: 300000 }); expect(mapPointStyle({ x: -100000, y: 100000 }, bounds)).toEqual({ left: "99%", top: "99%" }); const pluginData = pluginDataActions(); await saveMapSettings({ pluginData }, { customMapEnabled: true, centerX: 100000, centerY: 200000, widthKm: 4, heightKm: 2 }); expect(pluginData.put).toHaveBeenCalledWith(scumCollections.mapSettings, "current", expect.objectContaining(bounds)); expect(pageSource).toContain('textField(point, "imagePath", "image_path")'); expect(pageSource).toContain('"className", "vehicleClass", "entityClass", "vehicleType"'); expect(pageSource).not.toContain("visible.slice(0, 240)"); }); it("contains no specialized host callbacks, machine paths, or fake-data branches", () => { const source = `${pageSource}\n${dataClientSource}`; for (const forbidden of ["listSCUM", "gameGift", "createSCUMOperation", "createSCUMWorkflow", "C:/", "/Users/", "hostPath", "sampleCoordinates", "samplePlayers"]) expect(source).not.toContain(forbidden); expect(source).toContain("pluginData"); expect(source).toContain("remote.access.request"); expect(source).not.toContain("input.templateKey"); expect(source).not.toContain("requestSCUMPageQueries"); expect(pageSource).toContain("setInterval(refresh, 3000)"); expect(pageSource).toContain("clearInterval(interval)"); }); }); function pluginDataActions(overrides: Partial<{ list: (collection: string, key?: string) => Promise }> = {}) { return { list: vi.fn(overrides.list ?? (async () => ({ items: [], count: 0 }))), put: vi.fn(async () => ({})), delete: vi.fn(async () => undefined), transact: vi.fn(async () => ({ items: [], count: 0 })) }; } function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; playerSearch?: string } = {}) { const nodes: string[] = []; const texts: string[] = []; const buttons: Array<{ label: string; disabled: boolean; onClick?: () => void }> = []; const inputs: Array<{ label: string; value: unknown; onChange?: (event: unknown) => void }> = []; const elements: Array<{ label: string; style?: Record }> = []; 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 | null, ...children: unknown[]) => { if (typeof type === "string") nodes.push(`${type}:${String(props?.["aria-label"] ?? "")}`); if (typeof type === "string") elements.push({ label: String(props?.["aria-label"] ?? ""), style: props?.style as Record | undefined }); children.forEach(collectText); if (type === "button") buttons.push({ label: String(children[0]), disabled: Boolean(props?.disabled), onClick: props?.onClick as (() => void) | undefined }); if (type === "input" || type === "select" || type === "textarea") inputs.push({ label: String(props?.["aria-label"] ?? ""), value: props?.value ?? props?.checked, onChange: props?.onChange as ((event: unknown) => void) | undefined }); return { type, props, children }; }, useEffect: () => undefined, useState: (initial: T | (() => T)): [T, (next: T | ((previous: T) => T)) => void] => { stateCall += 1; if (stateCall === 1) return [{ status: "ready", data: options.data ?? surfaceData } as T, () => undefined]; if (stateCall === 3 && options.playerSearch !== undefined) return [options.playerSearch as T, () => undefined]; const value = typeof initial === "function" ? (initial as () => T)() : initial; if (options.giftTab && value === "definitions") return [options.giftTab as T, () => undefined]; return [value, () => undefined]; } }; const actions: SCUMWorkspaceActions = { pluginData: pluginDataActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) }; renderPluginPage(react, { page: { key: options.pageKey ?? "players", title: options.pageTitle ?? "用户管理" }, context: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.read", "server.remote.access"] }, availability: { available: true, features: [{ key: "player.intelligence", available: true }] }, workspaceActions: actions }); return { nodes, texts, buttons, inputs, elements, actions }; }