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 { createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, parseGiftCommands, parseGiftItems, queueGiftDelivery, 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 mapTestWindow = { from: "2026-08-09T23:00:00Z", to: "2026-08-10T01:00:00Z" }; 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: [{ id: "scum-vehicle-1", vehicleId: "scum-vehicle-1", gameVehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", locked: true, position: { x: 400, y: 200, z: 0 }, freshness: { status: "fresh" } }], vehicleLocks: [{ id: "lock-1", scumVehicleId: "scum-vehicle-1", gameVehicleId: "veh-1", scumUserId: "scum-user-1", steamId: "76561198000000001", lockedAt: "2026-08-10T00:00:04Z", source: "platform.scum_vehicle_lock" }], trajectories: [ { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:03Z", source: "platform.scum_user_trajectory" }, { 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: "platform.scum_vehicle_trajectory" } ], 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 through plugin collections plus platform SCUM tables", async () => { const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 })); const scum = scumActions(); const data = await loadSCUMSurface({ pluginData: pluginDataActions({ list }), scum }, "gifts"); expect(list.mock.calls.map(([collection]) => collection)).toEqual([scumCollections.gifts, scumCollections.giftClaims, scumCollections.pendingGifts, scumCollections.giftDeliveries, scumCollections.timedGiftEvents, scumCollections.tradeGoods]); expect(scum.surface).toHaveBeenCalledTimes(1); expect(data.gifts[0]).toMatchObject({ collection: scumCollections.gifts, _recordKey: `${scumCollections.gifts}-1` }); }); it("reads player records from platform scum_user table only", async () => { const pluginData = pluginDataActions(); const scum = scumActions({ surface: async () => ({ users: { items: [{ gamePlayerId: "steam-1", steamId: "steam-1", displayName: "Mira", online: false, source: "platform.scum_user" }], count: 1 }, vehicles: { items: [], count: 0 }, userTrajectories: { items: [], count: 0 }, vehicleTrajectories: { items: [], count: 0 }, vehicleLocks: { items: [], count: 0 } }) }); const data = await loadSCUMSurface({ pluginData, scum }, "players"); expect(pluginData.list).not.toHaveBeenCalledWith("scum_users"); expect(scum.surface).toHaveBeenCalledTimes(1); expect(data.players[0]).toMatchObject({ gamePlayerId: "steam-1", displayName: "Mira", online: false }); expect(dataClientSource).not.toContain("scum-client-manager"); }); it("does not queue SCUM database reads from the plugin page", async () => { const dispatch = vi.fn>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } })); const scum = scumActions(); await expect(loadSCUMSurface({ pluginData: pluginDataActions(), scum, dispatch }, "live-map")).resolves.toMatchObject({ players: expect.any(Array), vehicles: expect.any(Array), trajectories: expect.any(Array) }); expect(dispatch).not.toHaveBeenCalled(); expect(scum.surface).toHaveBeenCalledTimes(1); expect(dataClientSource).not.toContain("remote.run.db.sqlite"); expect(dataClientSource).not.toContain("input.templateKey"); expect(dataClientSource).not.toContain("projectSCUMLoginLogs"); expect(dataClientSource).not.toContain("logs.query"); }); it("loads live-map page data without page-triggered SCUM database dispatch", async () => { const list = vi.fn(async (collection: string) => ({ items: [{ key: `${collection}-1`, value: { collection } }], count: 1 })); const dispatch = vi.fn>(async () => ({ status: "queued" })); await expect(loadSCUMSurface({ pluginData: pluginDataActions({ list }), scum: scumActions(), dispatch }, "live-map")).resolves.toMatchObject({ players: expect.any(Array), vehicles: expect.any(Array) }); expect(dispatch).not.toHaveBeenCalled(); }); 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 }), scum: scumActions() }, "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 }), scum: scumActions() }, "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(pageSource).not.toContain('open("trajectory")'); expect(pageSource).toContain("/live-map?${query.toString()}"); 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("keeps user management off SCUM database SQL execution paths", () => { const source = `${pageSource}\n${dataClientSource}`; expect(source).toContain("平台 scum_user_trajectory 与 scum_vehicle_trajectory 表"); expect(source).toContain("平台 SCUM 用户与载具表"); expect(source).not.toContain("remote.run.db.sqlite.execute"); expect(source).not.toContain("scum-database"); expect(source).not.toContain("input.sqlText"); }); 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.nodes).toContain("div:SCUM 地图视图"); expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间"])); expect(view.inputs.map((input) => input.label)).not.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-terrain-4096.webp", import.meta.url).href'); expect(view.elements.find((element) => element.label === "SCUM 地图图层")?.style?.backgroundImage).toContain("scum-map-terrain-4096.webp"); expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["+", "-", "复位", "适应轨迹"])); const vehicleView = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: { ...surfaceData, players: [], mapPoints: [], flags: [], mapRegions: [] } }); expect(vehicleView.texts).toEqual(expect.arrayContaining(["BPC_Laika_C · unknown · 已上锁 · 访问 unknown", "scum-user-1", "76561198000000001"])); }); it("keeps the live map full width with one compact filter row and dropdown menus", () => { const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图" }); expect(view.texts).not.toContain("载具目录"); expect(view.texts.some((text) => /\d+ \/ \d+ 个点 · \d+ 条轨迹/.test(text))).toBe(true); for (const layer of ["地形", "等高线", "夜间", "建筑区"]) expect(view.texts).toContain(layer); expect(view.buttons.map((button) => button.label)).not.toEqual(expect.arrayContaining(["地形", "等高线", "夜间"])); expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["轨迹快捷时间", "轨迹开始时间", "轨迹结束时间", "地图底图", "轨迹回放进度"])); expect(view.inputs.map((input) => input.label)).not.toContain("启用自定义地图"); expect(view.texts).not.toContain("地图范围与图层"); expect(view.texts).not.toContain("自定义范围"); expect(view.elements.map((element) => element.label)).toEqual(expect.arrayContaining(["轨迹用户筛选", "地图图层筛选"])); expect(view.texts).not.toContain("轨迹列表"); expect(view.elements.map((element) => element.label)).toContain("轨迹颜色图例"); expect(view.buttons.map((button) => button.label)).toEqual(expect.arrayContaining(["全选", "清空", "不限", "回放", "回到最新"])); expect(pageSource).toContain('new URL("../assets/map/scum-map-building-zones-4096.png", import.meta.url).href'); expect(pageSource).toContain("mapBoardStyle(baseLayer, zonesOverlay)"); }); it("draws ridden-vehicle trajectory segments with vehicle icons and per-user selection", () => { const riding: SCUMSurfaceData = { ...surfaceData, trajectories: [ { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, sampledAt: "2026-08-10T00:00:01Z" }, { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 60, y: 80, z: 3, sampledAt: "2026-08-10T00:00:02Z" }, { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 120, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:03Z" }, { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 180, y: 140, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:04Z" } ] }; const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: riding, pageQuery: mapTestWindow }); expect(view.nodes).toContain("polyline:"); expect(view.texts).toEqual(expect.arrayContaining(["用户 · 4 点 · 步行 2 / 乘车 2"])); expect(view.elements.filter((element) => element.className.includes("map-ride-marker")).length).toBeGreaterThan(0); expect(view.elements.filter((element) => element.className.includes("map-track-endpoint")).length).toBeGreaterThan(1); expect(view.texts).toContain("Mira · 4 点"); }); it("keeps the extracted icon catalog closed until an editor asks for it", () => { const gifts = renderAndCollect({ pageKey: "gifts", pageTitle: "礼包管理" }); expect(gifts.texts).toContain("从 SCUM 目录选择礼包物品"); expect(gifts.inputs.map((input) => input.label)).not.toContain("搜索礼包物品"); const workflows = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理" }); expect(workflows.texts).toContain("从 SCUM 目录选择物品"); expect(workflows.inputs.map((input) => input.label)).not.toContain("搜索生成物品"); }); it("focuses the live map on the user who was picked from the user list", () => { const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", pageQuery: { focus: "76561198000000001", from: "2026-08-09T23:00:00Z", to: "2026-08-10T01:00:00Z" } }); expect(view.texts).toContain("用户 1/1"); expect(view.inputs.find((input) => input.label === "轨迹开始时间")?.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); expect(view.inputs.find((input) => input.label === "轨迹结束时间")?.value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/); expect(pageSource).toContain('"lastSeenAt", "lastLoginTime", "lastLoginAt", "lastLoginObservedAt", "onlineObservedAt", "updatedAt"'); expect(pageSource).toContain("scumMapDefaultWindowMs"); }); it("keeps only the selected user's tracks and the vehicles that user rode", () => { const data: SCUMSurfaceData = { ...surfaceData, trajectories: [ { subjectType: "player", subjectId: "76561198000000001", steamId: "76561198000000001", displayName: "Mira", x: 10, y: 20, z: 3, riddenVehicleId: "veh-1", gameVehicleId: "veh-1", sampledAt: "2026-08-10T00:00:01Z" }, { subjectType: "player", subjectId: "76561198000000009", steamId: "76561198000000009", displayName: "Nova", x: 30, y: 40, z: 3, sampledAt: "2026-08-10T00:00:02Z" }, { subjectType: "vehicle", subjectId: "veh-1", vehicleId: "veh-1", gameVehicleId: "veh-1", label: "Laika", className: "BPC_Laika_C", x: 400, y: 200, z: 0, sampledAt: "2026-08-10T00:00:03Z" }, { subjectType: "vehicle", subjectId: "veh-2", vehicleId: "veh-2", gameVehicleId: "veh-2", label: "WolfsWagen", className: "BPC_WolfsWagen_C", x: 500, y: 300, z: 0, sampledAt: "2026-08-10T00:00:04Z" } ] }; const view = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data, pageQuery: { ...mapTestWindow, focus: "76561198000000001" } }); expect(view.texts).toContain("Mira"); expect(view.texts).toContain("Laika"); expect(view.texts).not.toContain("Nova"); expect(view.texts).not.toContain("WolfsWagen"); const all = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data, pageQuery: mapTestWindow }); expect(all.texts).toEqual(expect.arrayContaining(["Mira", "Nova", "Laika", "WolfsWagen"])); }); it("loads the vehicle icon for trajectory rows and gift items", () => { expect(pageSource).toContain("trajectoryVehicleIcon(middle.row)"); expect(pageSource).toContain("catalogIconStrip(e, giftCatalogIcons(gift))"); }); 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.gameVehicleId === "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("remote.run.db.sqlite"); expect(source).not.toContain(`${"SCUM"}.db`); expect(source).not.toContain("projectSCUMLoginLogs"); expect(source).not.toContain("logs.query"); expect(source).not.toContain("requestSCUMPageQueries"); expect(pageSource).toContain("scumSurfaceRefreshMs = 15000"); expect(pageSource).toContain("setInterval(refresh, scumSurfaceRefreshMs)"); 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 scumActions(overrides: Partial> = {}): NonNullable { return { surface: vi.fn(overrides.surface ?? (async () => ({ users: { items: surfaceData.players, count: surfaceData.players.length }, vehicles: { items: surfaceData.vehicles, count: surfaceData.vehicles.length }, userTrajectories: { items: surfaceData.trajectories.filter((row) => row.subjectType === "player"), count: 1 }, vehicleTrajectories: { items: surfaceData.trajectories.filter((row) => row.subjectType === "vehicle"), count: 1 }, vehicleLocks: { items: surfaceData.vehicleLocks, count: surfaceData.vehicleLocks.length } }))) }; } function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; playerSearch?: string; pageQuery?: Record } = {}) { 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; className: 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"] ?? ""), className: String(props?.className ?? ""), 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(), scum: scumActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) }; renderPluginPage(react, { page: { key: options.pageKey ?? "players", title: options.pageTitle ?? "用户管理" }, context: { pluginId: "game.scum", serverInstanceId: "server-1", pageQuery: options.pageQuery ?? {}, 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 }; }