feat: 完整游戏运维功能
This commit is contained in:
@@ -15,12 +15,15 @@ import {
|
||||
createRunDistributionRequest,
|
||||
hasPluginPermission,
|
||||
parseArtifactReference,
|
||||
parseClientManagerLifecycleStatus,
|
||||
parseBridgeExecutionResponse,
|
||||
parseAIInvocationResponse,
|
||||
type GamePluginManifest,
|
||||
type RuntimeClientManagerProfile,
|
||||
type PluginLifecycleActionDeclaration,
|
||||
type PluginBridgeContext
|
||||
} from "../sdk/index.js";
|
||||
import { validateManifestFile } from "../scripts/validate-manifest.js";
|
||||
import { validateLifecycleActionFile, validateManifestFile } from "../scripts/validate-manifest.js";
|
||||
|
||||
describe("plugin manifest validation", () => {
|
||||
it("accepts the development example manifest", () => {
|
||||
@@ -54,6 +57,24 @@ describe("plugin manifest validation", () => {
|
||||
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("client-manager.deploy is required"))).toBe(true);
|
||||
expect(errors.some((error) => error.includes("offline threshold"))).toBe(true);
|
||||
expect(errors.some((error) => error.includes("profile version is below minimumVersion"))).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -323,6 +344,24 @@ describe("plugin SDK", () => {
|
||||
});
|
||||
|
||||
it("types runtime profile declarations without raw credentials", () => {
|
||||
const clientManager: RuntimeClientManagerProfile = {
|
||||
key: "safe-client-manager",
|
||||
version: "1.2.3",
|
||||
repository: { url: "https://github.com/example/safe-client.git", revisionPolicy: "pinned", revision: "0123456789abcdef" },
|
||||
supportedTargets: [{ os: "linux", arch: "amd64" }],
|
||||
build: { system: "go", entryRef: "cmd/client/main.go" },
|
||||
outputArtifacts: ["safe-client"],
|
||||
deployment: {
|
||||
mode: "run-supervised",
|
||||
executableRef: "safe-client",
|
||||
arguments: ["--config", "config.json"],
|
||||
requiredRunCapabilities: ["client-manager.deploy", "client-manager.control", "client-manager.update", "client-manager.rollback", "client-manager.uninstall"]
|
||||
},
|
||||
lifecycle: { actions: ["start", "stop", "restart", "status", "update", "rollback", "uninstall"], startupTimeoutSeconds: 30, stopTimeoutSeconds: 15 },
|
||||
health: { mode: "component-heartbeat", intervalSeconds: 15, degradedAfterSeconds: 45, offlineAfterSeconds: 120, requiredCapabilities: ["component.register", "component.heartbeat", "component.health"] },
|
||||
compatibility: { minimumVersion: "1.0.0", allowDowngrade: false },
|
||||
updatePolicy: { strategy: "manual-staged", requireApproval: true, healthConfirmationSeconds: 60, retainPrevious: true }
|
||||
};
|
||||
const manifest: GamePluginManifest = {
|
||||
id: "game.runtime",
|
||||
name: "Runtime Fixture",
|
||||
@@ -335,11 +374,13 @@ describe("plugin SDK", () => {
|
||||
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"] }]
|
||||
transportProfiles: [{ key: "files", kind: "file", capabilities: ["files.read"] }],
|
||||
clientManagers: [clientManager]
|
||||
}
|
||||
};
|
||||
|
||||
expect(manifest.runtimeProfiles?.discovery?.[0].targetKey).toBe("java");
|
||||
expect(manifest.runtimeProfiles?.clientManagers?.[0].deployment?.requiredRunCapabilities).toContain("client-manager.deploy");
|
||||
expect(JSON.stringify(manifest)).not.toContain("password=");
|
||||
});
|
||||
|
||||
@@ -360,6 +401,11 @@ describe("plugin SDK", () => {
|
||||
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" }
|
||||
@@ -369,6 +415,31 @@ describe("plugin SDK", () => {
|
||||
payload: { operation: "generate", profileKey: "scum-client-manager" }
|
||||
});
|
||||
expect(JSON.stringify(createClientManagerRequest({ requestId: "client-2", context, operation: "reset-key", profileKey: "scum-client-manager", idempotencyKey: "idem-reset" }))).not.toContain("secret");
|
||||
expect(createClientManagerRequest({ requestId: "client-3", context, operation: "deploy", profileKey: "scum-client-manager", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: 2, idempotencyKey: "idem-deploy" })).toMatchObject({
|
||||
action: "client-manager.request",
|
||||
payload: { operation: "deploy", installationId: "cm-install-1", artifactId: "artifact-1", expectedDeploymentGeneration: "2" }
|
||||
});
|
||||
expect(parseClientManagerLifecycleStatus({
|
||||
installationId: "cm-install-1",
|
||||
profileKey: "scum-client-manager",
|
||||
status: "online",
|
||||
phase: "healthy",
|
||||
targetOS: "windows",
|
||||
targetArch: "amd64",
|
||||
version: "1.0.0",
|
||||
artifactId: "artifact-1",
|
||||
deploymentGeneration: "2",
|
||||
health: "healthy",
|
||||
actions: "stop,restart,update,uninstall"
|
||||
})).toMatchObject({ installationId: "cm-install-1", deploymentGeneration: 2, actions: ["stop", "restart", "update", "uninstall"] });
|
||||
expect(parseClientManagerLifecycleStatus({
|
||||
installationId: "cm-install-1",
|
||||
profileKey: "scum-client-manager",
|
||||
status: "online",
|
||||
deploymentGeneration: "2",
|
||||
actions: "stop",
|
||||
healthReason: "Bearer stolen-session"
|
||||
})).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user