Files
browser/plugins/tests/manifest-validation.test.ts
T
npc0-hue c5d8141d2a Tuck the trajectory replay controls into a compact pill
The wide range scrubber under the map was the loudest element on the live
map. Keep replay, speed and 回到最新, drop the slider itself, and shrink the
bar to a right aligned pill so the map and the colour legend stay in focus.
2026-09-16 21:31:11 +08:00

1169 lines
64 KiB
TypeScript

import { describe, expect, it } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Ajv2020 } from "ajv/dist/2020.js";
import {
bridgeError,
canRequestBridgeAction,
createAIInvocationRequest,
createArtifactOpenRequest,
createBridgeExecutionRequest,
createLifecycleDispatchRequest,
createProductionPluginLifecycleRequest,
createBridgeRequest,
createDependencyActionRequest,
createLogBackfillRequest,
createGameClientBridgeQueueRequest,
createRemoteAccessRequest,
createRunDistributionRequest,
hasPluginPermission,
parseArtifactReference,
parseBridgeExecutionResponse,
parseAIInvocationResponse,
type GameClientBridgeQueryTemplateDeclaration,
type GamePluginManifest,
type PluginLifecycleActionDeclaration,
type PluginBridgeContext
} from "../sdk/index.js";
import { validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js";
const pluginsRoot = fileURLToPath(new URL("..", import.meta.url));
type MutableBridgeManifest = {
capabilities: string[];
permissions: string[];
remoteAccess?: {
methods: string[];
runCapabilities?: string[];
databaseEngines?: string[];
};
runtimeProfiles?: {
transportProfiles?: Array<Record<string, unknown>>;
};
pages?: Array<{ key?: string; permissions?: string[]; bridgeActions?: string[] }>;
gameClientBridge: {
commands: Array<Record<string, unknown>>;
snapshots: Array<Record<string, unknown>>;
queryTemplates?: Array<Record<string, unknown>>;
commandRetentionSeconds: number;
maxCommands: number;
pages: Array<Record<string, unknown>>;
};
};
function bridgeObjectSchema(properties: Record<string, unknown>, required: string[] = []): Record<string, unknown> {
return {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
additionalProperties: false,
properties,
...(required.length > 0 ? { required } : {})
};
}
function writeFixtureJSON(fixtureDir: string, relativePath: string, value: unknown): void {
const target = path.join(fixtureDir, relativePath);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.writeFileSync(target, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function validateTemporaryBridgeManifest(mutate?: (manifest: MutableBridgeManifest, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-bridge-manifest-"));
try {
fs.cpSync(path.join(pluginsRoot, "examples/dev-game-plugin"), fixtureDir, { recursive: true });
const manifestPath = path.join(fixtureDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as MutableBridgeManifest;
manifest.capabilities = [...manifest.capabilities, "remote.run.db.sqlite.query", "remote.run.rcon.command"];
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read", "server.game-client.maintenance"];
manifest.remoteAccess = { methods: ["run"], runCapabilities: ["remote.run.db.sqlite.query", "remote.run.rcon.command"], databaseEngines: ["sqlite"] };
manifest.runtimeProfiles = {
transportProfiles: [
{ key: "sqlite-db", kind: "sqlite", targetKey: "db/sqlite", capabilities: ["remote.run.db.sqlite.query"] },
{ key: "scum-rcon", kind: "rcon", targetKey: "scum-rcon", capabilities: ["remote.run.rcon.command"] }
]
};
const overviewPage = manifest.pages?.find((page) => page.key === "overview");
if (overviewPage) {
overviewPage.permissions = [...(overviewPage.permissions ?? []), "server.game-client.read", "server.game-client.command", "server.game-client.maintenance", "server.remote.access"];
overviewPage.bridgeActions = [...(overviewPage.bridgeActions ?? []), "remote.access.request"];
}
manifest.gameClientBridge = {
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", resultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }],
snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
queryTemplates: [{ key: "player.by-id", title: "Find player by ID", permission: "server.game-client.read", engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", parameterSchemaRef: "schemas/bridge/player-by-id.parameters.schema.json", resultSchemaRef: "schemas/bridge/player-by-id.result.schema.json", maxRows: 1, timeoutSeconds: 10 }],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: [{ pageKey: "overview", commandTypes: ["diagnostic.ping"], snapshotTypes: ["players"], queryTemplateKeys: ["player.by-id"] }]
};
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json", bridgeObjectSchema({ message: { type: "string", minLength: 1, maxLength: 200 } }, ["message"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping-result.schema.json", bridgeObjectSchema({ accepted: { type: "boolean" } }, ["accepted"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 100, items: bridgeObjectSchema({ id: { type: "string", minLength: 1, maxLength: 80 } }, ["id"]) } }, ["players"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ players: { type: "array", maxItems: 1, items: bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId"]) } }, ["players"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-fame-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, fame: { type: "integer" } }, ["playerId", "fame"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.payload.schema.json", bridgeObjectSchema({ playerId: { type: "string", minLength: 1, maxLength: 96 }, before: { type: "number" }, after: { type: "number" }, safetyWindow: { type: "string", minLength: 1, maxLength: 96 } }, ["playerId", "before", "after", "safetyWindow"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.result.schema.json", bridgeObjectSchema({ outcome: { enum: ["succeeded", "failed", "unknown"] }, rowsAffected: { type: "integer", minimum: 0, maximum: 1 } }, ["outcome", "rowsAffected"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/player-attribute-855-set.confirmation.schema.json", bridgeObjectSchema({ playerId: { type: "string" }, value: { type: "number" } }, ["playerId", "value"]));
mutate?.(manifest, fixtureDir);
writeFixtureJSON(fixtureDir, "manifest.json", manifest);
return validateManifestFile(manifestPath);
} finally {
fs.rmSync(fixtureDir, { recursive: true, force: true });
}
}
function validateTemporaryScumManifest(mutate: (manifest: Record<string, any>, fixtureDir: string) => void): string[] {
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), "browser-scum-manifest-"));
try {
fs.cpSync(path.join(pluginsRoot, "examples/scum-server-plugin"), fixtureDir, { recursive: true });
const manifestPath = path.join(fixtureDir, "manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, any>;
mutate(manifest, fixtureDir);
writeFixtureJSON(fixtureDir, "manifest.json", manifest);
return validateManifestFile(manifestPath);
} finally {
fs.rmSync(fixtureDir, { recursive: true, force: true });
}
}
describe("plugin manifest validation", () => {
it("accepts the development example manifest", () => {
expect(validateManifestFile("examples/dev-game-plugin/manifest.json")).toEqual([]);
});
it("accepts the SCUM server plugin manifest", () => {
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
});
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; sqlRef: string }>;
pages: Array<{ pageKey: string; queryTemplateKeys?: 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"]));
for (const template of manifest.gameClientBridge.queryTemplates ?? []) {
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/[a-z-]+\.sql$/);
}
expect(manifest.gameClientBridge.pages.some((page) => (page.queryTemplateKeys ?? []).length > 0)).toBe(false);
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(false);
});
it("declares bounded SCUM database read templates for platform-dispatched 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;
pollIntervalSeconds: number;
maxRows: number;
timeoutSeconds: number;
projections?: Array<{ collection: string; rowPath: string; upsertKeys: string[] }>;
}>;
};
};
const templates = manifest.gameClientBridge.queryTemplates ?? [];
expect(templates.map((template) => template.key)).toEqual(["scum.database.players", "scum.database.vehicles"]);
for (const template of templates) {
expect(template.engine).toBe("sqlite");
expect(template.transportKey).toBe("scum-database");
expect(template.targetKey).toBe("scum-database");
expect(template.pollIntervalSeconds).toBeGreaterThan(0);
expect(template.pollIntervalSeconds).toBeLessThanOrEqual(300);
expect(template.maxRows).toBeGreaterThan(0);
expect(template.maxRows).toBeLessThanOrEqual(500);
expect(template.timeoutSeconds).toBeGreaterThan(0);
expect(template.timeoutSeconds).toBeLessThanOrEqual(60);
expect((template.projections ?? []).length).toBe(1);
for (const projection of template.projections ?? []) {
expect(projection.rowPath).toBe("rows");
expect(projection.upsertKeys.length).toBeGreaterThan(0);
}
}
expect(templates.find((template) => template.key === "scum.database.players")?.projections?.[0].collection).toBe("scum.users");
expect(templates.find((template) => template.key === "scum.database.vehicles")?.projections?.[0].collection).toBe("scum.vehicles");
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
});
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as any;
const assetPaths = manifest.assetFiles.map((file: { path: string }) => file.path);
const installAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.install), "utf8"));
const startAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.start), "utf8"));
const stopAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.stop), "utf8"));
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
const stopScript = fs.readFileSync(path.join(pluginDir, stopAction.gracefulStop.executableKey), "utf8");
const rconScript = fs.readFileSync(path.join(pluginDir, "bin/scum-rcon.ps1"), "utf8");
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
expect(JSON.stringify(manifest.runtimeProfiles.installPlans ?? [])).not.toContain("steamcmd-app");
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "actions/stop.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "bin/scum-stop.cmd", "bin/scum-rcon.ps1", "assets/map/scum-map-overview.jpg"]));
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 validate +quit" } });
expect(installAction.timeoutMs).toBe(7200000);
expect(startAction).toMatchObject({ executableKey: "bin/scum-start.cmd", targetExecutableKey: "SCUM/Binaries/Win64/SCUMServer.exe", outputMode: "pipes", environment: { SERVER_LOG_FLAG: "-log -stdout -FullStdOutLogOutput" } });
expect(installScript).not.toContain("taskkill");
expect(installScript).toContain("call :stop_running_scum");
expect(installScript).toContain("scum-stop.cmd");
expect(installScript).toContain("SCUM server did not shut down gracefully; refusing to update server files.");
expect(stopAction).toMatchObject({
mode: "control",
environment: { SERVER_STOP_SHUTDOWN_COMMAND: "Quit", SERVER_STOP_NOTICE_SECONDS: "15", SERVER_STOP_TIMEOUT_SECONDS: "120" },
gracefulStop: { executableKey: "bin/scum-stop.cmd", fallback: "terminate", timeoutMs: 300000 }
});
expect(stopScript).not.toContain("taskkill");
expect(stopScript).toContain("Get-CimInstance Win32_Process");
expect(stopScript).toContain("[IO.Path]::GetFullPath($_.ExecutablePath) -ieq $full");
expect(stopScript).toContain("scum-rcon.ps1");
expect(stopScript).toContain("ue4ss\\Mods\\scum_simple_rcon\\config.ini");
expect(stopScript).toContain('call :send_rcon "Announce %SERVER_STOP_ANNOUNCE%"');
expect(stopScript).toContain('call :send_rcon "%SERVER_STOP_SHUTDOWN_COMMAND%"');
expect(stopScript).toContain("exit /b 2");
expect(rconScript).toContain("Read-RconConfig");
expect(rconScript).toContain('$config["rcon.password"]');
expect(rconScript).toContain('$config["rcon.port"]');
expect(rconScript).toContain("Send-RconPacket");
expect(rconScript).toContain("Receive-RconPacket");
expect(stopScript).toContain('set "SCUM_RCON_PORT_ARG=-Port %SERVER_STOP_RCON_PORT%"');
expect(installScript).toContain("SERVER_ROOT_WINDOWS=%SERVER_ROOT:/=\\%");
expect(installScript).toContain("SERVER_INSTALL_DIR=%SERVER_ROOT_WINDOWS%");
expect(installScript).toContain("SERVER_STEAMCMD_DIR=%SERVER_ROOT_DRIVE%\\steamcmd");
expect(installScript).toContain("STEAMCMD_DIR=%SERVER_STEAMCMD_DIR_WINDOWS%");
expect(installScript).toContain("SERVER_INSTALL_DIR_STEAM=%SERVER_INSTALL_DIR_WINDOWS:\\=/%");
expect(installScript).toContain("%SERVER_STEAMCMD_INSTALL_DIR_ARG% \"%SERVER_INSTALL_DIR_STEAM%\" %SERVER_STEAMCMD_UPDATE_ARGS%");
expect(installScript).toContain("SERVER_STEAMCMD_MAX_ATTEMPTS=3");
expect(installScript).toContain("%SERVER_INSTALL_DIR_WINDOWS%\\%SERVER_EXECUTABLE_REF:/=\\%");
expect(installScript).toContain("steamcmd\\steamapps\\common\\SCUM Server");
expect(installScript).toContain(".scum-exe-path");
expect(startScript).toContain(".scum-exe-path");
expect(startScript).toContain("call :install_or_update");
expect(startScript).toContain("scum-install-update.cmd");
expect(startScript).toContain("SCUM executable was not found. Running plugin install/update script.");
expect(startScript).not.toContain("SCUM.log");
expect(startScript).toContain("%SERVER_INSTALL_DIR_WINDOWS%\\%SERVER_EXECUTABLE_REF:/=\\%");
expect(startScript).toContain("steamcmd\\steamapps\\common\\SCUM Server");
expect(startScript).toContain("SCUM_WORKDIR");
expect(startScript).toContain('if "%SERVER_CREATE_QUERYPORT%"=="" set "SERVER_CREATE_QUERYPORT=27015"');
expect(startScript).toContain("-port=%SERVER_CREATE_GAMEPORT% -QueryPort=%SERVER_CREATE_QUERYPORT% -MaxPlayers=%SERVER_CREATE_MAXPLAYERS% %SERVER_LOG_FLAG%");
});
it("declares a plugin-owned SCUM build probe that the update button can check", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
runtimeProfiles: { dependencyProbes: Array<{ key: string; kind: string; targetKey: string; required?: boolean; steamAppId?: string; platforms?: string[] }> };
};
const probe = manifest.runtimeProfiles.dependencyProbes.find((candidate) => candidate.key === "scum-server-build");
expect(probe).toMatchObject({ kind: "steam.update", targetKey: "steamcmd", steamAppId: "3792580", required: false, platforms: ["windows"] });
expect(manifest.runtimeProfiles.dependencyProbes.filter((candidate) => candidate.kind === "steam.update")).toEqual([probe]);
});
it("rejects lifecycle executable assets missing from the manifest seed declaration", () => {
const errors = validateTemporaryScumManifest((manifest) => {
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-start.cmd");
});
expect(errors.some((error) => error.includes("lifecycleAction.start.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
});
it("rejects graceful stop scripts missing from the manifest seed declaration", () => {
const missingAsset = validateTemporaryScumManifest((manifest) => {
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-stop.cmd");
});
expect(missingAsset.some((error) => error.includes("lifecycleAction.stop.gracefulStop.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
const wrongAction = validateTemporaryScumManifest((manifest) => {
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "actions/install.json");
});
expect(wrongAction.some((error) => error.includes("lifecycleAction.install: action file must be declared"))).toBe(true);
});
it("rejects unsupported or unsafe inline create-field declarations", () => {
const malformed = validateTemporaryScumManifest((manifest) => {
manifest.server.createFields[0].type = "path";
});
expect(malformed.some((error) => error.includes("createFields") && error.includes("type"))).toBe(true);
const unsafe = validateTemporaryScumManifest((manifest) => {
manifest.server.createFields[0].defaultValue = "/srv/hidden-server";
});
expect(unsafe.some((error) => error.includes("raw host path"))).toBe(true);
});
it("declares a read-only SCUM database transport beside the management transports", () => {
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(["sqlite"]);
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.remoteAccess?.runCapabilities).not.toContain("remote.run.db.sqlite.execute");
expect(manifest.runtimeProfiles?.dataTargets).toEqual([
expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot" })
]);
expect(manifest.runtimeProfiles?.transportProfiles).toEqual(expect.arrayContaining([
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.objectContaining({ key: "scum-database", kind: "sqlite", targetKey: "scum-database", capabilities: ["remote.run.db.sqlite.query"] })
]));
expect(manifest.runtimeProfiles?.transportProfiles?.flatMap((profile) => profile.capabilities ?? [])).not.toContain("remote.run.db.sqlite.execute");
});
it("covers the SCUM 4.1 bridge and lifecycle declarations", () => {
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
permissions: string[];
gameClientBridge: {
commands: Array<{
type: string;
permission: string;
payloadSchemaRef: string;
resultSchemaRef?: string;
timeoutSeconds: number;
maxPayloadBytes: number;
}>;
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
queryTemplates?: Array<{ key: string; projections?: unknown[] }>;
lifecycleProjections?: unknown[];
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
};
pages: Array<{ key: string; permissions?: string[] }>;
fileWorkspace?: {
defaultDirectoryKey: string;
directories: Array<{ key: string; label: string; scope: string }>;
files: Array<{ key: string; directoryKey: string; label: string; kind: string; streamKey?: string; editable?: boolean }>;
configFields: Array<{ key: string; fileKey: string; configKey: string; label: string }>;
};
runtimeProfiles?: { lifecycleProfiles?: Array<{ key: string; capabilities?: string[] }>; logSources?: Array<{ key: string }>; clientManagers?: unknown[] };
version: string;
};
const installAction = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/actions/install.json"), "utf8")) as { environment?: Record<string, string> };
const serialized = JSON.stringify(manifest).toLowerCase();
expect(serialized).not.toContain("local-proof");
expect(manifest.version).toBe("0.1.24");
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
"player.lookup",
"reward.deliver",
"player.notify",
"vehicle.spawn",
"event.start",
"restart.prepare",
"maintenance.prepare"
]));
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();
expect((manifest.gameClientBridge.queryTemplates ?? []).map((template) => template.key)).toEqual(["scum.database.players", "scum.database.vehicles"]);
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.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"]));
expect(manifest.fileWorkspace?.defaultDirectoryKey).toBe("scum-config");
expect(manifest.fileWorkspace?.directories.map((directory) => `${directory.key}:${directory.scope}`)).toEqual(expect.arrayContaining(["scum-config:config", "scum-logs:logs"]));
expect(manifest.fileWorkspace?.files.map((file) => file.key)).toEqual(expect.arrayContaining(["scum-server-settings", "scum-admin-users", "scum-chat-log", "scum-performance-log"]));
expect(manifest.fileWorkspace?.configFields.map((field) => field.key)).toEqual(expect.arrayContaining(["server-name", "max-players", "welcome-message", "server-description", "server-playstyle"]));
expect(manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "scum-client")).toBeUndefined();
expect(manifest.runtimeProfiles?.logSources?.map((source) => source.key)).toEqual(expect.arrayContaining(["scum-chat-events", "scum-server-events", "scum-login-events", "scum-trade-events"]));
});
it("declares bounded and permissioned SCUM bridge commands", () => {
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;
permission: string;
payloadSchemaRef: string;
resultSchemaRef?: string;
timeoutSeconds: number;
maxPayloadBytes: number;
}>;
};
};
const expected = {
"player.lookup": { permission: "server.game-client.read" },
"reward.deliver": { permission: "server.game-client.command" },
"player.notify": { permission: "server.game-client.command" },
"vehicle.spawn": { permission: "server.game-client.command" },
"event.start": { permission: "server.game-client.command" },
"restart.prepare": { permission: "server.game-client.maintenance" },
"maintenance.prepare": { permission: "server.game-client.maintenance" }
} as const;
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining(Object.keys(expected)));
for (const command of manifest.gameClientBridge.commands) {
const policy = expected[command.type as keyof typeof expected];
if (!policy) {
continue;
}
expect(command.permission).toBe(policy.permission);
expect(command.timeoutSeconds).toBeGreaterThan(0);
expect(command.timeoutSeconds).toBeLessThanOrEqual(3600);
expect(command.maxPayloadBytes).toBeGreaterThan(0);
expect(command.maxPayloadBytes).toBeLessThanOrEqual(65536);
expect(command.resultSchemaRef).toBeTruthy();
}
const schemaRefs = manifest.gameClientBridge.commands.flatMap((command) => [command.payloadSchemaRef, command.resultSchemaRef].filter((ref): ref is string => Boolean(ref)));
for (const schemaRef of schemaRefs) {
const schema = JSON.parse(fs.readFileSync(path.join(pluginDir, schemaRef), "utf8")) as Record<string, unknown>;
const hasPluginInventedCountLimitsRemoved = ["schemas/bridge/reward-deliver.payload.schema.json", "schemas/bridge/event-start.payload.schema.json"].includes(schemaRef);
const visit = (value: unknown): void => {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (typeof value !== "object" || value === null) {
return;
}
const record = value as Record<string, unknown>;
if (record.type === "object" || Object.hasOwn(record, "properties")) {
expect(record.additionalProperties).toBe(false);
}
if (record.type === "array") {
expect(record.items).toBeDefined();
if (!hasPluginInventedCountLimitsRemoved) expect(record.maxItems).toBeGreaterThan(0);
}
if (record.type === "string") {
expect(record.maxLength).toBeGreaterThan(0);
}
if (record.type === "integer" || record.type === "number") {
if (!hasPluginInventedCountLimitsRemoved) expect(record.maximum).toBeDefined();
if (typeof record.minimum === "number" && typeof record.maximum === "number") expect(record.minimum).toBeLessThanOrEqual(record.maximum);
}
Object.values(record).forEach(visit);
};
expect(schema.type).toBe("object");
expect(schema.additionalProperties).toBe(false);
visit(schema.properties);
}
});
it("declares bounded SCUM snapshot schemas for operations projections", () => {
const manifestPath = path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json");
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
gameClientBridge: {
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
pages: Array<{ pageKey: string; snapshotTypes?: string[] }>;
};
};
const expectedTypes = ["online.sessions", "players", "squads", "vehicles", "flags"];
const snapshotsByType = new Map(manifest.gameClientBridge.snapshots.map((snapshot) => [snapshot.type, snapshot]));
expect([...snapshotsByType.keys()]).toEqual(expect.arrayContaining(expectedTypes));
for (const type of expectedTypes) {
const snapshot = snapshotsByType.get(type);
expect(snapshot?.schemaVersion).toBe("1");
expect(snapshot?.schemaRef).toMatch(/^schemas\/bridge\/[a-z-]+\.snapshot\.schema\.json$/);
const schema = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin", snapshot!.schemaRef), "utf8")) as {
type?: string;
additionalProperties?: boolean;
properties?: Record<string, { type?: string; maxItems?: number }>;
};
expect(schema.type).toBe("object");
expect(schema.additionalProperties).toBe(false);
for (const property of Object.values(schema.properties ?? {})) {
if (property.type === "array") {
expect(property.maxItems).toBeGreaterThan(0);
}
}
}
const pageSnapshotTypes = manifest.gameClientBridge.pages.flatMap((page) => page.snapshotTypes ?? []);
expect(pageSnapshotTypes).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "players")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "online.sessions"]));
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "vehicles", "flags"]));
});
it("keeps SCUM database reads platform-dispatched, read-only, and package-scoped", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
capabilities: string[];
remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[]; rcon?: boolean };
gameClientBridge: {
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 }>;
};
assetFiles?: Array<{ path: string }>;
};
const templates = manifest.gameClientBridge.queryTemplates ?? [];
expect(templates.length).toBe(2);
const assetPaths = (manifest.assetFiles ?? []).map((file) => file.path);
for (const template of templates) {
const sqlRef = String(template.sqlRef);
expect(sqlRef.startsWith("sql/scum-db-v57/")).toBe(true);
expect(assetPaths).toContain(sqlRef);
expect(fs.existsSync(path.join(pluginDir, sqlRef))).toBe(true);
}
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.capabilities).not.toContain("remote.run.db.sqlite.execute");
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
expect(manifest.remoteAccess?.runCapabilities).not.toContain("remote.run.db.sqlite.execute");
expect(manifest.remoteAccess?.databaseEngines).toEqual(["sqlite"]);
expect(manifest.remoteAccess?.rcon).toBe(true);
expect(manifest.runtimeProfiles?.dataTargets).toEqual([
expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot" })
]);
expect(manifest.runtimeProfiles?.transportProfiles?.some((profile) => profile.kind === "sqlite" && profile.capabilities.includes("remote.run.db.sqlite.query"))).toBe(true);
expect(manifest.gameClientBridge.pages.some((page) => (page.queryTemplateKeys ?? []).length > 0)).toBe(false);
for (const template of templates) {
const schemaRefs = [String(template.parameterSchemaRef), String(template.resultSchemaRef)];
for (const schemaRef of schemaRefs) {
expect(fs.existsSync(path.join(pluginDir, schemaRef))).toBe(true);
}
}
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).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"]) {
const pluginPage = manifest.pages.find((page) => page.key === pageKey);
expect(pluginPage?.permissions).toContain("server.game-client.read");
expect(pluginPage?.permissions).toContain("server.remote.access");
expect(pluginPage?.bridgeActions).toContain("remote.access.request");
}
for (const pageKey of ["gifts", "workflows"]) {
expect(manifest.pages.find((page) => page.key === pageKey)?.permissions).toContain("server.game-client.command");
}
});
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-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"));
const mapGeometry = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![1]), "utf8"));
expect(configMaps.maps.map((map: { key: string }) => map.key)).toEqual(expect.arrayContaining(["server-settings", "economy-override", "raid-times", "notifications", "admin-users", "banned-users"]));
expect(giftMetadata).toMatchObject({ databaseUserVersion: 57, catalogSource: { configMapKey: "economy-override" } });
expect(mapGeometry).toMatchObject({ databaseUserVersion: 57, image: { path: "assets/map/scum-map-terrain-4096.webp", width: 4096, height: 4096 }, runtimeOverride: { kilometersToWorldUnits: 100000 } });
});
it("does not declare a SCUM client-manager profile", () => {
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
runtimeProfiles?: { clientManagers?: unknown[] };
};
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
});
it("accepts the Minecraft server plugin manifest", () => {
expect(validateManifestFile("examples/minecraft-server-plugin/manifest.json")).toEqual([]);
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/minecraft-server-plugin/manifest.json"), "utf8")) as { pages: Array<{ key: string; title: string }>; fileWorkspace?: { files?: Array<{ key: string }> } };
expect(manifest.pages.map((page) => `${page.key}:${page.title}`)).toEqual(["files:文件管理", "mods-market:mod/插件市场"]);
expect(manifest.fileWorkspace?.files?.map((file) => file.key)).toEqual(expect.arrayContaining(["server-properties", "ops-json", "whitelist-json", "minecraft-latest-log"]));
});
it("validates typed game-client bridge catalogs", () => {
const schema = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "manifests/game-plugin.manifest.schema.json"), "utf8"));
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/dev-game-plugin/manifest.json"), "utf8"));
manifest.permissions = [...manifest.permissions, "server.game-client.command", "server.game-client.read"];
manifest.gameClientBridge = {
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.schema.json", resultSchemaRef: "schemas/bridge/diagnostic-ping-result.schema.json", timeoutSeconds: 60, maxPayloadBytes: 4096 }],
snapshots: [{ type: "players", schemaVersion: "1", schemaRef: "schemas/bridge/players.schema.json", keepForSeconds: 3600, maxRecords: 100 }],
commandRetentionSeconds: 86400,
maxCommands: 1000,
pages: []
};
const validate = new Ajv2020({ allErrors: true }).compile(schema);
expect(validate(manifest), JSON.stringify(validate.errors)).toBe(true);
manifest.gameClientBridge.commands[0].unexpectedControlGate = "automatic";
expect(validate(manifest)).toBe(false);
});
it("loads and validates every schema referenced by a safe game-client bridge manifest", () => {
expect(validateTemporaryBridgeManifest()).toEqual([]);
});
it.each(["sql.execute", "sqlExecute", "database.execute", "database.query"])("rejects arbitrary SQL command type %s independently", (commandType) => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.commands[0].type = commandType;
});
expect(errors.some((error) => error.includes("commands[0].type") && error.includes("arbitrary SQL"))).toBe(true);
});
it.each(["shell.execute", "powershell.execute", "script.run", "terminal.execute", "command.run"])("rejects arbitrary shell command type %s independently", (commandType) => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.commands[0].type = commandType;
});
expect(errors.some((error) => error.includes("commands[0].type") && error.includes("arbitrary shell"))).toBe(true);
});
it("rejects raw bridge schema paths end to end", () => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.commands[0].payloadSchemaRef = "/etc/scum-query.json";
});
expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("raw host paths"))).toBe(true);
});
it("rejects undeclared page command references end to end", () => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.pages[0].commandTypes = ["undeclared.command"];
});
expect(errors.some((error) => error.includes("undeclared command undeclared.command"))).toBe(true);
});
it("rejects duplicate and undeclared page query template keys", () => {
const duplicateErrors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.queryTemplates?.push({ ...manifest.gameClientBridge.queryTemplates[0] });
});
expect(duplicateErrors.some((error) => error.includes("duplicate query template player.by-id"))).toBe(true);
const pageErrors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.pages[0].queryTemplateKeys = ["undeclared.query"];
});
expect(pageErrors.some((error) => error.includes("undeclared query template undeclared.query"))).toBe(true);
});
it("rejects unsafe or unbounded query template declarations", () => {
const errors = validateTemporaryBridgeManifest((manifest) => {
Object.assign(manifest.gameClientBridge.queryTemplates?.[0] ?? {}, {
key: "../raw-query",
engine: "mysql",
permission: "server.not-declared",
parameterSchemaRef: "/etc/query.json",
maxRows: 501,
timeoutSeconds: 61
});
});
expect(errors.some((error) => error.includes("queryTemplates/0/key") || error.includes("queryTemplates[0].key"))).toBe(true);
expect(errors.some((error) => error.includes("engine") && error.includes("sqlite"))).toBe(true);
expect(errors.some((error) => error.includes("permission") && error.includes("declared"))).toBe(true);
expect(errors.some((error) => error.includes("parameterSchemaRef") && error.includes("raw host paths"))).toBe(true);
expect(errors.some((error) => error.includes("maxRows"))).toBe(true);
expect(errors.some((error) => error.includes("timeoutSeconds"))).toBe(true);
});
it("requires query templates to match a declared sqlite transport target and capability", () => {
const targetErrors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.queryTemplates![0].targetKey = "db/other";
});
expect(targetErrors.some((error) => error.includes("targetKey") && error.includes("sqlite transport target"))).toBe(true);
const capabilityErrors = validateTemporaryBridgeManifest((manifest) => {
manifest.runtimeProfiles!.transportProfiles![0].capabilities = ["remote.run.files.read"];
});
expect(capabilityErrors.some((error) => error.includes("transportKey") && error.includes("remote.run.db.sqlite.query"))).toBe(true);
});
it("requires query template pages to declare template permission and remote access", () => {
const permissionErrors = validateTemporaryBridgeManifest((manifest) => {
const overviewPage = manifest.pages?.find((page) => page.key === "overview");
overviewPage!.permissions = overviewPage!.permissions?.filter((permission) => permission !== "server.game-client.read");
});
expect(permissionErrors.some((error) => error.includes("page must declare query template permission"))).toBe(true);
const actionErrors = validateTemporaryBridgeManifest((manifest) => {
const overviewPage = manifest.pages?.find((page) => page.key === "overview");
overviewPage!.bridgeActions = overviewPage!.bridgeActions?.filter((action) => action !== "remote.access.request");
});
expect(actionErrors.some((error) => error.includes("page must declare remote.access.request"))).toBe(true);
});
it.each(["sqlText", "dsn", "hostPath", "shellCommand", "socketAddress", "accessToken", "credential"])("keeps opaque query parameter schema field %s", (fieldName) => {
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.parameters.schema.json", bridgeObjectSchema({ [fieldName]: { type: "string", minLength: 1, maxLength: 120 } }, [fieldName]));
});
expect(errors.some((error) => error.includes("queryTemplates[0].parameterSchemaRef"))).toBe(false);
});
it("keeps opaque SQL-looking text embedded in a query result schema", () => {
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
writeFixtureJSON(fixtureDir, "schemas/bridge/player-by-id.result.schema.json", bridgeObjectSchema({ summary: { type: "string", minLength: 1, maxLength: 200, const: "SELECT id FROM players" } }, ["summary"]));
});
expect(errors.some((error) => error.includes("queryTemplates[0].resultSchemaRef"))).toBe(false);
});
it("rejects missing bridge schema files end to end", () => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.gameClientBridge.commands[0].resultSchemaRef = "schemas/bridge/missing.schema.json";
});
expect(errors.some((error) => error.includes("resultSchemaRef") && error.includes("missing bridge schema file"))).toBe(true);
});
it("rejects unsafe executor capabilities end to end", () => {
const errors = validateTemporaryBridgeManifest((manifest) => {
manifest.capabilities = [...manifest.capabilities, "shell.exec"];
});
expect(errors.some((error) => error.includes("capabilities") && error.includes("allowed values"))).toBe(true);
});
it("rejects invalid bridge schema JSON without throwing", () => {
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
fs.writeFileSync(path.join(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json"), "{ invalid", "utf8");
});
expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("not valid JSON"))).toBe(true);
});
it("keeps opaque fields and values in payload, result, and snapshot schemas", () => {
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json", bridgeObjectSchema({ sqlText: { type: "string" } }, ["sqlText"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping-result.schema.json", bridgeObjectSchema({ shellCommand: { type: "string", const: "bash -c whoami" } }, ["shellCommand"]));
writeFixtureJSON(fixtureDir, "schemas/bridge/players.schema.json", bridgeObjectSchema({ hostPath: { type: "string" }, mode: { type: "string", const: "run.socket" }, runCapability: { type: "string" } }, ["hostPath", "mode", "runCapability"]));
});
expect(errors.some((error) => error.includes("payloadSchemaRef") || error.includes("resultSchemaRef") || error.includes("snapshots[0].schemaRef"))).toBe(false);
});
it("requires bounded object schemas for every bridge reference", () => {
const errors = validateTemporaryBridgeManifest((_manifest, fixtureDir) => {
writeFixtureJSON(fixtureDir, "schemas/bridge/diagnostic-ping.schema.json", { type: "object", properties: { message: { type: "string" } } });
});
expect(errors.some((error) => error.includes("payloadSchemaRef") && error.includes("additionalProperties to false"))).toBe(true);
});
it("rejects a manifest with an invalid create form schema", () => {
const errors = validateManifestFile("tests/fixtures/invalid-create-form-manifest.json");
expect(errors.some((error) => error.includes("createForm") && error.includes("label"))).toBe(true);
});
it("rejects unsafe direct run and raw AI key requests", () => {
const errors = validateManifestFile("tests/fixtures/unsafe-manifest.json");
expect(errors.some((error) => error.includes("direct run access"))).toBe(true);
expect(errors.some((error) => error.includes("raw credential or AI/provider key"))).toBe(true);
});
it("rejects unsafe runtime profile values", () => {
const errors = validateManifestFile("tests/fixtures/unsafe-runtime-profile-manifest.json");
expect(errors.some((error) => error.includes("raw credential or AI/provider key"))).toBe(true);
expect(errors.some((error) => error.includes("raw host path"))).toBe(true);
expect(errors.some((error) => error.includes("arbitrary shell"))).toBe(true);
expect(errors.some((error) => error.includes("not approved for dependency download"))).toBe(true);
expect(errors.some((error) => error.includes("runtimeProfiles.serverDeployments") && error.includes("no longer supported"))).toBe(true);
expect(errors.some((error) => error.includes("runtimeProfiles.clientManagers") && error.includes("no longer supported"))).toBe(true);
});
it("validates typed lifecycle declarations and rejects shell/path escapes", () => {
const declaration: PluginLifecycleActionDeclaration = {
version: 1,
action: "start",
mode: "supervised",
executableKey: "bin/game-server",
arguments: ["--foreground"]
};
expect(declaration.action).toBe("start");
const errors = validateLifecycleActionFile("tests/fixtures/unsafe-lifecycle-action.json", "start");
expect(errors.some((error) => error.includes("pattern") || error.includes("arbitrary shell"))).toBe(true);
expect(errors.some((error) => error.includes("raw credential"))).toBe(true);
});
});
describe("plugin SDK", () => {
it("types read-only SQLite query template declarations", () => {
const declaration: GameClientBridgeQueryTemplateDeclaration = {
key: "scum.player.by-id",
title: "Find SCUM player by ID",
permission: "server.game-client.read",
engine: "sqlite",
transportKey: "sqlite-db",
targetKey: "db/sqlite",
parameterSchemaRef: "schemas/bridge/queries/player-by-id.parameters.schema.json",
resultSchemaRef: "schemas/bridge/queries/player-by-id.result.schema.json",
maxRows: 1,
timeoutSeconds: 10,
pollIntervalSeconds: 0
};
expect(declaration).toMatchObject({ engine: "sqlite", transportKey: "sqlite-db", targetKey: "db/sqlite", maxRows: 1 });
expect(JSON.stringify(declaration).toLowerCase()).not.toMatch(/sqltext|dsn|hostpath|socket|credential/);
});
it("builds safe game-client bridge requests without component transport material", () => {
const request = createGameClientBridgeQueueRequest({
profileKey: "example-client",
commandType: "diagnostic.ping",
payload: { message: "hello" },
idempotencyKey: "diagnostic-1",
expiresAt: "2026-07-20T12:00:00Z"
});
expect(request.commandType).toBe("diagnostic.ping");
expect(request).not.toHaveProperty("sessionToken");
expect(request).not.toHaveProperty("componentKey");
expect(request).not.toHaveProperty("runEndpoint");
expect(request).not.toHaveProperty("hostPath");
expect(request).not.toHaveProperty("dsn");
});
it("checks declared bridge permissions", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
permissions: ["server.read", "server.logs.read", "server.artifacts.read"]
};
expect(hasPluginPermission(context, "server.logs.read")).toBe(true);
expect(hasPluginPermission(context, "server.artifacts.read")).toBe(true);
expect(hasPluginPermission(context, "ai.invoke")).toBe(false);
});
it("builds typed bridge request envelopes without owning transport", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
permissions: ["server.read", "server.logs.read"]
};
const request = createBridgeRequest({
id: "request-1",
context,
action: "logs.query",
payload: { streamKey: "stdout", limit: 100 }
});
expect(request).toEqual({
id: "request-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "logs.query",
payload: { streamKey: "stdout", limit: 100 }
});
});
it("checks bridge action permissions and AI purposes locally", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
permissions: ["server.read", "server.logs.read", "ai.invoke"],
aiPurposes: ["logs.diagnose"]
};
expect(canRequestBridgeAction(context, "logs.query")).toBe(true);
expect(canRequestBridgeAction(context, "files.request")).toBe(false);
expect(canRequestBridgeAction(context, "ai.invoke", { aiPurpose: "logs.diagnose" })).toBe(true);
expect(canRequestBridgeAction(context, "ai.invoke", { aiPurpose: "config.suggest" })).toBe(false);
});
it("returns safe bridge errors without credential or transport fields", () => {
const error = bridgeError("missing_permission", "Permission is required", ["server.files.read"]);
expect(error).toEqual({
code: "missing_permission",
message: "Permission is required",
details: ["server.files.read"]
});
expect(error).not.toHaveProperty("apiKey");
expect(error).not.toHaveProperty("runSocket");
expect(error).not.toHaveProperty("hostPath");
});
it("builds and parses execution envelopes without owning transport", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
permissions: ["server.read", "server.logs.read"]
};
const request = createBridgeExecutionRequest({
requestId: "exec-1",
context,
action: "logs.query",
payload: { logStreamId: "log-1", limit: "100" }
});
expect(request).toEqual({
requestId: "exec-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "logs.query",
aiPurpose: undefined,
payload: { logStreamId: "log-1", limit: "100" }
});
expect(request).not.toHaveProperty("fetch");
expect(request).not.toHaveProperty("authorization");
const parsed = parseBridgeExecutionResponse({
requestId: "exec-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "logs.query",
status: "ok",
result: { entryCount: "0" }
});
expect(parsed).toMatchObject({ status: "ok", result: { entryCount: "0" } });
expect(parsed).not.toHaveProperty("apiKey");
expect(parsed).not.toHaveProperty("runSocket");
});
it("builds mediated AI requests and parses redacted responses", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
permissions: ["ai.invoke"],
aiPurposes: ["logs.diagnose"]
};
const request = createAIInvocationRequest({
requestId: "ai-1",
context,
purpose: "logs.diagnose",
prompt: "Summarize warnings",
contextRefs: { server: "server://server-1" }
});
expect(request).toMatchObject({ pluginId: "game.example", purpose: "logs.diagnose", prompt: "Summarize warnings" });
expect(request).not.toHaveProperty("apiKeyRef");
expect(request).not.toHaveProperty("providerBaseUrl");
const parsed = parseAIInvocationResponse({
requestId: "ai-1",
purpose: "logs.diagnose",
status: "ok",
recommendation: "Review the warning trend.",
usage: { model: "mock", mocked: true, inputTokens: 5, outputTokens: 6 }
});
expect(parsed).toMatchObject({ status: "ok", usage: { mocked: true } });
expect(JSON.stringify(parsed)).not.toContain("sk-");
expect(JSON.stringify(parsed)).not.toContain("apiKeyRef");
});
it("builds artifact open envelopes and parses safe platform references", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
permissions: ["server.artifacts.read"]
};
expect(canRequestBridgeAction(context, "artifacts.open")).toBe(true);
expect(
createArtifactOpenRequest({ requestId: "artifact-open-1", context, artifactId: "artifact-1" })
).toEqual({
requestId: "artifact-open-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "artifacts.open",
aiPurpose: undefined,
payload: { artifactId: "artifact-1" }
});
const reference = parseArtifactReference({
artifactId: "artifact-1",
filename: "artifact-1.bin",
contentType: "application/octet-stream",
sizeBytes: "64",
checksum: "sha256:abc",
downloadUrl: "/api/v1/artifacts/artifact-1/content",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: "true",
chunkSizeBytes: "1048576",
storageBehavior: "platform-memory-transfer-session"
});
expect(reference).toMatchObject({ artifactId: "artifact-1", rangeSupported: true });
expect(
parseArtifactReference({
artifactId: "artifact-1",
filename: "/Users/operator/password=opaque.bin",
contentType: "application/octet-stream",
sizeBytes: "64",
checksum: "sha256:abc",
downloadUrl: "/api/v1/artifacts/artifact-1/content",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: "true",
chunkSizeBytes: "1048576",
storageBehavior: "plugin-owned opaque storage:// label"
})
).toMatchObject({ filename: "/Users/operator/password=opaque.bin", storageBehavior: "plugin-owned opaque storage:// label" });
expect(
parseArtifactReference({
artifactId: "artifact-1",
filename: "artifact.bin",
contentType: "application/octet-stream",
sizeBytes: "64",
checksum: "sha256:abc",
downloadUrl: "storage://bucket/artifact-1",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: "true",
chunkSizeBytes: "1048576"
})
).toBeUndefined();
});
it("builds lifecycle dispatch envelopes without direct run transport", () => {
const context: PluginBridgeContext = {
pluginId: "game.example",
routeKey: "overview",
serverInstanceId: "server-1",
permissions: ["server.lifecycle"]
};
expect(canRequestBridgeAction(context, "jobs.dispatch")).toBe(true);
const request = createLifecycleDispatchRequest({
requestId: "lifecycle-start-1",
context,
action: "start",
expectedConfigVersion: 2,
idempotencyKey: "idem-lifecycle-start"
});
expect(request).toEqual({
requestId: "lifecycle-start-1",
pluginId: "game.example",
routeKey: "overview",
serverInstanceId: "server-1",
action: "jobs.dispatch",
aiPurpose: undefined,
payload: {
lifecycleAction: "start",
capability: "process.start",
expectedConfigVersion: "2",
idempotencyKey: "idem-lifecycle-start"
}
});
expect(JSON.stringify(request)).not.toContain("http://");
expect(JSON.stringify(request)).not.toContain("unix://");
expect(JSON.stringify(request)).not.toContain("/Users/");
expect(JSON.stringify(request)).not.toContain("Bearer ");
expect(JSON.stringify(request)).not.toContain("sk-");
});
it("builds remote access request envelopes without direct transport secrets", () => {
const context: PluginBridgeContext = {
pluginId: "game.minecraft",
routeKey: "rcon",
serverInstanceId: "server-1",
permissions: ["server.remote.access"]
};
expect(canRequestBridgeAction(context, "remote.access.request")).toBe(true);
const request = createRemoteAccessRequest({
requestId: "remote-rcon-1",
context,
capability: "remote.run.rcon.command",
targetKey: "rcon/command",
inputRef: "input://server-1/rcon/command/1",
inputs: { playerId: "steam-123", limit: "25" },
idempotencyKey: "idem-remote-rcon"
});
expect(request).toEqual({
requestId: "remote-rcon-1",
pluginId: "game.minecraft",
routeKey: "rcon",
serverInstanceId: "server-1",
action: "remote.access.request",
aiPurpose: undefined,
payload: {
capability: "remote.run.rcon.command",
targetKey: "rcon/command",
inputRef: "input://server-1/rcon/command/1",
"input.playerId": "steam-123",
"input.limit": "25",
idempotencyKey: "idem-remote-rcon"
}
});
expect(JSON.stringify(request)).not.toContain("tcp://");
expect(JSON.stringify(request)).not.toContain("password=");
expect(JSON.stringify(request)).not.toContain("/Users/");
});
it("types runtime profile declarations without raw credentials", () => {
const manifest: GamePluginManifest = {
id: "game.runtime",
name: "Runtime Fixture",
version: "0.1.0",
kind: "game-plugin",
server: { type: "runtime", displayName: "Runtime Fixture", createFormSchema: "schemas/create-form.schema.json" },
capabilities: ["process.start", "process.stop", "logs.read"],
permissions: ["server.read", "server.lifecycle", "server.logs.read"],
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
runtimeProfiles: {
discovery: [{ key: "java", kind: "command.version", targetKey: "java", required: true }],
dependencyProbes: [{ key: "java-21", kind: "java.version", targetKey: "java", minimumVersion: "21" }],
logSources: [{ key: "console", kind: "process.stdout", streamKey: "console", cursorKind: "sequence" }],
transportProfiles: [{ key: "files", kind: "file", capabilities: ["files.read"] }]
}
};
expect(manifest.runtimeProfiles?.discovery?.[0].targetKey).toBe("java");
expect(JSON.stringify(manifest)).not.toContain("password=");
});
it("builds run distribution, dependency, and log backfill envelopes", () => {
const context: PluginBridgeContext = {
pluginId: "game.scum",
routeKey: "remote",
serverInstanceId: "server-1",
permissions: ["server.run.distribution", "server.dependencies.manage", "server.logs.read"]
};
expect(canRequestBridgeAction(context, "run.distribution.request")).toBe(true);
expect(createRunDistributionRequest({ requestId: "run-gen-1", context, operation: "generate", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-run" })).toMatchObject({
action: "run.distribution.request",
payload: { operation: "generate", targetOS: "windows", targetArch: "amd64", idempotencyKey: "idem-run" }
});
expect(createDependencyActionRequest({ requestId: "dep-1", context, operation: "check", probeKey: "steamcmd", idempotencyKey: "idem-dep" })).toMatchObject({
action: "dependencies.request",
payload: { operation: "check", probeKey: "steamcmd" }
});
expect(createDependencyActionRequest({ requestId: "dep-2", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}`, idempotencyKey: "idem-dep-install" })).toMatchObject({
action: "dependencies.request",
payload: { operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", planDigest: `sha256:${"a".repeat(64)}` }
});
expect(() => createDependencyActionRequest({ requestId: "dep-unsafe", context, operation: "install", probeKey: "steamcmd", planKey: "install-steamcmd-linux", idempotencyKey: "idem-dep-unsafe" })).toThrow(/reviewed plan SHA-256 digest/);
expect(createLogBackfillRequest({ requestId: "logs-1", context, sourceKey: "chat-log", limit: 500, idempotencyKey: "idem-logs" })).toMatchObject({
action: "logs.backfill.request",
payload: { sourceKey: "chat-log", limit: "500" }
});
});
it("builds full plugin lifecycle envelopes through Platform only", () => {
const context: PluginBridgeContext = {
pluginId: "game.scum",
routeKey: "overview",
serverInstanceId: "server-1",
permissions: ["server.lifecycle"]
};
const request = createProductionPluginLifecycleRequest({ requestId: "plugin-upgrade-1", context, operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" });
expect(request).toMatchObject({ action: "plugin-lifecycle.request", payload: { operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "plugin-upgrade-v1" } });
expect(JSON.stringify(request)).not.toMatch(/apiKey|providerBaseUrl|runSocket|runEndpoint|hostPath|credential/i);
});
});