feat(scum): rebuild plugin-owned management data
This commit is contained in:
@@ -1,13 +1,40 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import hostSource from "./PluginPageHostPage.tsx?raw";
|
||||
|
||||
import type { GamePluginResponse } from "../api/types";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import type { PluginPageWorkspaceActions } from "../contracts/pluginPageHost";
|
||||
import { capabilitiesForRoles } from "../contracts/workspace";
|
||||
import type { OperationTracker } from "../stores/operations";
|
||||
import { PluginPageHostPage } from "./PluginPageHostPage";
|
||||
|
||||
const apiMocks = vi.hoisted(() => ({
|
||||
deletePluginData: vi.fn(),
|
||||
executePluginBridge: vi.fn(),
|
||||
getGameClientBridgeCommand: vi.fn(),
|
||||
getGameClientBridgeStatus: vi.fn(),
|
||||
listGameClientBridgeCommands: vi.fn(),
|
||||
listGameClientBridgeSnapshots: vi.fn(),
|
||||
listGamePlugins: vi.fn(),
|
||||
listPluginData: vi.fn(),
|
||||
putPluginData: vi.fn(),
|
||||
queueGameClientBridgeCommand: vi.fn(),
|
||||
transactPluginData: vi.fn()
|
||||
}));
|
||||
|
||||
const bundleMocks = vi.hoisted(() => ({ loadPluginPageBundle: vi.fn() }));
|
||||
|
||||
vi.mock("../api/client", () => ({ platformApiClient: apiMocks }));
|
||||
vi.mock("../utils/pluginPageBundles", async (importOriginal) => ({
|
||||
...await importOriginal<typeof import("../utils/pluginPageBundles")>(),
|
||||
loadPluginPageBundle: bundleMocks.loadPluginPageBundle
|
||||
}));
|
||||
|
||||
const operations: OperationTracker = {
|
||||
operations: [],
|
||||
begin: () => "operation-test",
|
||||
@@ -37,7 +64,7 @@ const plugin: GamePluginResponse = {
|
||||
bundleKey: "scum-server-plugin",
|
||||
bundleVersion: "1.0.1",
|
||||
bundleIntegritySha256: "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
|
||||
permissions: ["server.game-client.read", "server.game-client.command"],
|
||||
permissions: ["server.read", "server.game-client.read", "server.game-client.command"],
|
||||
bridgeActions: ["server.instances.read"]
|
||||
}],
|
||||
tags: ["scum"],
|
||||
@@ -53,6 +80,38 @@ const plugin: GamePluginResponse = {
|
||||
status: "installed"
|
||||
};
|
||||
|
||||
let root: Root | null = null;
|
||||
let container: HTMLDivElement | null = null;
|
||||
let capturedWorkspaceActions: PluginPageWorkspaceActions | undefined;
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
beforeEach(() => {
|
||||
capturedWorkspaceActions = undefined;
|
||||
apiMocks.getGameClientBridgeStatus.mockResolvedValue({ available: true, features: [] });
|
||||
apiMocks.listPluginData.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.putPluginData.mockResolvedValue({ key: "user-1", value: { name: "Ada" } });
|
||||
apiMocks.deletePluginData.mockResolvedValue(undefined);
|
||||
apiMocks.transactPluginData.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.queueGameClientBridgeCommand.mockResolvedValue({ id: "command-1" });
|
||||
apiMocks.getGameClientBridgeCommand.mockResolvedValue({ id: "command-1" });
|
||||
apiMocks.listGameClientBridgeCommands.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.listGameClientBridgeSnapshots.mockResolvedValue({ items: [], count: 0 });
|
||||
apiMocks.executePluginBridge.mockResolvedValue({ requestId: "request-1", action: "server.instances.read", status: "ok", result: { state: "ready" } });
|
||||
bundleMocks.loadPluginPageBundle.mockResolvedValue(({ workspaceActions }: { workspaceActions?: PluginPageWorkspaceActions }) => {
|
||||
capturedWorkspaceActions = workspaceActions;
|
||||
return <div>mock plugin bundle</div>;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (root) await act(async () => root?.unmount());
|
||||
container?.remove();
|
||||
root = null;
|
||||
container = null;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function props(serverId = "server-1"): PageComponentProps {
|
||||
const session = {
|
||||
id: "operator-1",
|
||||
@@ -101,6 +160,11 @@ describe("PluginPageHostPage", () => {
|
||||
expect(hostSource).toContain("hostContextRef.current = hostContext");
|
||||
expect(hostSource).toContain("list: (collection, key) => platformApiClient.listPluginData(serverId, collection, key)");
|
||||
expect(hostSource).toContain("put: (collection, key, value) => platformApiClient.putPluginData(serverId, collection, key, value)");
|
||||
expect(hostSource).toContain("delete: (collection, key) => platformApiClient.deletePluginData(serverId, collection, key)");
|
||||
expect(hostSource).toContain("transact: (collection, mutations) => platformApiClient.transactPluginData(serverId, collection, mutations)");
|
||||
expect(hostSource).toContain("queue: (request) => platformApiClient.queueGameClientBridgeCommand(serverId, request)");
|
||||
expect(hostSource).toContain("snapshots: (query) => platformApiClient.listGameClientBridgeSnapshots(serverId, query)");
|
||||
expect(hostSource).toContain("createPluginBridgeDispatcher(context, platformApiClient)");
|
||||
expect(hostSource).not.toContain("listSCUMPlayers:");
|
||||
expect(hostSource).not.toContain("refreshWorkspace");
|
||||
expect(hostSource).not.toContain("requestFile");
|
||||
@@ -109,4 +173,62 @@ describe("PluginPageHostPage", () => {
|
||||
expect(hostSource).toContain("}, [pluginId, serverId]);");
|
||||
expect(hostSource).not.toContain("}, [hostContext, readyPlugin, serverId]);");
|
||||
});
|
||||
|
||||
it("passes every generic data, dispatch, and game-client operation through the hosted bundle", async () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(<PluginPageHostPage {...props()} initialPlugin={plugin} embedded />);
|
||||
});
|
||||
|
||||
const actions = capturedWorkspaceActions;
|
||||
expect(actions?.pluginData).toBeDefined();
|
||||
expect(actions?.gameClient).toBeDefined();
|
||||
expect(actions?.dispatch).toBeDefined();
|
||||
|
||||
const mutations = [
|
||||
{ operation: "put" as const, key: "user-1", value: { name: "Ada" } },
|
||||
{ operation: "delete" as const, key: "user-2" }
|
||||
];
|
||||
const queueRequest = {
|
||||
profileKey: "scum-client",
|
||||
commandType: "reward.deliver",
|
||||
payload: { steamId: "redacted", giftKey: "starter" },
|
||||
idempotencyKey: "gift:user-1:starter",
|
||||
expiresAt: "2026-08-15T12:00:00Z"
|
||||
};
|
||||
const commandFilter = { profileKey: "scum-client", state: "pending" as const, commandType: "reward.deliver" };
|
||||
const snapshotQuery = { profileKey: "scum-client", type: "scum.positions", streamKey: "current", limit: 50 };
|
||||
const dispatchEnvelope = { requestId: "request-1", action: "server.instances.read" as const, payload: { view: "summary" } };
|
||||
|
||||
await actions?.pluginData?.list("scum_users", "user-1");
|
||||
await actions?.pluginData?.put("scum_users", "user-1", { name: "Ada" });
|
||||
await actions?.pluginData?.delete("scum_users", "user-2");
|
||||
await actions?.pluginData?.transact("scum_users", mutations);
|
||||
await actions?.gameClient?.queue(queueRequest);
|
||||
await actions?.gameClient?.get("command-1");
|
||||
await actions?.gameClient?.list(commandFilter);
|
||||
await actions?.gameClient?.snapshots(snapshotQuery);
|
||||
await expect(actions?.dispatch?.(dispatchEnvelope)).resolves.toMatchObject({ status: "ok", result: { state: "ready" } });
|
||||
|
||||
expect(apiMocks.listPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1");
|
||||
expect(apiMocks.putPluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-1", { name: "Ada" });
|
||||
expect(apiMocks.deletePluginData).toHaveBeenCalledWith("server-1", "scum_users", "user-2");
|
||||
expect(apiMocks.transactPluginData).toHaveBeenCalledWith("server-1", "scum_users", mutations);
|
||||
expect(apiMocks.queueGameClientBridgeCommand).toHaveBeenCalledWith("server-1", queueRequest);
|
||||
expect(apiMocks.getGameClientBridgeCommand).toHaveBeenCalledWith("server-1", "command-1");
|
||||
expect(apiMocks.listGameClientBridgeCommands).toHaveBeenCalledWith("server-1", commandFilter);
|
||||
expect(apiMocks.listGameClientBridgeSnapshots).toHaveBeenCalledWith("server-1", snapshotQuery);
|
||||
expect(apiMocks.executePluginBridge).toHaveBeenCalledWith({
|
||||
requestId: "request-1",
|
||||
pluginId: "game.scum",
|
||||
routeKey: "players",
|
||||
serverInstanceId: "server-1",
|
||||
action: "server.instances.read",
|
||||
aiPurpose: undefined,
|
||||
payload: { view: "summary" }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user