first commit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
bridgeError,
|
||||
canRequestBridgeAction,
|
||||
createAIInvocationRequest,
|
||||
createArtifactOpenRequest,
|
||||
createBridgeExecutionRequest,
|
||||
createLifecycleDispatchRequest,
|
||||
createBridgeRequest,
|
||||
hasPluginPermission,
|
||||
parseArtifactReference,
|
||||
parseBridgeExecutionResponse,
|
||||
parseAIInvocationResponse,
|
||||
type PluginBridgeContext
|
||||
} from "../sdk/index.js";
|
||||
import { validateManifestFile } from "../scripts/validate-manifest.js";
|
||||
|
||||
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("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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin SDK", () => {
|
||||
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(JSON.stringify(reference)).not.toContain("/Users/");
|
||||
expect(JSON.stringify(reference)).not.toContain("storage://");
|
||||
expect(JSON.stringify(reference)).not.toContain("Bearer ");
|
||||
|
||||
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-");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user