201 lines
8.4 KiB
TypeScript
201 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",
|
|
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" });
|
|
});
|
|
});
|