From acec5e4367392bab2dbfad33a88f2c3b5a0853ef Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 6 Aug 2026 18:18:20 +0800 Subject: [PATCH] Allow restarting failed server instances --- platform/service/server_lifecycle.go | 1 + platform/service/server_lifecycle_test.go | 17 +++++++++++++++++ .../contracts/serverManagement.test.ts | 19 +++++++++++++++++++ platform_web/contracts/serverManagement.ts | 2 +- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 platform_web/contracts/serverManagement.test.ts diff --git a/platform/service/server_lifecycle.go b/platform/service/server_lifecycle.go index 9fcb707..3386487 100644 --- a/platform/service/server_lifecycle.go +++ b/platform/service/server_lifecycle.go @@ -173,6 +173,7 @@ func (svc *CoreService) StartServerInstance(command domain.ServerLifecycleComman return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStart, []domain.ServerInstanceState{ domain.ServerInstanceStateReady, domain.ServerInstanceStateStopped, + domain.ServerInstanceStateFailed, }) } diff --git a/platform/service/server_lifecycle_test.go b/platform/service/server_lifecycle_test.go index e76238b..150f1d4 100644 --- a/platform/service/server_lifecycle_test.go +++ b/platform/service/server_lifecycle_test.go @@ -283,6 +283,23 @@ func TestCoreServiceLifecycleFailureProjectsFailedState(t *testing.T) { if instance.State != domain.ServerInstanceStateFailed { t.Fatalf("expected failed install result to mark failed, got %+v", instance) } + + started, err := svc.StartServerInstance(domain.ServerLifecycleCommand{ + ServerInstanceID: instance.ID, + ExpectedConfigVersion: instance.ConfigVersion, + IdempotencyKey: "idem-start-after-failure", + }) + if err != nil || started.Job.Capability != domain.LifecycleCapabilityStart || started.Job.TargetKey != "actions/start.json" { + t.Fatalf("failed server should allow explicit restart dispatch, result=%+v err=%v", started, err) + } + claimAndCompleteLifecycleJob(t, svc, sessionToken, domain.LifecycleCapabilityStart, domain.JobStateSucceeded) + recovered, err := svc.GetServerInstance("server-1") + if err != nil { + t.Fatalf("get recovered instance: %v", err) + } + if recovered.State != domain.ServerInstanceStateRunning { + t.Fatalf("expected explicit restart to recover failed instance, got %+v", recovered) + } } func TestCoreServicePluginLifecycleManagesMultipleInstancesIndependently(t *testing.T) { diff --git a/platform_web/contracts/serverManagement.test.ts b/platform_web/contracts/serverManagement.test.ts new file mode 100644 index 0000000..58e426b --- /dev/null +++ b/platform_web/contracts/serverManagement.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { canStartServer, canStopServer } from "./serverManagement"; +import type { ServerInstanceState } from "../api/types"; + +describe("server management lifecycle contracts", () => { + it("allows explicit starts from recoverable non-running states", () => { + const startable: ServerInstanceState[] = ["ready", "stopped", "failed"]; + const blocked: ServerInstanceState[] = ["draft", "installing", "running", "deleted"]; + + expect(startable.every(canStartServer)).toBe(true); + expect(blocked.some(canStartServer)).toBe(false); + }); + + it("keeps stop limited to running servers", () => { + expect(canStopServer("running")).toBe(true); + expect(canStopServer("failed")).toBe(false); + }); +}); diff --git a/platform_web/contracts/serverManagement.ts b/platform_web/contracts/serverManagement.ts index cf5a29e..bb41b9a 100644 --- a/platform_web/contracts/serverManagement.ts +++ b/platform_web/contracts/serverManagement.ts @@ -108,7 +108,7 @@ export function endpointLabel(endpoint: RunEndpointResponse | undefined, runEndp } export function canStartServer(state: ServerInstanceState): boolean { - return state === "ready" || state === "stopped"; + return state === "ready" || state === "stopped" || state === "failed"; } export function canStopServer(state: ServerInstanceState): boolean {