Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+193 -2
View File
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { PlatformApiClient, setPlatformApiSessionToken } from "./client";
import type { AiProviderResponse, GamePluginResponse, JobResponse, MarketplacePluginResponse, RunEndpointResponse, ServerInstanceResponse } from "./types";
import type { AiProviderResponse, ArtifactDownloadReferenceResponse, GamePluginResponse, JobResponse, MarketplacePluginResponse, RunEndpointResponse, ServerInstanceResponse } from "./types";
const provider: AiProviderResponse = {
id: "ai.openai",
@@ -105,6 +105,37 @@ const artifact = {
updatedAt: "2026-07-03T00:00:00Z"
};
const runtimeActions = {
serverInstanceId: server.id,
pluginId: plugin.id,
runEndpointId: endpoint.id,
runStatus: "online",
actions: [
{ key: "generate-run", label: "Generate run", available: true },
{ key: "download-run", label: "Download run", available: true },
{ key: "push-run-update", label: "Push run update", available: true },
{ key: "generate-client-manager", label: "Generate client manager", available: true },
{ key: "dependencies-check", label: "Check dependencies", available: true },
{ key: "historical-logs", label: "Historical logs", available: true }
]
};
const runtimeDownload: ArtifactDownloadReferenceResponse = {
artifactId: "artifact-run-1",
ownerKind: "server-instance",
ownerId: server.id,
filename: "run-linux-amd64.zip",
contentType: "application/zip",
sizeBytes: 128,
checksum: "sha256:runchecksum",
state: "available",
downloadUrl: "/api/v1/artifacts/artifact-run-1/content",
expiresAt: "2026-07-03T00:15:00Z",
rangeSupported: true,
chunkSizeBytes: 1048576,
storageBehavior: "platform-memory-transfer-session"
};
describe("PlatformApiClient AI providers", () => {
afterEach(() => {
setPlatformApiSessionToken(null);
@@ -160,6 +191,13 @@ describe("PlatformApiClient AI providers", () => {
if (url.endsWith("/api/v1/server-instances") && (!init?.method || init.method === "GET")) {
return jsonResponse({ items: [server], count: 1 });
}
if (url.endsWith("/api/v1/server-instances/server-1") && init?.method === "PUT") {
expect(JSON.parse(String(init.body))).toEqual({ name: "Example Survival Renamed" });
return jsonResponse({ ...server, name: "Example Survival Renamed" });
}
if (url.endsWith("/api/v1/server-instances/server-1") && init?.method === "DELETE") {
return new Response(null, { status: 204 });
}
if (url.endsWith("/api/v1/metrics/platform")) {
return jsonResponse({ cpuPercent: 28, memoryPercent: 42, diskPercent: 19, source: "platform-derived", collectedAt: "2026-07-03T00:00:00Z" });
}
@@ -314,6 +352,122 @@ describe("PlatformApiClient AI providers", () => {
if (url.endsWith("/api/v1/server-instances/server-1/administrators/user-2") && init?.method === "DELETE") {
return jsonResponse({ ...server, adminUserIds: [] });
}
if (url.endsWith("/api/v1/server-instances/server-1/runtime/actions")) {
return jsonResponse(runtimeActions);
}
if (url.endsWith("/api/v1/server-instances/server-1/run/generate") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ targetOs: "linux", targetArch: "amd64", idempotencyKey: "idem-run-generate" });
return jsonResponse({
id: "run-dist-1",
serverInstanceId: server.id,
pluginId: plugin.id,
runEndpointId: endpoint.id,
targetOs: "linux",
targetArch: "amd64",
packageFormat: "zip",
artifactId: "artifact-run-1",
checksum: "sha256:runchecksum",
keyGeneration: 1,
secretRef: "secret://runtime-keys/server-1/run/current",
status: "available",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/run/download") && init?.method === "POST") {
return jsonResponse(runtimeDownload);
}
if (url.endsWith("/api/v1/server-instances/server-1/run/key/reset") && init?.method === "POST") {
return jsonResponse({
id: "runtime-key-server-1-run-2",
serverInstanceId: server.id,
componentKind: "run",
secretRef: "secret://runtime-keys/server-1/run/current",
fingerprint: "abc123def456",
generation: 2,
status: "active",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/run/update") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ artifactId: "artifact-run-1", checksum: "sha256:runchecksum", idempotencyKey: "idem-run-update" });
return jsonResponse({
id: "run-update-1",
serverInstanceId: server.id,
runEndpointId: endpoint.id,
artifactId: "artifact-run-1",
checksum: "sha256:runchecksum",
jobId: "job-run-update",
idempotencyKey: "idem-run-update",
status: "queued",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/generate") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({
profileKey: "scum-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
});
return jsonResponse({
id: "client-dist-1",
serverInstanceId: server.id,
pluginId: plugin.id,
profileKey: "scum-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
sourceRevision: "main",
buildJobId: "client-build-1",
artifactId: "artifact-client-1",
checksum: "sha256:clientchecksum",
keyGeneration: 1,
secretRef: "secret://runtime-keys/server-1/client-manager/scum-client-manager/current",
status: "available",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/download") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ profileKey: "scum-client-manager" });
return jsonResponse({ ...runtimeDownload, artifactId: "artifact-client-1", filename: "scum-client-manager.exe" });
}
if (url.endsWith("/api/v1/server-instances/server-1/client-managers/key/reset") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ componentKind: "client-manager", componentKey: "scum-client-manager" });
return jsonResponse({
id: "runtime-key-server-1-client-2",
serverInstanceId: server.id,
componentKind: "client-manager",
componentKey: "scum-client-manager",
secretRef: "secret://runtime-keys/server-1/client-manager/scum-client-manager/current",
fingerprint: "def456abc123",
generation: 2,
status: "active",
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
});
}
if (url.endsWith("/api/v1/server-instances/server-1/dependencies/check") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ probeKey: "java-21", idempotencyKey: "idem-dep-check" });
return jsonResponse({ ...job, id: "job-dep-check", capability: "dependencies.check", targetKey: "dependencies/java-21" });
}
if (url.endsWith("/api/v1/server-instances/server-1/dependencies/install") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ probeKey: "java-21", installPlanKey: "install-java-linux", idempotencyKey: "idem-dep-install" });
return jsonResponse({ ...job, id: "job-dep-install", capability: "dependencies.install", targetKey: "dependencies/install/install-java-linux" });
}
if (url.endsWith("/api/v1/server-instances/server-1/logs/live")) {
return jsonResponse({ items: [], count: 0 });
}
if (url.endsWith("/api/v1/server-instances/server-1/logs/backfill") && init?.method === "POST") {
expect(JSON.parse(String(init.body))).toEqual({ sourceKey: "latest-log", checkpointRef: "artifact://logs/checkpoint/1", limit: 200, idempotencyKey: "idem-log-backfill" });
return jsonResponse({ ...job, id: "job-log-backfill", capability: "logs.backfill", targetKey: "logs/latest-log", inputRef: "artifact://logs/checkpoint/1" });
}
if (url.endsWith("/api/v1/plugin-bridge/authorize") && init?.method === "POST") {
return jsonResponse({
pluginId: plugin.id,
@@ -371,6 +525,8 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.health()).resolves.toMatchObject({ status: "ok" });
await expect(client.listGamePlugins()).resolves.toMatchObject({ count: 1 });
await expect(client.listServerInstances()).resolves.toMatchObject({ count: 1 });
await expect(client.updateServerInstance(server.id, { name: "Example Survival Renamed" })).resolves.toMatchObject({ name: "Example Survival Renamed" });
await expect(client.archiveServerInstance(server.id)).resolves.toBeUndefined();
await expect(client.getPlatformResourceUsage()).resolves.toMatchObject({ source: "platform-derived", cpuPercent: 28 });
await expect(client.listServerMetrics()).resolves.toMatchObject({ count: 1, items: [{ serverInstanceId: server.id, online: true }] });
await expect(client.getServerConfig(server.id)).resolves.toMatchObject({ content: "server.name=Example Survival #1\n" });
@@ -398,6 +554,41 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.listServerAdministratorCandidates(server.id)).resolves.toMatchObject({ count: 1 });
await expect(client.addServerAdministrator(server.id, { userId: "user-2" })).resolves.toMatchObject({ adminUserIds: ["user-admin-1", "user-2"] });
await expect(client.removeServerAdministrator(server.id, "user-2")).resolves.toMatchObject({ adminUserIds: [] });
const runtime = await client.getServerRuntimeActions(server.id);
expect(runtime.runStatus).toBe("online");
expect(runtime.actions.some((action) => action.key === "generate-run" && action.available)).toBe(true);
await expect(client.generateRunDistribution(server.id, { targetOs: "linux", targetArch: "amd64", idempotencyKey: "idem-run-generate" })).resolves.toMatchObject({
artifactId: "artifact-run-1",
keyGeneration: 1,
secretRef: "secret://runtime-keys/server-1/run/current"
});
await expect(client.downloadLatestRunDistribution(server.id)).resolves.toMatchObject({ artifactId: "artifact-run-1", rangeSupported: true });
await expect(client.resetRunKey(server.id)).resolves.toMatchObject({ componentKind: "run", generation: 2 });
await expect(client.pushRunUpdate(server.id, { artifactId: "artifact-run-1", checksum: "sha256:runchecksum", idempotencyKey: "idem-run-update" })).resolves.toMatchObject({
jobId: "job-run-update",
status: "queued"
});
await expect(
client.generateClientManager(server.id, {
profileKey: "scum-client-manager",
targetOs: "windows",
targetArch: "amd64",
repositoryUrl: "https://github.com/F88888/scum_client.git",
sourceRevision: "main",
idempotencyKey: "idem-client-generate"
})
).resolves.toMatchObject({ artifactId: "artifact-client-1", profileKey: "scum-client-manager" });
await expect(client.downloadLatestClientManager(server.id, { profileKey: "scum-client-manager" })).resolves.toMatchObject({ artifactId: "artifact-client-1" });
await expect(client.resetClientManagerKey(server.id, { componentKind: "client-manager", componentKey: "scum-client-manager" })).resolves.toMatchObject({ generation: 2 });
await expect(client.checkDependencies(server.id, { probeKey: "java-21", idempotencyKey: "idem-dep-check" })).resolves.toMatchObject({ capability: "dependencies.check" });
await expect(client.installDependencies(server.id, { probeKey: "java-21", installPlanKey: "install-java-linux", idempotencyKey: "idem-dep-install" })).resolves.toMatchObject({
capability: "dependencies.install"
});
await expect(client.listServerLiveLogs(server.id)).resolves.toMatchObject({ count: 0 });
await expect(client.requestLogBackfill(server.id, { sourceKey: "latest-log", checkpointRef: "artifact://logs/checkpoint/1", limit: 200, idempotencyKey: "idem-log-backfill" })).resolves.toMatchObject({
capability: "logs.backfill"
});
await expect(client.authorizePluginBridge({ pluginId: plugin.id, routeKey: "logs", action: "logs.query" })).resolves.toMatchObject({
allowed: true
});
@@ -408,7 +599,7 @@ describe("PlatformApiClient AI providers", () => {
client.invokeAI({ requestId: "ai-1", serverInstanceId: server.id, purpose: "config.suggest", prompt: "Tune PVP safely", currentConfig: "server.name=Example Survival #1\n" })
).resolves.toMatchObject({ status: "ok", usage: { mocked: true }, configRecommendation: { diffSummary: "review required" } });
expect(fetchMock).toHaveBeenCalledTimes(24);
expect(fetchMock).toHaveBeenCalledTimes(38);
});
it("calls plugin marketplace endpoints with filter and state contracts", async () => {
+102
View File
@@ -15,7 +15,13 @@ import type {
ArtifactListResponse,
AuthSessionResponse,
AuditEventListResponse,
ClientManagerBuildRequest,
ClientManagerDistributionResponse,
ClientManagerDownloadRequest,
ComponentKeyResponse,
ComponentKeyResetRequest,
CurrentUserResponse,
DependencyJobRequest,
FileOperationDispatchRequest,
FileOperationDispatchResponse,
GamePluginListResponse,
@@ -25,6 +31,7 @@ import type {
JobResponse,
LlmConfigSuggestionRequest,
LlmConfigSuggestionResponse,
LogBackfillRequest,
LogStreamCursorRequest,
LogStreamCursorResponse,
LogStreamListResponse,
@@ -39,7 +46,11 @@ import type {
PluginBridgeExecuteRequest,
PluginBridgeExecuteResponse,
RegisterRequest,
RunDistributionGenerateRequest,
RunDistributionResponse,
RunEndpointListResponse,
RunUpdateJobResponse,
RunUpdateRequest,
ServerConfigResponse,
ServerConfigDiffPreviewRequest,
ServerConfigDiffPreviewResponse,
@@ -49,10 +60,12 @@ import type {
ServerConfigWriteApprovalRequest,
ServerConfigWriteDispatchResponse,
ServerInstanceListResponse,
ServerInstanceUpdateRequest,
ServerInstanceResponse,
ServerMemberListResponse,
ServerMemberRequest,
ServerMetricsListResponse,
ServerRuntimeActionsResponse,
UserCreateRequest,
UserListResponse,
UserProfileUpdateRequest,
@@ -186,6 +199,84 @@ export class PlatformApiClient {
return this.request<JobResponse>(`/jobs/${encodeURIComponent(id)}`);
}
async getServerRuntimeActions(id: string): Promise<ServerRuntimeActionsResponse> {
return this.request<ServerRuntimeActionsResponse>(`/server-instances/${encodeURIComponent(id)}/runtime/actions`);
}
async generateRunDistribution(id: string, request: RunDistributionGenerateRequest): Promise<RunDistributionResponse> {
return this.request<RunDistributionResponse>(`/server-instances/${encodeURIComponent(id)}/run/generate`, {
method: "POST",
body: request
});
}
async downloadLatestRunDistribution(id: string): Promise<ArtifactDownloadReferenceResponse> {
return this.request<ArtifactDownloadReferenceResponse>(`/server-instances/${encodeURIComponent(id)}/run/download`, {
method: "POST",
body: {}
});
}
async resetRunKey(id: string): Promise<ComponentKeyResponse> {
return this.request<ComponentKeyResponse>(`/server-instances/${encodeURIComponent(id)}/run/key/reset`, {
method: "POST",
body: {}
});
}
async pushRunUpdate(id: string, request: RunUpdateRequest): Promise<RunUpdateJobResponse> {
return this.request<RunUpdateJobResponse>(`/server-instances/${encodeURIComponent(id)}/run/update`, {
method: "POST",
body: request
});
}
async generateClientManager(id: string, request: ClientManagerBuildRequest): Promise<ClientManagerDistributionResponse> {
return this.request<ClientManagerDistributionResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/generate`, {
method: "POST",
body: request
});
}
async downloadLatestClientManager(id: string, request: ClientManagerDownloadRequest = {}): Promise<ArtifactDownloadReferenceResponse> {
return this.request<ArtifactDownloadReferenceResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/download`, {
method: "POST",
body: request
});
}
async resetClientManagerKey(id: string, request: ComponentKeyResetRequest): Promise<ComponentKeyResponse> {
return this.request<ComponentKeyResponse>(`/server-instances/${encodeURIComponent(id)}/client-managers/key/reset`, {
method: "POST",
body: request
});
}
async checkDependencies(id: string, request: DependencyJobRequest): Promise<JobResponse> {
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/dependencies/check`, {
method: "POST",
body: request
});
}
async installDependencies(id: string, request: DependencyJobRequest): Promise<JobResponse> {
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/dependencies/install`, {
method: "POST",
body: request
});
}
async listServerLiveLogs(id: string): Promise<LogStreamListResponse> {
return this.request<LogStreamListResponse>(`/server-instances/${encodeURIComponent(id)}/logs/live`);
}
async requestLogBackfill(id: string, request: LogBackfillRequest): Promise<JobResponse> {
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/logs/backfill`, {
method: "POST",
body: request
});
}
async createJob(request: JobCreateRequest): Promise<JobResponse> {
return this.request<JobResponse>("/jobs", { method: "POST", body: request });
}
@@ -230,6 +321,17 @@ export class PlatformApiClient {
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}`);
}
async updateServerInstance(id: string, request: ServerInstanceUpdateRequest): Promise<ServerInstanceResponse> {
return this.request<ServerInstanceResponse>(`/server-instances/${encodeURIComponent(id)}`, {
method: "PUT",
body: request
});
}
async archiveServerInstance(id: string): Promise<void> {
return this.request<void>(`/server-instances/${encodeURIComponent(id)}`, { method: "DELETE", parseJson: false });
}
async getPlatformResourceUsage(): Promise<PlatformResourceUsageResponse> {
return this.request<PlatformResourceUsageResponse>("/metrics/platform");
}
+127
View File
@@ -109,6 +109,10 @@ export interface ServerInstanceResponse {
updatedAt: string;
}
export interface ServerInstanceUpdateRequest {
name?: string;
}
export interface ServerInstanceListResponse {
items: ServerInstanceResponse[];
count: number;
@@ -233,6 +237,129 @@ export interface ArtifactContentChunk {
storageBehavior?: string;
}
export interface ServerRuntimeActionResponse {
key: string;
label: string;
available: boolean;
reason?: string;
}
export interface ServerRuntimeActionsResponse {
serverInstanceId: string;
pluginId: string;
runEndpointId: string;
runStatus: string;
actions: ServerRuntimeActionResponse[];
}
export interface RunDistributionGenerateRequest {
targetOs: string;
targetArch: string;
idempotencyKey?: string;
}
export interface RunDistributionResponse {
id: string;
serverInstanceId: string;
pluginId: string;
runEndpointId: string;
targetOs: string;
targetArch: string;
packageFormat: string;
artifactId: string;
checksum: string;
keyGeneration: number;
secretRef: string;
status: string;
createdAt: string;
updatedAt: string;
}
export interface RunUpdateRequest {
artifactId: string;
checksum?: string;
idempotencyKey?: string;
}
export interface RunUpdateJobResponse {
id: string;
serverInstanceId: string;
runEndpointId: string;
artifactId: string;
checksum: string;
jobId?: string;
idempotencyKey?: string;
status: string;
createdAt: string;
updatedAt: string;
}
export interface ClientManagerBuildRequest {
profileKey: string;
targetOs: string;
targetArch: string;
repositoryUrl: string;
sourceRevision?: string;
idempotencyKey?: string;
}
export interface ClientManagerDistributionResponse {
id: string;
serverInstanceId: string;
pluginId: string;
profileKey: string;
targetOs: string;
targetArch: string;
repositoryUrl: string;
sourceRevision: string;
buildJobId: string;
artifactId: string;
checksum: string;
keyGeneration: number;
secretRef: string;
status: string;
createdAt: string;
updatedAt: string;
}
export interface ClientManagerDownloadRequest {
profileKey?: string;
}
export interface ComponentKeyResetRequest {
componentKind: "run" | "client-manager" | string;
componentKey?: string;
}
export interface ComponentKeyResponse {
id: string;
serverInstanceId: string;
componentKind: string;
componentKey?: string;
secretRef: string;
fingerprint: string;
generation: number;
status: string;
createdAt: string;
updatedAt: string;
resetAt?: string;
}
export interface DependencyJobRequest {
probeKey: string;
installPlanKey?: string;
targetOs?: string;
targetArch?: string;
idempotencyKey?: string;
}
export interface LogBackfillRequest {
sourceKey: string;
checkpointRef?: string;
limit?: number;
idempotencyKey?: string;
}
export interface PluginBridgeAuthorizeRequest {
pluginId: string;
routeKey: string;