fix server creation run binding
This commit is contained in:
@@ -783,14 +783,22 @@ func TestServerLifecycleWorkflowAPI(t *testing.T) {
|
|||||||
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{
|
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{
|
||||||
ID: "server-create",
|
ID: "server-create",
|
||||||
PluginID: "server.scum",
|
PluginID: "server.scum",
|
||||||
RunEndpointID: "run-local",
|
|
||||||
Name: "SCUM Create",
|
Name: "SCUM Create",
|
||||||
IdempotencyKey: "idem-create",
|
IdempotencyKey: "idem-create",
|
||||||
ProfileKey: "local",
|
|
||||||
}, adminSession)
|
}, adminSession)
|
||||||
if created.Action != domain.ServerLifecycleActionCreate || created.Instance.State != domain.ServerInstanceStateInstalling || created.Job.Capability != domain.LifecycleCapabilityInstall {
|
if created.Action != domain.ServerLifecycleActionCreate || created.Instance.State != domain.ServerInstanceStateDraft || created.Job.ID != "" || created.Instance.RunEndpointID != "" {
|
||||||
t.Fatalf("expected create workflow response, got %+v", created)
|
t.Fatalf("expected create workflow response, got %+v", created)
|
||||||
}
|
}
|
||||||
|
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{
|
||||||
|
ID: "server-create-legacy",
|
||||||
|
PluginID: "server.scum",
|
||||||
|
RunEndpointID: "run-local",
|
||||||
|
Name: "SCUM Legacy Create",
|
||||||
|
IdempotencyKey: "idem-create-legacy",
|
||||||
|
ProfileKey: "local",
|
||||||
|
Bindings: map[string]string{"server-root": "runtime.server-root"},
|
||||||
|
}, adminSession)
|
||||||
|
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeValidation)
|
||||||
|
|
||||||
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||||
ID: "server-ready",
|
ID: "server-ready",
|
||||||
@@ -844,7 +852,7 @@ func TestServerDeploymentRevealAPIIsExplicitAndOwnerScoped(t *testing.T) {
|
|||||||
endpoint := validRunEndpointRequest()
|
endpoint := validRunEndpointRequest()
|
||||||
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
|
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
|
||||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpoint)
|
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpoint)
|
||||||
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "deployment-reveal", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Reveal", IdempotencyKey: "deployment-reveal", ProfileKey: "local", Deployment: dto.ServerDeploymentRequest{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/reveal", WorkingDirectory: "/srv/reveal", StartCommand: "./start-server"}}, adminSession)
|
postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "deployment-reveal", PluginID: "server.scum", Name: "Reveal", IdempotencyKey: "deployment-reveal", Deployment: dto.ServerDeploymentRequest{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/reveal", WorkingDirectory: "/srv/reveal", StartCommand: "./start-server"}}, adminSession)
|
||||||
|
|
||||||
redactedRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/deployment-reveal/deployment", "", adminSession)
|
redactedRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/deployment-reveal/deployment", "", adminSession)
|
||||||
assertStatus(t, redactedRecorder, http.StatusOK)
|
assertStatus(t, redactedRecorder, http.StatusOK)
|
||||||
@@ -852,8 +860,8 @@ func TestServerDeploymentRevealAPIIsExplicitAndOwnerScoped(t *testing.T) {
|
|||||||
t.Fatalf("normal deployment view leaked protected inputs: %s", body)
|
t.Fatalf("normal deployment view leaked protected inputs: %s", body)
|
||||||
}
|
}
|
||||||
redacted := decodeBody[dto.ServerDeploymentResponse](t, redactedRecorder)
|
redacted := decodeBody[dto.ServerDeploymentResponse](t, redactedRecorder)
|
||||||
if redacted.LatestDispatch == nil || !redacted.LatestDispatch.DeploymentDefinitionIncluded || redacted.LatestDispatch.JobID != created.Job.ID || redacted.LatestDispatch.DeploymentRevision != 1 {
|
if redacted.LatestDispatch != nil {
|
||||||
t.Fatalf("expected safe deployment dispatch evidence, got %+v", redacted.LatestDispatch)
|
t.Fatalf("create-time deployment settings must not dispatch to Run, got %+v", redacted.LatestDispatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
revealed := getJSONWithAuth[dto.ServerDeploymentRevealResponse](t, router, "/api/v1/server-instances/deployment-reveal/deployment/reveal", adminSession)
|
revealed := getJSONWithAuth[dto.ServerDeploymentRevealResponse](t, router, "/api/v1/server-instances/deployment-reveal/deployment/reveal", adminSession)
|
||||||
@@ -1631,10 +1639,8 @@ func TestRuntimeBindingAPIIsAuthorizedValidatedAndRedacted(t *testing.T) {
|
|||||||
undeclared := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/"+server.ID+"/runtime-binding", dto.RuntimeBindingUpdateRequest{ProfileKey: "local", Bindings: map[string]string{"host.socket": "runtime.socket"}}, adminSession)
|
undeclared := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/"+server.ID+"/runtime-binding", dto.RuntimeBindingUpdateRequest{ProfileKey: "local", Bindings: map[string]string{"host.socket": "runtime.socket"}}, adminSession)
|
||||||
assertErrorResponse(t, undeclared, http.StatusBadRequest, errorCodeValidation)
|
assertErrorResponse(t, undeclared, http.StatusBadRequest, errorCodeValidation)
|
||||||
|
|
||||||
created := postOKJSONWithAuth[dto.ServerLifecycleResponse](t, router, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-complete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Complete", IdempotencyKey: "runtime-create-complete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root", "rcon.password": "secret://runtime-create-complete/rcon"}}, adminSession)
|
legacyCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-complete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Complete", IdempotencyKey: "runtime-create-complete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root", "rcon.password": "secret://runtime-create-complete/rcon"}}, adminSession)
|
||||||
if created.Job.TargetKey != "actions/install.json" {
|
assertErrorResponse(t, legacyCreate, http.StatusBadRequest, errorCodeValidation)
|
||||||
t.Fatalf("create workflow did not dispatch selected profile: %+v", created.Job)
|
|
||||||
}
|
|
||||||
incompleteCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-incomplete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Incomplete", IdempotencyKey: "runtime-create-incomplete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root"}}, adminSession)
|
incompleteCreate := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/workflows/create", dto.ServerLifecycleCreateRequest{ID: "runtime-create-incomplete", PluginID: registration.Manifest.ID, RunEndpointID: "run-local", Name: "Runtime Create Incomplete", IdempotencyKey: "runtime-create-incomplete", ProfileKey: "local", Bindings: map[string]string{"server-root": "runtime.server-root"}}, adminSession)
|
||||||
assertErrorResponse(t, incompleteCreate, http.StatusBadRequest, errorCodeValidation)
|
assertErrorResponse(t, incompleteCreate, http.StatusBadRequest, errorCodeValidation)
|
||||||
missingServer := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/runtime-create-incomplete", "", adminSession)
|
missingServer := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/runtime-create-incomplete", "", adminSession)
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ Artifact bridge execution returns safe metadata and platform content routes only
|
|||||||
|
|
||||||
## Implemented Server Lifecycle Actions
|
## Implemented Server Lifecycle Actions
|
||||||
|
|
||||||
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. Creation starts from `pluginId` and `name`, and may include the create-wizard deployment definition such as deployment mode, plugin create inputs, server root, or custom start command. The browser must not require a deployment target, run endpoint, or runtime profile at creation; those bindings remain post-creation/runtime-registration concerns. Legacy `runEndpointId` and `deploymentTargetId` inputs remain accepted for compatible programmatic flows, but are never prerequisites for creation and do not select a distribution builder.
|
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. Creation starts from `pluginId` and `name`, and may include the create-wizard deployment definition such as deployment mode, plugin create inputs, server root, or custom start command. The browser must not require or submit a deployment target, run endpoint, runtime profile, or runtime bindings during creation; those bindings remain post-creation/runtime-registration concerns. Requests that include `deploymentTargetId`, `runEndpointId`, `profileKey`, `bindings`, `deployment.runEndpointId`, `deployment.profileKey`, or `deployment.runtimeBindings` are rejected.
|
||||||
- `POST /api/v1/server-instances/{id}/start`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.start` job using `ServerLifecycleResponse`.
|
- `POST /api/v1/server-instances/{id}/start`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.start` job using `ServerLifecycleResponse`.
|
||||||
- `POST /api/v1/server-instances/{id}/stop`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.stop` job using `ServerLifecycleResponse`.
|
- `POST /api/v1/server-instances/{id}/stop`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.stop` job using `ServerLifecycleResponse`.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (h *coreHandlers) serverInstanceCreateWorkflow(w http.ResponseWriter, r *ht
|
|||||||
writeDecodeError(w, err)
|
writeDecodeError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := request.ValidateCreateOnly(); err != nil {
|
||||||
|
writeServiceError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
result, err := h.core.CreateServerInstanceWorkflowForSession(bearerToken(r), request.ToDomain())
|
result, err := h.core.CreateServerInstanceWorkflowForSession(bearerToken(r), request.ToDomain())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeServiceError(w, err)
|
writeServiceError(w, err)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"browser.local/platform/domain"
|
"browser.local/platform/domain"
|
||||||
|
"browser.local/platform/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerDeploymentRequest struct {
|
type ServerDeploymentRequest struct {
|
||||||
@@ -78,7 +80,7 @@ type ServerLifecycleCreateRequest struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
PluginID string `json:"pluginId"`
|
PluginID string `json:"pluginId"`
|
||||||
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
|
DeploymentTargetID string `json:"deploymentTargetId,omitempty"`
|
||||||
RunEndpointID string `json:"runEndpointId"`
|
RunEndpointID string `json:"runEndpointId,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
OwnerUserID string `json:"ownerUserId,omitempty"`
|
OwnerUserID string `json:"ownerUserId,omitempty"`
|
||||||
IdempotencyKey string `json:"idempotencyKey"`
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
@@ -87,6 +89,35 @@ type ServerLifecycleCreateRequest struct {
|
|||||||
Deployment ServerDeploymentRequest `json:"deployment,omitempty"`
|
Deployment ServerDeploymentRequest `json:"deployment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (request ServerLifecycleCreateRequest) ValidateCreateOnly() error {
|
||||||
|
var violations []string
|
||||||
|
if strings.TrimSpace(request.DeploymentTargetID) != "" {
|
||||||
|
violations = append(violations, "deploymentTargetId must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(request.RunEndpointID) != "" {
|
||||||
|
violations = append(violations, "runEndpointId must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(request.ProfileKey) != "" {
|
||||||
|
violations = append(violations, "profileKey must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if len(request.Bindings) > 0 {
|
||||||
|
violations = append(violations, "bindings must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(request.Deployment.RunEndpointID) != "" {
|
||||||
|
violations = append(violations, "deployment.runEndpointId must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(request.Deployment.ProfileKey) != "" {
|
||||||
|
violations = append(violations, "deployment.profileKey must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if len(request.Deployment.RuntimeBindings) > 0 {
|
||||||
|
violations = append(violations, "deployment.runtimeBindings must not be provided during server creation")
|
||||||
|
}
|
||||||
|
if len(violations) > 0 {
|
||||||
|
return validator.ValidationError{Violations: violations}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ServerLifecycleCommandRequest struct {
|
type ServerLifecycleCommandRequest struct {
|
||||||
ExpectedConfigVersion int `json:"expectedConfigVersion"`
|
ExpectedConfigVersion int `json:"expectedConfigVersion"`
|
||||||
IdempotencyKey string `json:"idempotencyKey"`
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
@@ -103,13 +134,13 @@ func (request ServerLifecycleCreateRequest) ToDomain() domain.ServerLifecycleCre
|
|||||||
return domain.ServerLifecycleCreate{
|
return domain.ServerLifecycleCreate{
|
||||||
ID: request.ID,
|
ID: request.ID,
|
||||||
PluginID: request.PluginID,
|
PluginID: request.PluginID,
|
||||||
DeploymentTargetID: request.DeploymentTargetID,
|
DeploymentTargetID: "",
|
||||||
RunEndpointID: request.RunEndpointID,
|
RunEndpointID: "",
|
||||||
Name: request.Name,
|
Name: request.Name,
|
||||||
OwnerUserID: request.OwnerUserID,
|
OwnerUserID: request.OwnerUserID,
|
||||||
IdempotencyKey: request.IdempotencyKey,
|
IdempotencyKey: request.IdempotencyKey,
|
||||||
ProfileKey: request.ProfileKey,
|
ProfileKey: "",
|
||||||
Bindings: domain.CopyStringMap(request.Bindings),
|
Bindings: nil,
|
||||||
Deployment: request.Deployment.deploymentDefinition(),
|
Deployment: request.Deployment.deploymentDefinition(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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.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.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"
|
action: "create"
|
||||||
});
|
});
|
||||||
await expect(client.startServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-start" })).resolves.toMatchObject({ action: "start" });
|
await expect(client.startServerInstance(server.id, { expectedConfigVersion: 1, idempotencyKey: "idem-start" })).resolves.toMatchObject({ action: "start" });
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Normal browser login uses the platform's HttpOnly SameSite cookie and `credentia
|
|||||||
|
|
||||||
## Server Management Workflows
|
## 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.
|
- `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.
|
- `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.
|
- `listServerAdministratorCandidates`, `addServerAdministrator`, and `removeServerAdministrator` call server membership endpoints so server owners can invite or remove active non-platform-admin server administrators.
|
||||||
|
|||||||
@@ -505,12 +505,8 @@ export interface ServerInstanceListResponse {
|
|||||||
export interface ServerLifecycleCreateRequest {
|
export interface ServerLifecycleCreateRequest {
|
||||||
id: string;
|
id: string;
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
deploymentTargetId?: string;
|
|
||||||
runEndpointId?: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
profileKey?: string;
|
|
||||||
bindings?: Record<string, string>;
|
|
||||||
deployment?: ServerDeploymentRequest;
|
deployment?: ServerDeploymentRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,13 +116,17 @@ describe("ServerDeploymentWorkflow", () => {
|
|||||||
pluginId: "game.runtime",
|
pluginId: "game.runtime",
|
||||||
name: "Custom Runtime Server",
|
name: "Custom Runtime Server",
|
||||||
idempotencyKey: "web:create:server-custom-runtime-server-17:17",
|
idempotencyKey: "web:create:server-custom-runtime-server-17:17",
|
||||||
runEndpointId: undefined,
|
|
||||||
deployment: {
|
deployment: {
|
||||||
mode: "custom-command",
|
mode: "custom-command",
|
||||||
serverRoot: "/srv/custom-runtime",
|
serverRoot: "/srv/custom-runtime",
|
||||||
startCommand: "./start-runtime.sh"
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { UsersPage } from "./UsersPage";
|
|||||||
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
|
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
|
||||||
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
|
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
|
||||||
import serverLiveOperationsSource from "../components/ServerLiveOperations.tsx?raw";
|
import serverLiveOperationsSource from "../components/ServerLiveOperations.tsx?raw";
|
||||||
|
import serverCreateSchemaSource from "../schemas/serverManagement.ts?raw";
|
||||||
import serversPageSource from "./ServersPage.tsx?raw";
|
import serversPageSource from "./ServersPage.tsx?raw";
|
||||||
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
||||||
import type { PageComponentProps } from "../contracts/page";
|
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 configurationStep = kind === "create" ? 2 : needsTargetSelection ? 1 : 0;');
|
||||||
expect(serverDeploymentWorkflowSource).toContain('const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;');
|
expect(serverDeploymentWorkflowSource).toContain('const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;');
|
||||||
expect(serversPageSource).toContain("serverCreateRequestFromForm(nextForm)");
|
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", () => {
|
it("renders server runtime actions as a compact popover trigger instead of an in-card details stack", () => {
|
||||||
|
|||||||
@@ -107,9 +107,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
pluginId: plugin?.id ?? "",
|
pluginId: plugin?.id ?? "",
|
||||||
profileKey,
|
profileKey,
|
||||||
bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {},
|
bindings: plugin?.id === current.pluginId && profileKey === current.profileKey ? current.bindings : {},
|
||||||
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId)
|
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId) ? current.runEndpointId : ""
|
||||||
? current.runEndpointId
|
|
||||||
: endpointResponse.items[0]?.id || ""
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe("runtime profile server creation contracts", () => {
|
|||||||
expect(runtimeBindingFields(plugin, "local").some((field) => field.key === "ftp.profile")).toBe(false);
|
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], []);
|
const form = defaultServerCreateForm([plugin], []);
|
||||||
expect(form.profileKey).toBe("local");
|
expect(form.profileKey).toBe("local");
|
||||||
expect(
|
expect(
|
||||||
@@ -69,15 +69,10 @@ describe("runtime profile server creation contracts", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
id: "server-1",
|
id: "server-1",
|
||||||
pluginId: "game.runtime",
|
pluginId: "game.runtime",
|
||||||
runEndpointId: undefined,
|
|
||||||
name: "Runtime Server",
|
name: "Runtime Server",
|
||||||
idempotencyKey: "web:create:server-1:17",
|
idempotencyKey: "web:create:server-1:17",
|
||||||
profileKey: "local",
|
|
||||||
bindings: { "server-root": "runtime.server-root", "rcon.password": "secret://runtime/server-1/rcon" },
|
|
||||||
deployment: {
|
deployment: {
|
||||||
mode: "guided-install",
|
mode: "guided-install",
|
||||||
profileKey: "local",
|
|
||||||
runtimeBindings: { "server-root": "runtime.server-root", "rcon.password": "secret://runtime/server-1/rcon" },
|
|
||||||
createInputs: { gamePort: "7777", maxPlayers: "64" }
|
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", () => {
|
it("keeps complete paths and commands in a write-only deployment payload", () => {
|
||||||
const form = defaultServerCreateForm([plugin], []);
|
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);
|
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(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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,16 +26,10 @@ export function serverCreateRequestFromForm(form: ServerCreateFormState, sequenc
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
pluginId: form.pluginId.trim(),
|
pluginId: form.pluginId.trim(),
|
||||||
deploymentTargetId: form.deploymentTargetId.trim() || undefined,
|
|
||||||
runEndpointId: form.runEndpointId.trim() || undefined,
|
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
idempotencyKey: lifecycleIdempotencyKey("create", id, sequence),
|
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: {
|
deployment: {
|
||||||
mode: form.deploymentMode,
|
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()])),
|
createInputs: Object.fromEntries(Object.entries(form.createInputs).map(([key, value]) => [key, value.trim()])),
|
||||||
serverRoot: form.serverRoot.trim() || undefined,
|
serverRoot: form.serverRoot.trim() || undefined,
|
||||||
workingDirectory: form.workingDirectory.trim() || undefined,
|
workingDirectory: form.workingDirectory.trim() || undefined,
|
||||||
|
|||||||
@@ -847,11 +847,8 @@ cat >"$WORK_DIR/create-scum-beta.request.json" <<JSON
|
|||||||
{
|
{
|
||||||
"id": "$SCUM_BETA_ID",
|
"id": "$SCUM_BETA_ID",
|
||||||
"pluginId": "game.scum",
|
"pluginId": "game.scum",
|
||||||
"runEndpointId": "$RUN_ENDPOINT_ID",
|
|
||||||
"name": "SCUM Beta $SMOKE_INVOCATION_ID",
|
"name": "SCUM Beta $SMOKE_INVOCATION_ID",
|
||||||
"idempotencyKey": "local-debug-scum-beta-create-$SMOKE_INVOCATION_ID",
|
"idempotencyKey": "local-debug-scum-beta-create-$SMOKE_INVOCATION_ID"
|
||||||
"profileKey": "run-local",
|
|
||||||
"bindings": {}
|
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
@@ -859,11 +856,8 @@ cat >"$WORK_DIR/create-scum-dynamic.request.json" <<JSON
|
|||||||
{
|
{
|
||||||
"id": "$SCUM_DYNAMIC_ID",
|
"id": "$SCUM_DYNAMIC_ID",
|
||||||
"pluginId": "game.scum",
|
"pluginId": "game.scum",
|
||||||
"runEndpointId": "$RUN_ENDPOINT_ID",
|
|
||||||
"name": "SCUM Dynamic $SMOKE_INVOCATION_ID",
|
"name": "SCUM Dynamic $SMOKE_INVOCATION_ID",
|
||||||
"idempotencyKey": "local-debug-scum-dynamic-create-$SMOKE_INVOCATION_ID",
|
"idempotencyKey": "local-debug-scum-dynamic-create-$SMOKE_INVOCATION_ID"
|
||||||
"profileKey": "run-local",
|
|
||||||
"bindings": {}
|
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
|
|
||||||
@@ -1000,8 +994,6 @@ curl -fsS -X PUT -H 'Content-Type: application/json' "${AUTH_HEADER[@]}" \
|
|||||||
reject_forbidden_fragments "$WORK_DIR/scum-alpha-runtime-binding.response.json"
|
reject_forbidden_fragments "$WORK_DIR/scum-alpha-runtime-binding.response.json"
|
||||||
require_file_contains "$WORK_DIR/scum-alpha-runtime-binding.response.json" '"status"[[:space:]]*:[[:space:]]*"complete"'
|
require_file_contains "$WORK_DIR/scum-alpha-runtime-binding.response.json" '"status"[[:space:]]*:[[:space:]]*"complete"'
|
||||||
|
|
||||||
wait_for_lifecycle_install_success "$SCUM_DYNAMIC_ID" "$WORK_DIR/scum-dynamic-jobs.response.json"
|
|
||||||
|
|
||||||
printf 'checking SCUM runtime distribution action\n'
|
printf 'checking SCUM runtime distribution action\n'
|
||||||
json_get "$API_URL/server-instances/$SCUM_ALPHA_ID/runtime/actions" "$WORK_DIR/scum-alpha-runtime-actions.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/server-instances/$SCUM_ALPHA_ID/runtime/actions" "$WORK_DIR/scum-alpha-runtime-actions.response.json" "${AUTH_HEADER[@]}"
|
||||||
reject_forbidden_fragments "$WORK_DIR/scum-alpha-runtime-actions.response.json"
|
reject_forbidden_fragments "$WORK_DIR/scum-alpha-runtime-actions.response.json"
|
||||||
@@ -1047,8 +1039,6 @@ printf 'checking jobs, logs, artifacts, and marketplace refs\n'
|
|||||||
json_get "$API_URL/server-instances" "$WORK_DIR/server-instances.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/server-instances" "$WORK_DIR/server-instances.response.json" "${AUTH_HEADER[@]}"
|
||||||
json_get "$API_URL/jobs?serverInstanceId=$SERVER_ID" "$WORK_DIR/jobs.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/jobs?serverInstanceId=$SERVER_ID" "$WORK_DIR/jobs.response.json" "${AUTH_HEADER[@]}"
|
||||||
json_get "$API_URL/jobs?serverInstanceId=$SCUM_ALPHA_ID" "$WORK_DIR/scum-alpha-jobs.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/jobs?serverInstanceId=$SCUM_ALPHA_ID" "$WORK_DIR/scum-alpha-jobs.response.json" "${AUTH_HEADER[@]}"
|
||||||
json_get "$API_URL/jobs?serverInstanceId=$SCUM_BETA_ID" "$WORK_DIR/scum-beta-jobs.response.json" "${AUTH_HEADER[@]}"
|
|
||||||
json_get "$API_URL/jobs?serverInstanceId=$SCUM_DYNAMIC_ID" "$WORK_DIR/scum-dynamic-jobs.response.json" "${AUTH_HEADER[@]}"
|
|
||||||
json_get "$API_URL/log-streams" "$WORK_DIR/log-streams.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/log-streams" "$WORK_DIR/log-streams.response.json" "${AUTH_HEADER[@]}"
|
||||||
json_get "$API_URL/artifacts" "$WORK_DIR/artifacts.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/artifacts" "$WORK_DIR/artifacts.response.json" "${AUTH_HEADER[@]}"
|
||||||
json_get "$API_URL/plugin-marketplace/plugins" "$WORK_DIR/marketplace.response.json" "${AUTH_HEADER[@]}"
|
json_get "$API_URL/plugin-marketplace/plugins" "$WORK_DIR/marketplace.response.json" "${AUTH_HEADER[@]}"
|
||||||
@@ -1056,8 +1046,8 @@ json_get "$API_URL/plugin-marketplace/plugins?serverType=scum&keyword=scum" "$WO
|
|||||||
require_file_contains "$WORK_DIR/scum-marketplace.response.json" '"id"[[:space:]]*:[[:space:]]*"game.scum"'
|
require_file_contains "$WORK_DIR/scum-marketplace.response.json" '"id"[[:space:]]*:[[:space:]]*"game.scum"'
|
||||||
require_file_contains "$WORK_DIR/artifacts.response.json" "\"id\"[[:space:]]*:[[:space:]]*\"artifact-run-dist-$SCUM_ALPHA_ID"
|
require_file_contains "$WORK_DIR/artifacts.response.json" "\"id\"[[:space:]]*:[[:space:]]*\"artifact-run-dist-$SCUM_ALPHA_ID"
|
||||||
require_file_contains "$WORK_DIR/scum-alpha-jobs.response.json" "\"serverInstanceId\"[[:space:]]*:[[:space:]]*\"$SCUM_ALPHA_ID\""
|
require_file_contains "$WORK_DIR/scum-alpha-jobs.response.json" "\"serverInstanceId\"[[:space:]]*:[[:space:]]*\"$SCUM_ALPHA_ID\""
|
||||||
require_file_contains "$WORK_DIR/scum-beta-jobs.response.json" "\"serverInstanceId\"[[:space:]]*:[[:space:]]*\"$SCUM_BETA_ID\""
|
require_file_contains "$WORK_DIR/server-instances.response.json" "\"id\"[[:space:]]*:[[:space:]]*\"$SCUM_BETA_ID\""
|
||||||
require_file_contains "$WORK_DIR/scum-dynamic-jobs.response.json" '"state"[[:space:]]*:[[:space:]]*"succeeded"'
|
require_file_contains "$WORK_DIR/server-instances.response.json" "\"id\"[[:space:]]*:[[:space:]]*\"$SCUM_DYNAMIC_ID\""
|
||||||
|
|
||||||
for file in \
|
for file in \
|
||||||
"$WORK_DIR"/server-instances.response.json \
|
"$WORK_DIR"/server-instances.response.json \
|
||||||
|
|||||||
Reference in New Issue
Block a user