Remove SCUM sqlite user projections
This commit is contained in:
@@ -140,21 +140,19 @@ describe("plugin manifest validation", () => {
|
||||
|
||||
it("removes raw SQL command surfaces", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates: Array<{ key: string }> } };
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { commands: Array<{ type: string; payloadSchemaRef: string }>; queryTemplates?: Array<{ key: string }> } };
|
||||
expect(manifest.gameClientBridge.commands.some((command) => command.type === "diagnostic.ping")).toBe(false);
|
||||
expect(manifest.gameClientBridge.commands.map((command) => command.type)).not.toEqual(expect.arrayContaining(["config.read", "config.patch", "database.request", "management.rcon.request", "management.program.request"]));
|
||||
expect(manifest.gameClientBridge.queryTemplates.map((query) => query.key)).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
||||
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
|
||||
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(false);
|
||||
});
|
||||
|
||||
it("declares SCUM SQLite templates without platform projections", () => {
|
||||
it("does not declare SCUM direct database templates or bridge projections", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
|
||||
gameClientBridge: { lifecycleProjections?: unknown[]; queryTemplates: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: unknown[] }> };
|
||||
gameClientBridge: { lifecycleProjections?: unknown[]; queryTemplates?: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: unknown[] }> };
|
||||
};
|
||||
const users = manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.player.profile");
|
||||
expect(users).toMatchObject({ engine: "sqlite", transportKey: "scum-database", targetKey: "scum-database" });
|
||||
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
|
||||
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
|
||||
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
|
||||
});
|
||||
|
||||
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
|
||||
@@ -215,22 +213,28 @@ describe("plugin manifest validation", () => {
|
||||
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
|
||||
});
|
||||
|
||||
it("declares database and management transports for direct run jobs", () => {
|
||||
it("declares management transports without SCUM database access", () => {
|
||||
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
|
||||
remoteAccess?: { databaseEngines?: string[]; runCapabilities?: string[] };
|
||||
runtimeProfiles?: {
|
||||
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
|
||||
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
|
||||
dataTargets?: Array<{ key?: string; kind?: string }>;
|
||||
};
|
||||
};
|
||||
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
|
||||
expect(local?.capabilities).toContain("remote.run.rcon.command");
|
||||
expect(local?.transportKeys).toContain("scum-management");
|
||||
expect(local?.transportKeys).not.toContain("scum-database");
|
||||
expect(manifest.remoteAccess?.databaseEngines).toEqual([]);
|
||||
expect(manifest.remoteAccess?.runCapabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
|
||||
expect(manifest.runtimeProfiles?.dataTargets).toEqual([]);
|
||||
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ key: "scum-database", kind: "sqlite", capabilities: expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]) }),
|
||||
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.rcon.command"] }),
|
||||
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
||||
]));
|
||||
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.key === "scum-database" || profile.kind === "sqlite")).toBe(false);
|
||||
});
|
||||
|
||||
it("covers the SCUM 4.1 bridge and lifecycle declarations", () => {
|
||||
@@ -247,7 +251,7 @@ describe("plugin manifest validation", () => {
|
||||
maxPayloadBytes: number;
|
||||
}>;
|
||||
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
||||
queryTemplates: Array<{ key: string; projections?: unknown[] }>;
|
||||
queryTemplates?: Array<{ key: string; projections?: unknown[] }>;
|
||||
lifecycleProjections?: unknown[];
|
||||
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||
};
|
||||
@@ -280,10 +284,10 @@ describe("plugin manifest validation", () => {
|
||||
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
|
||||
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
|
||||
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
|
||||
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
|
||||
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
|
||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||
expect(manifest.gameClientBridge.pages.some((page) => page.queryTemplateKeys?.length)).toBe(false);
|
||||
expect(manifest.pages.map((page) => page.key)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||
expect(manifest.pages.map((page) => page.key)).not.toContain("files-config");
|
||||
expect(manifest.pages.find((page) => page.key === "players")?.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
|
||||
@@ -404,108 +408,39 @@ describe("plugin manifest validation", () => {
|
||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "vehicles", "flags"]));
|
||||
});
|
||||
|
||||
it("declares typed SCUM.db query templates without browser-visible SQL", () => {
|
||||
it("keeps SCUM user and vehicle data off direct database query templates", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
permissions: string[];
|
||||
capabilities: string[];
|
||||
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[]; rcon?: boolean };
|
||||
gameClientBridge: {
|
||||
queryTemplates: Array<{
|
||||
key: string;
|
||||
title?: string;
|
||||
permission: string;
|
||||
engine: string;
|
||||
transportKey: string;
|
||||
targetKey: string;
|
||||
parameterSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
sqlRef: string;
|
||||
pollIntervalSeconds: number;
|
||||
maxRows: number;
|
||||
timeoutSeconds: number;
|
||||
projections?: unknown[];
|
||||
}>;
|
||||
queryTemplates?: Array<Record<string, unknown>>;
|
||||
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||
};
|
||||
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
|
||||
runtimeProfiles?: {
|
||||
transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }>;
|
||||
dataTargets?: Array<{ key: string; kind: string; transportKey: string; sourceRootKey: string; sourcePath: string; workspaceKey: string; refreshPolicy: string; maxBytes: number; platforms?: string[] }>;
|
||||
dataTargets?: Array<{ key: string; kind: string }>;
|
||||
};
|
||||
assetFiles?: Array<{ path: string }>;
|
||||
};
|
||||
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.tasks", "scum.events", "scum.native-timed-gifts"];
|
||||
const expectedColumnsByKey: Record<string, string[]> = {
|
||||
"scum.player.profile": ["userProfileId", "steamId", "gamePlayerId", "displayName", "lastLoginIp", "registeredAt", "squadId", "squadName", "famePoints", "moneyBalance", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastLogoutTime", "lastSaveTime"],
|
||||
"scum.squads": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
|
||||
"scum.squad-members": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
|
||||
"scum.vehicles": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional", "existsInGame", "mountedPrisonerIds", "mountedUserProfileIds", "mountedSteamIds"],
|
||||
"scum.flags": ["flagId", "entityId", "baseId", "ownerProfileId", "ownerPlayerId", "ownerSquadId", "ownerSquadName", "overtakerProfileId", "overtakeEndTime", "ownershipConfidence", "x", "y", "z"],
|
||||
"scum.positions": ["subjectType", "subjectId", "userProfileId", "gamePlayerId", "vehicleId", "entityId", "baseId", "x", "y", "z", "observedAt"],
|
||||
"scum.tasks": ["taskRecordId", "taskKind", "userProfileId", "mapId", "trackingDataSetId", "dataAssetPath", "sequenceIndex", "isTracked", "state", "completionDeadline"],
|
||||
"scum.events": ["eventRecordId", "eventId", "roundId", "userProfileId", "startTime", "endTime", "state", "score", "enemyKills", "teamKills", "deaths", "assists", "headshots"],
|
||||
"scum.native-timed-gifts": ["timedGiftId", "userProfileId", "mapId", "spawnTime", "spawnAt"]
|
||||
};
|
||||
const fastTemplates = new Set(["scum.player.profile", "scum.vehicles", "scum.positions"]);
|
||||
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
|
||||
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
|
||||
expect([...templatesByKey.values()].some((template) => Boolean(template.projections))).toBe(false);
|
||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
|
||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.execute");
|
||||
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
|
||||
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.execute");
|
||||
expect(manifest.remoteAccess?.databaseEngines).toContain("sqlite");
|
||||
expect(manifest.gameClientBridge.queryTemplates ?? []).toEqual([]);
|
||||
expect(manifest.capabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
|
||||
expect(manifest.remoteAccess?.runCapabilities).not.toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
|
||||
expect(manifest.remoteAccess?.databaseEngines).toEqual([]);
|
||||
expect(manifest.remoteAccess?.rcon).toBe(true);
|
||||
const sqliteTransport = manifest.runtimeProfiles?.transportProfiles?.find((profile) => profile.key === "scum-database");
|
||||
expect(sqliteTransport).toMatchObject({ kind: "sqlite", targetKey: "scum-database" });
|
||||
expect(sqliteTransport?.capabilities).toEqual(expect.arrayContaining(["remote.run.db.sqlite.query", "remote.run.db.sqlite.execute"]));
|
||||
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: "scum-database",
|
||||
kind: "sqlite.snapshot",
|
||||
transportKey: "scum-database",
|
||||
sourceRootKey: "server-root",
|
||||
sourcePath: "SCUM/Saved/SaveFiles/SCUM.db",
|
||||
workspaceKey: "databases/scum-database",
|
||||
refreshPolicy: "on-demand-snapshot",
|
||||
maxBytes: 1073741824,
|
||||
platforms: ["windows"]
|
||||
})
|
||||
]));
|
||||
for (const key of expectedKeys) {
|
||||
const template = templatesByKey.get(key)!;
|
||||
expect(template.engine).toBe("sqlite");
|
||||
expect(template.transportKey).toBe("scum-database");
|
||||
expect(template.targetKey).toBe("scum-database");
|
||||
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/.+\.sql$/);
|
||||
expect(template.pollIntervalSeconds).toBe(fastTemplates.has(key) ? 3 : 1800);
|
||||
expect(fs.existsSync(path.join(pluginDir, template.sqlRef))).toBe(true);
|
||||
expect(JSON.stringify(template).toLowerCase()).not.toMatch(/select\s|from\s|sqlite:|scum\.db|databasepath|hostpath|dsn/);
|
||||
const parameters = JSON.parse(fs.readFileSync(path.join(pluginDir, template.parameterSchemaRef), "utf8"));
|
||||
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, template.resultSchemaRef), "utf8"));
|
||||
const sql = fs.readFileSync(path.join(pluginDir, template.sqlRef), "utf8");
|
||||
const expectedColumns = expectedColumnsByKey[key];
|
||||
expect(parameters).toMatchObject({ type: "object", additionalProperties: false });
|
||||
expect(result).toMatchObject({ type: "object", additionalProperties: false, required: ["rows"] });
|
||||
expect(result.properties.rows.maxItems).toBeLessThanOrEqual(template.maxRows);
|
||||
expect(Object.keys(result.properties.rows.items.properties).sort()).toEqual([...expectedColumns].sort());
|
||||
expect([...result.properties.rows.items.required].sort()).toEqual([...expectedColumns].sort());
|
||||
for (const column of expectedColumns) {
|
||||
expect(sql).toMatch(new RegExp(`\\bAS\\s+${column}\\b`, "i"));
|
||||
}
|
||||
}
|
||||
const userSQL = fs.readFileSync(path.join(pluginDir, templatesByKey.get("scum.player.profile")!.sqlRef), "utf8");
|
||||
const positionSQL = fs.readFileSync(path.join(pluginDir, templatesByKey.get("scum.positions")!.sqlRef), "utf8");
|
||||
expect(userSQL).toMatch(/FROM user account\s+LEFT JOIN user_profile profile/i);
|
||||
expect(positionSQL).toMatch(/account\.id AS subjectId/i);
|
||||
expect(manifest.runtimeProfiles?.dataTargets).toEqual([]);
|
||||
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.kind === "sqlite" || profile.key === "scum-database")).toBe(false);
|
||||
expect((manifest.assetFiles ?? []).map((file) => file.path).some((assetPath) => assetPath.startsWith("sql/") || assetPath.includes("SCUM_DB_CONTRACT"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(pluginDir, "sql/scum-db-v57"))).toBe(false);
|
||||
const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players");
|
||||
const squadsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "squads");
|
||||
const mapPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map");
|
||||
const giftsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "gifts");
|
||||
const workflowsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows");
|
||||
expect(playersPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.positions"]));
|
||||
expect(squadsPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.squads", "scum.squad-members", "scum.flags"]));
|
||||
expect(mapPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.vehicles", "scum.flags", "scum.positions"]));
|
||||
expect(playersPage?.queryTemplateKeys).toBeUndefined();
|
||||
expect(squadsPage?.queryTemplateKeys).toBeUndefined();
|
||||
expect(mapPage?.queryTemplateKeys).toBeUndefined();
|
||||
expect(giftsPage?.commandTypes).toEqual(["reward.deliver"]);
|
||||
expect(workflowsPage?.commandTypes).toEqual(["event.start"]);
|
||||
for (const pageKey of ["players", "squads", "live-map", "gifts", "workflows"]) {
|
||||
@@ -519,12 +454,12 @@ describe("plugin manifest validation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("packages SCUM v57 config and gift metadata inside the plugin", () => {
|
||||
it("packages SCUM v57 config and gift metadata without database query assets", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; configMapRefs: string[]; dataRefs?: string[] }> };
|
||||
};
|
||||
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-db-v57");
|
||||
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-config-v57");
|
||||
expect(pack).toMatchObject({ databaseUserVersion: 57 });
|
||||
const configMaps = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.configMapRefs[0]), "utf8"));
|
||||
const giftMetadata = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![0]), "utf8"));
|
||||
|
||||
@@ -4,7 +4,7 @@ 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, queueGiftDelivery, queuePlayerAttributePatch, queueSCUMDatabaseRefresh, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.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";
|
||||
@@ -37,10 +37,11 @@ const surfaceData: SCUMSurfaceData = {
|
||||
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" } }],
|
||||
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: "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" }
|
||||
{ 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" } }]
|
||||
};
|
||||
@@ -86,49 +87,53 @@ describe("SCUM plugin feature module", () => {
|
||||
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 () => {
|
||||
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 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]);
|
||||
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.users).toHaveBeenCalledWith({ limit: 500 });
|
||||
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");
|
||||
it("reads player records from platform scum_user table only", async () => {
|
||||
const pluginData = pluginDataActions();
|
||||
const scum = scumActions({ users: async () => ({ items: [{ gamePlayerId: "steam-1", steamId: "steam-1", displayName: "Mira", online: false, source: "platform.scum_user" }], count: 1 }) });
|
||||
const data = await loadSCUMSurface({ pluginData, scum }, "players");
|
||||
expect(pluginData.list).not.toHaveBeenCalledWith("scum_users");
|
||||
expect(scum.users).toHaveBeenCalledWith({ limit: 500 });
|
||||
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("queues SCUM database reads through declared template keys instead of log parsing", async () => {
|
||||
it("does not queue SCUM database reads from the plugin page", async () => {
|
||||
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
|
||||
await expect(queueSCUMDatabaseRefresh({ dispatch }, "players", 5000)).resolves.toHaveLength(2);
|
||||
expect(dispatch.mock.calls.map(([envelope]) => envelope.payload?.["input.templateKey"])).toEqual(["scum.player.profile", "scum.positions"]);
|
||||
expect(dispatch.mock.calls.map(([envelope]) => envelope.payload)).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ capability: "remote.run.db.sqlite.query", declarationKey: "scum-database", targetKey: "scum-database", "input.templateKey": "scum.player.profile", "input.activeWithinSeconds": "600" }),
|
||||
expect.objectContaining({ capability: "remote.run.db.sqlite.query", declarationKey: "scum-database", targetKey: "scum-database", "input.templateKey": "scum.positions", "input.activeWithinSeconds": "600" })
|
||||
]));
|
||||
expect(dispatch.mock.calls[0]?.[0].payload).not.toHaveProperty("input.sqlText");
|
||||
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.users).toHaveBeenCalledWith({ limit: 500 });
|
||||
expect(scum.vehicles).toHaveBeenCalledWith({ limit: 500 });
|
||||
expect(scum.userTrajectories).toHaveBeenCalledWith({ limit: 500 });
|
||||
expect(scum.vehicleTrajectories).toHaveBeenCalledWith({ limit: 500 });
|
||||
expect(scum.vehicleLocks).toHaveBeenCalledWith({ limit: 500 });
|
||||
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("does not block page data reads on SCUM database refresh dispatch", async () => {
|
||||
const releaseDispatches: Array<(value: { status: "queued" }) => void> = [];
|
||||
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(() => new Promise((resolve) => { releaseDispatches.push(resolve); }));
|
||||
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 }));
|
||||
await expect(loadSCUMSurface({ pluginData: pluginDataActions({ list }), dispatch }, "live-map")).resolves.toMatchObject({ players: expect.any(Array), vehicles: expect.any(Array) });
|
||||
releaseDispatches.forEach((release) => release({ status: "queued" }));
|
||||
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(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 }) }, "workflows");
|
||||
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 }) }, "activity");
|
||||
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]);
|
||||
});
|
||||
|
||||
@@ -199,8 +204,8 @@ describe("SCUM plugin feature module", () => {
|
||||
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.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");
|
||||
@@ -221,14 +226,13 @@ describe("SCUM plugin feature module", () => {
|
||||
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<NonNullable<SCUMWorkspaceActions["dispatch"]>>(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("keeps user management off SCUM database SQL execution paths", () => {
|
||||
const source = `${pageSource}\n${dataClientSource}`;
|
||||
expect(source).toContain("平台 scum_user_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", () => {
|
||||
@@ -290,12 +294,14 @@ describe("SCUM plugin feature module", () => {
|
||||
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", "地图宽度公里", "地图高度公里"]));
|
||||
const vehicleView = renderAndCollect({ pageKey: "live-map", pageTitle: "实时地图", data: { ...surfaceData, players: [], mapPoints: [], flags: [], mapRegions: [] } });
|
||||
expect(vehicleView.texts).toEqual(expect.arrayContaining(["锁 已上锁", "scum-user-1", "76561198000000001"]));
|
||||
});
|
||||
|
||||
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" });
|
||||
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%" });
|
||||
@@ -312,7 +318,9 @@ describe("SCUM plugin feature module", () => {
|
||||
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).toContain("input.templateKey");
|
||||
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");
|
||||
@@ -331,6 +339,16 @@ function pluginDataActions(overrides: Partial<{ list: (collection: string, key?:
|
||||
};
|
||||
}
|
||||
|
||||
function scumActions(overrides: Partial<NonNullable<SCUMWorkspaceActions["scum"]>> = {}): NonNullable<SCUMWorkspaceActions["scum"]> {
|
||||
return {
|
||||
users: vi.fn(overrides.users ?? (async () => ({ items: surfaceData.players, count: surfaceData.players.length }))),
|
||||
vehicles: vi.fn(overrides.vehicles ?? (async () => ({ items: surfaceData.vehicles, count: surfaceData.vehicles.length }))),
|
||||
userTrajectories: vi.fn(overrides.userTrajectories ?? (async () => ({ items: surfaceData.trajectories.filter((row) => row.subjectType === "player"), count: 1 }))),
|
||||
vehicleTrajectories: vi.fn(overrides.vehicleTrajectories ?? (async () => ({ items: surfaceData.trajectories.filter((row) => row.subjectType === "vehicle"), count: 1 }))),
|
||||
vehicleLocks: vi.fn(overrides.vehicleLocks ?? (async () => ({ 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 } = {}) {
|
||||
const nodes: string[] = [];
|
||||
const texts: string[] = [];
|
||||
@@ -358,7 +376,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
|
||||
return [value, () => undefined];
|
||||
}
|
||||
};
|
||||
const actions: SCUMWorkspaceActions = { pluginData: pluginDataActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) };
|
||||
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: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.read", "server.remote.access"] },
|
||||
|
||||
Reference in New Issue
Block a user