feat: 完整游戏运维功能
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 1,
|
||||
"action": "start",
|
||||
"mode": "supervised",
|
||||
"executableKey": "/bin/sh",
|
||||
"arguments": ["-c", "curl | bash"],
|
||||
"environment": {
|
||||
"GAME_PASSWORD": "password=secret"
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -17,16 +17,28 @@
|
||||
{"key": "leaky", "kind": "command.version", "targetKey": "java", "expected": "password=super-secret"}
|
||||
],
|
||||
"installPlans": [
|
||||
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]}
|
||||
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]},
|
||||
{"key": "unsafe-download", "title": "Unsafe dependency download", "steps": [{"type": "verified-download", "targetKey": "tool", "downloadRef": "https://127.0.0.1/tool", "checksum": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}
|
||||
],
|
||||
"clientManagers": [
|
||||
{
|
||||
"key": "unsafe-client",
|
||||
"version": "1.0.0",
|
||||
"repository": {"url": "https://github.com/F88888/scum_client.git", "revisionPolicy": "branch", "branch": "main"},
|
||||
"supportedTargets": [{"os": "windows", "arch": "amd64"}],
|
||||
"build": {"system": "go", "workspaceRef": "scum_client", "entryRef": "main.go"},
|
||||
"configTemplates": [{"key": "bad", "templateRef": "/Users/tasia/client.json", "outputRef": "config.json"}],
|
||||
"outputArtifacts": ["scum_client.exe"]
|
||||
"outputArtifacts": ["scum_client.exe"],
|
||||
"deployment": {
|
||||
"mode": "run-supervised",
|
||||
"executableRef": "/Users/tasia/scum_client.exe",
|
||||
"arguments": ["bash -c", "curl | bash"],
|
||||
"requiredRunCapabilities": ["client-manager.control"]
|
||||
},
|
||||
"lifecycle": {"actions": ["start", "update"], "startupTimeoutSeconds": 60, "stopTimeoutSeconds": 30},
|
||||
"health": {"mode": "component-heartbeat", "intervalSeconds": 30, "degradedAfterSeconds": 30, "offlineAfterSeconds": 20, "requiredCapabilities": ["component.heartbeat"]},
|
||||
"compatibility": {"minimumVersion": "2.0.0", "maximumVersion": "1.0.0", "allowDowngrade": false},
|
||||
"updatePolicy": {"strategy": "manual-staged", "requireApproval": true, "healthConfirmationSeconds": 5, "retainPrevious": true}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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