fix server creation run binding

This commit is contained in:
npc0-hue
2026-08-04 17:14:28 +08:00
parent 980f1786dd
commit 5fbe8092b9
13 changed files with 80 additions and 53 deletions
+1 -1
View File
@@ -621,7 +621,7 @@ describe("PlatformApiClient AI providers", () => {
await expect(client.listArtifacts({ ownerKind: "job", ownerId: job.id, state: "available" })).resolves.toMatchObject({ count: 1, items: [{ id: artifact.id }] });
await expect(client.openArtifactDownload(artifact.id)).resolves.toMatchObject({ downloadUrl: "/api/v1/artifacts/artifact-1/content", rangeSupported: true });
await expect(client.readArtifactContent(artifact.id, 0, 8)).resolves.toMatchObject({ contentLength: 8, contentRange: "bytes 0-7/18", checksum: artifact.checksum });
await expect(client.createServerWorkflow({ id: "server-2", pluginId: plugin.id, runEndpointId: endpoint.id, name: "Server 2", idempotencyKey: "idem-create", profileKey: "local", bindings: {} })).resolves.toMatchObject({
await expect(client.createServerWorkflow({ id: "server-2", pluginId: plugin.id, name: "Server 2", idempotencyKey: "idem-create" })).resolves.toMatchObject({
action: "create"
});
await expect(client.startServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-start" })).resolves.toMatchObject({ action: "start" });
+1 -1
View File
@@ -22,7 +22,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
## Server Management Workflows
- `createServerWorkflow` posts `ServerLifecycleCreateRequest` with the create-wizard deployment definition to `/server-instances/workflows/create`, including deployment mode, plugin create inputs, and custom startup fields when provided. It does not require deployment target, run endpoint, or runtime profile selection during creation.
- `createServerWorkflow` posts `ServerLifecycleCreateRequest` with the create-wizard deployment definition to `/server-instances/workflows/create`, including deployment mode, plugin create inputs, and custom startup fields when provided. It must not include deployment target, run endpoint, runtime profile, or runtime bindings during creation; those are established only after creation through generated Run registration, runtime binding, or deployment update flows.
- `getServerRuntimeBinding` reads `/server-instances/{id}/runtime-binding`; `updateServerRuntimeBinding` patches the selected profile and logical refs. Responses contain only profile metadata, logical key names, configured/secret-backed flags, missing keys, and safe reasons. They never contain stored refs or secret values.
- `startServerInstance` and `stopServerInstance` post `ServerLifecycleCommandRequest` with the current config version and receive the lifecycle job response.
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
-4
View File
@@ -505,12 +505,8 @@ export interface ServerInstanceListResponse {
export interface ServerLifecycleCreateRequest {
id: string;
pluginId: string;
deploymentTargetId?: string;
runEndpointId?: string;
name: string;
idempotencyKey: string;
profileKey?: string;
bindings?: Record<string, string>;
deployment?: ServerDeploymentRequest;
}
@@ -116,13 +116,17 @@ describe("ServerDeploymentWorkflow", () => {
pluginId: "game.runtime",
name: "Custom Runtime Server",
idempotencyKey: "web:create:server-custom-runtime-server-17:17",
runEndpointId: undefined,
deployment: {
mode: "custom-command",
serverRoot: "/srv/custom-runtime",
startCommand: "./start-runtime.sh"
}
});
expect(submitted && "runEndpointId" in submitted).toBe(false);
expect(submitted && "profileKey" in submitted).toBe(false);
expect(submitted && "bindings" in submitted).toBe(false);
expect(submitted?.deployment && "profileKey" in submitted.deployment).toBe(false);
expect(submitted?.deployment && "runtimeBindings" in submitted.deployment).toBe(false);
});
});
+5
View File
@@ -11,6 +11,7 @@ import { UsersPage } from "./UsersPage";
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
import serverLiveOperationsSource from "../components/ServerLiveOperations.tsx?raw";
import serverCreateSchemaSource from "../schemas/serverManagement.ts?raw";
import serversPageSource from "./ServersPage.tsx?raw";
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
import type { PageComponentProps } from "../contracts/page";
@@ -239,6 +240,10 @@ describe("first-party console pages", () => {
expect(serverDeploymentWorkflowSource).toContain('const configurationStep = kind === "create" ? 2 : needsTargetSelection ? 1 : 0;');
expect(serverDeploymentWorkflowSource).toContain('const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;');
expect(serversPageSource).toContain("serverCreateRequestFromForm(nextForm)");
expect(serverCreateSchemaSource).not.toContain("runEndpointId: form.runEndpointId");
expect(serverCreateSchemaSource).not.toContain("deploymentTargetId: form.deploymentTargetId");
expect(serverCreateSchemaSource).not.toContain("profileKey: form.profileKey");
expect(serverCreateSchemaSource).not.toContain("runtimeBindings:");
});
it("renders server runtime actions as a compact popover trigger instead of an in-card details stack", () => {
+1 -3
View File
@@ -107,9 +107,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
pluginId: plugin?.id ?? "",
profileKey,
bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {},
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId)
? current.runEndpointId
: endpointResponse.items[0]?.id || ""
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId) ? current.runEndpointId : ""
};
});
if (showLoading) {
@@ -53,7 +53,7 @@ describe("runtime profile server creation contracts", () => {
expect(runtimeBindingFields(plugin, "local").some((field) => field.key === "ftp.profile")).toBe(false);
});
it("selects the plugin profile and submits real profile bindings", () => {
it("submits deployment inputs without binding a Run or runtime profile", () => {
const form = defaultServerCreateForm([plugin], []);
expect(form.profileKey).toBe("local");
expect(
@@ -69,15 +69,10 @@ describe("runtime profile server creation contracts", () => {
).toEqual({
id: "server-1",
pluginId: "game.runtime",
runEndpointId: undefined,
name: "Runtime Server",
idempotencyKey: "web:create:server-1:17",
profileKey: "local",
bindings: { "server-root": "runtime.server-root", "rcon.password": "secret://runtime/server-1/rcon" },
deployment: {
mode: "guided-install",
profileKey: "local",
runtimeBindings: { "server-root": "runtime.server-root", "rcon.password": "secret://runtime/server-1/rcon" },
createInputs: { gamePort: "7777", maxPlayers: "64" }
}
});
@@ -117,7 +112,11 @@ describe("runtime profile server creation contracts", () => {
it("keeps complete paths and commands in a write-only deployment payload", () => {
const form = defaultServerCreateForm([plugin], []);
const request = serverCreateRequestFromForm({ ...form, name: "Venv Server", deploymentMode: "custom-command", serverRoot: "/srv/venv-server", workingDirectory: "/srv/venv-server", startCommand: "/srv/venv-server/.venv/bin/python server.py", shell: "" }, 19);
expect(request.runEndpointId).toBeUndefined();
expect("runEndpointId" in request).toBe(false);
expect("profileKey" in request).toBe(false);
expect("bindings" in request).toBe(false);
expect(request.deployment).toMatchObject({ mode: "custom-command", serverRoot: "/srv/venv-server", workingDirectory: "/srv/venv-server", startCommand: "/srv/venv-server/.venv/bin/python server.py" });
expect("profileKey" in (request.deployment ?? {})).toBe(false);
expect("runtimeBindings" in (request.deployment ?? {})).toBe(false);
});
});
-6
View File
@@ -26,16 +26,10 @@ export function serverCreateRequestFromForm(form: ServerCreateFormState, sequenc
return {
id,
pluginId: form.pluginId.trim(),
deploymentTargetId: form.deploymentTargetId.trim() || undefined,
runEndpointId: form.runEndpointId.trim() || undefined,
name: form.name.trim(),
idempotencyKey: lifecycleIdempotencyKey("create", id, sequence),
profileKey: form.profileKey.trim() || undefined,
bindings: Object.fromEntries(Object.entries(form.bindings).map(([key, value]) => [key, value.trim()]).filter(([, value]) => value !== "")),
deployment: {
mode: form.deploymentMode,
profileKey: form.profileKey.trim() || undefined,
runtimeBindings: Object.fromEntries(Object.entries(form.bindings).map(([key, value]) => [key, value.trim()]).filter(([, value]) => value !== "")),
createInputs: Object.fromEntries(Object.entries(form.createInputs).map(([key, value]) => [key, value.trim()])),
serverRoot: form.serverRoot.trim() || undefined,
workingDirectory: form.workingDirectory.trim() || undefined,