Files
browser/platform_web/utils/pluginBridgeHost.test.ts
T
npc0-hue bc45188be9 Send user trajectories from the user list into a focused live map
- Open 「用户轨迹」 straight on the live map instead of a drawer, carrying the
  server, the focused user and a start/end window through the plugin page hash.
- Default that window to one hour before the user's last seen time and keep the
  plugin page query available to bundles through the host context.
- Replace the time range preset gate with always visible start/end datetime
  fields plus a quick-range select, so the user list rebuilds from the window.
- Drop the bulky trajectory list and draw one compact colour legend under the
  map, keeping a stable colour per identity and restricting vehicle tracks to
  the vehicles the selected users actually rode.
2026-09-16 19:35:31 +08:00

202 lines
8.4 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { PluginBridgeManifestContract } from "../contracts/pluginBridge";
import {
createPluginBridgeDispatcher,
createPluginBridgeHostContext,
filterAllowedPermissions,
parsePluginArtifactReference,
validateBridgeExecutionRequest
} from "./pluginBridgeHost";
const plugin: PluginBridgeManifestContract = {
id: "game.example",
declaredPermissions: ["server.read", "server.logs.read", "server.files.read", "server.artifacts.read", "server.remote.access", "ai.invoke"],
bridgeActions: ["server.instances.read", "logs.query", "files.request", "artifacts.open", "remote.access.request", "ai.invoke"],
pages: [
{
key: "logs",
title: "Logs",
path: "/logs",
permissions: ["server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"],
bridgeActions: ["logs.query", "files.request", "artifacts.open", "ai.invoke"]
},
{
key: "remote",
title: "Remote",
path: "/remote",
permissions: ["server.remote.access"],
bridgeActions: ["remote.access.request"]
}
],
aiPurposes: ["logs.diagnose"]
};
describe("plugin bridge host utilities", () => {
it("filters page permissions against manifest permissions", () => {
expect(filterAllowedPermissions(plugin.declaredPermissions, ["server.logs.read", "server.artifacts.read"])).toEqual(["server.logs.read", "server.artifacts.read"]);
});
it("creates safe host context without secret-bearing fields", () => {
const context = createPluginBridgeHostContext({
plugin,
routeKey: "logs",
serverInstanceId: "server-1",
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" }
});
expect(context).toEqual({
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
pageQuery: {},
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" },
permissions: ["server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"],
bridgeActions: ["logs.query", "files.request", "artifacts.open", "ai.invoke"],
aiPurposes: ["logs.diagnose"]
});
expect(context).not.toHaveProperty("apiKey");
expect(context).not.toHaveProperty("runCredential");
expect(context).not.toHaveProperty("hostPath");
});
it("builds bridge execution requests through the platform client", async () => {
const context = createPluginBridgeHostContext({
plugin,
routeKey: "logs",
serverInstanceId: "server-1",
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" }
});
const client = {
executePluginBridge: vi.fn(async () => ({
requestId: "req-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "logs.query",
status: "ok",
result: { entryCount: "0" }
}))
};
const dispatch = createPluginBridgeDispatcher(context, client);
await expect(dispatch({ requestId: "req-1", action: "logs.query", payload: { logStreamId: "log-1" } })).resolves.toMatchObject({
status: "ok",
result: { entryCount: "0" }
});
expect(client.executePluginBridge).toHaveBeenCalledWith({
requestId: "req-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "logs.query",
aiPurpose: undefined,
payload: { logStreamId: "log-1" }
});
});
it("rejects denied, invalid, and cancelled bridge execution locally", async () => {
const context = createPluginBridgeHostContext({
plugin,
routeKey: "logs",
serverInstanceId: "server-1",
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" }
});
expect(validateBridgeExecutionRequest(context, { requestId: "req-denied", action: "server.instances.read" })).toMatchObject({ code: "unsupported_action" });
expect(validateBridgeExecutionRequest(context, { requestId: "req-invalid", action: "files.request", payload: { " key": "value" } })).toMatchObject({ code: "validation" });
const client = { executePluginBridge: vi.fn() };
const controller = new AbortController();
controller.abort();
const dispatch = createPluginBridgeDispatcher(context, client);
await expect(dispatch({ requestId: "req-cancel", action: "logs.query", payload: { logStreamId: "log-1" } }, controller.signal)).resolves.toMatchObject({
status: "cancelled",
error: { code: "cancelled" }
});
expect(client.executePluginBridge).not.toHaveBeenCalled();
});
it("allows mediated remote SQL execute payloads without direct connection material", () => {
const context = createPluginBridgeHostContext({ plugin, routeKey: "remote", serverInstanceId: "server-1", themeTokens: { colorScheme: "dark", accentColor: "#22c55e" } });
expect(validateBridgeExecutionRequest(context, { requestId: "sql-1", action: "remote.access.request", payload: { capability: "remote.run.db.sqlite.execute", declarationKey: "sqlite-db", targetKey: "game-db", idempotencyKey: "sql-1", "input.sqlText": "UPDATE players SET score = 855 WHERE id = 'player-123';" } })).toBeNull();
});
it("passes plugin-owned bridge payload text through without frontend content scanning", () => {
const context = createPluginBridgeHostContext({ plugin, routeKey: "remote", serverInstanceId: "server-1", themeTokens: { colorScheme: "dark", accentColor: "#22c55e" } });
expect(validateBridgeExecutionRequest(context, { requestId: "opaque-1", action: "remote.access.request", payload: { capability: "remote.run.rcon.command", command: "#Login password=opaque /Users/operator note tcp://127.0.0.1:7777" } })).toBeNull();
});
it("dispatches mediated AI requests without provider configuration", async () => {
const context = createPluginBridgeHostContext({
plugin,
routeKey: "logs",
serverInstanceId: "server-1",
themeTokens: { colorScheme: "dark", accentColor: "#22c55e" }
});
expect(validateBridgeExecutionRequest(context, { requestId: "ai-denied", action: "ai.invoke", aiPurpose: "config.suggest" })).toMatchObject({ code: "ai_purpose_denied" });
const client = {
executePluginBridge: vi.fn(async () => ({
requestId: "ai-1",
pluginId: "game.example",
routeKey: "logs",
serverInstanceId: "server-1",
action: "ai.invoke",
status: "ok",
result: { recommendation: "Mock AI recommendation", mocked: "true" }
}))
};
const dispatch = createPluginBridgeDispatcher(context, client);
const response = await dispatch({ requestId: "ai-1", action: "ai.invoke", aiPurpose: "logs.diagnose", payload: { prompt: "Summarize logs" } });
expect(response).toMatchObject({ status: "ok", result: { mocked: "true" } });
const serialized = JSON.stringify(response);
expect(serialized).not.toContain("apiKeyRef");
expect(serialized).not.toContain("rawApiKey");
expect(serialized).not.toContain("baseUrl");
});
it("parses safe artifact bridge references and rejects backend internals", () => {
expect(
parsePluginArtifactReference({
artifactId: "artifact-1",
filename: "artifact-1.bin",
contentType: "application/octet-stream",
sizeBytes: "128",
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"
})
).toMatchObject({ artifactId: "artifact-1", downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true });
expect(
parsePluginArtifactReference({
artifactId: "artifact-1",
filename: "/Users/tasia/artifact.bin",
contentType: "application/octet-stream",
sizeBytes: "128",
checksum: "sha256:abc",
downloadUrl: "storage://bucket/artifact-1",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: "true",
chunkSizeBytes: "1048576"
})
).toBeNull();
expect(
parsePluginArtifactReference({
artifactId: "artifact-1",
filename: "/Users/tasia/artifact.bin",
contentType: "application/octet-stream",
sizeBytes: "128",
checksum: "sha256:abc",
downloadUrl: "/api/v1/artifacts/artifact-1/content",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: "true",
chunkSizeBytes: "1048576"
})
).toMatchObject({ filename: "/Users/tasia/artifact.bin" });
});
});