Fix SCUM lifecycle install path handling

This commit is contained in:
npc0-hue
2026-08-01 11:04:51 +08:00
parent 64c703b2c5
commit 8c1c457a06
5 changed files with 165 additions and 4 deletions
+128
View File
@@ -110,6 +110,134 @@ func TestCoreServiceSCUMGuidedDeployDispatchesPluginOwnedInstallAction(t *testin
}
}
func TestCoreServiceDeploymentLifecycleFailureDoesNotRequireExecutionReceipt(t *testing.T) {
svc, sessionToken := newLifecycleRunService(t)
createLifecyclePlugin(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "deployment-failure-owner", DisplayName: "Deployment Failure Owner", Email: "deployment-failure@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
endpoint, err := svc.store.RunEndpoints().Get("run-local")
if err != nil {
t.Fatalf("get endpoint: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("enable deployment capability: %v", err)
}
created, err := svc.CreateServerInstanceWorkflowForSession(ownerSession, domain.ServerLifecycleCreate{
ID: "deployment-failure", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Deployment Failure", IdempotencyKey: "deployment-failure-create", ProfileKey: "local",
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\scumserver", WorkingDirectory: "C:\\scumserver", CreateInputs: map[string]string{"gamePort": "27000", "maxPlayers": "128"}},
})
if err != nil {
t.Fatalf("create deployment lifecycle server: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: sessionToken, Capabilities: []string{domain.LifecycleCapabilityInstall}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job.JobID != created.Job.ID || claim.Job.ExecutionInput.Deployment == nil {
t.Fatalf("claim deployment lifecycle job: claim=%+v err=%v", claim, err)
}
if _, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "installing"}); err != nil {
t.Fatalf("ack deployment lifecycle job: %v", err)
}
result, err := svc.CompleteRunJob(domain.RunJobResult{
RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt,
State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100, Message: "exit status 1"}, Message: "exit status 1", ErrorCode: "lifecycle_process_failed",
})
if err != nil {
t.Fatalf("failed deployment lifecycle result without receipt should be terminal: %v", err)
}
if result.Job.State != domain.JobStateFailed {
t.Fatalf("expected terminal failed job, got %+v", result.Job)
}
stored, err := svc.GetJob(claim.Job.JobID)
if err != nil {
t.Fatalf("get terminal failed job: %v", err)
}
if stored.State != domain.JobStateFailed || stored.TerminalAt.IsZero() {
t.Fatalf("expected stored terminal failed job, got %+v", stored)
}
instance, err := svc.GetServerInstance(created.Instance.ID)
if err != nil {
t.Fatalf("get failed deployment instance: %v", err)
}
if instance.State != domain.ServerInstanceStateFailed {
t.Fatalf("expected failed deployment lifecycle to project instance failed, got %+v", instance)
}
}
func TestCoreServiceGuidedPluginLifecycleSuccessDoesNotRequireExecutionReceipt(t *testing.T) {
svc, sessionToken := newLifecycleRunService(t)
createLifecyclePlugin(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "guided-success-owner", DisplayName: "Guided Success Owner", Email: "guided-success@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
endpoint, err := svc.store.RunEndpoints().Get("run-local")
if err != nil {
t.Fatalf("get endpoint: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("enable deployment capability: %v", err)
}
created, err := svc.CreateServerInstanceWorkflowForSession(ownerSession, domain.ServerLifecycleCreate{
ID: "guided-success", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Guided Success", IdempotencyKey: "guided-success-create", ProfileKey: "local",
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\scumserver", WorkingDirectory: "C:\\scumserver", CreateInputs: map[string]string{"gamePort": "27000", "maxPlayers": "128"}},
})
if err != nil {
t.Fatalf("create guided lifecycle server: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: sessionToken, Capabilities: []string{domain.LifecycleCapabilityInstall}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job.JobID != created.Job.ID || claim.Job.ExecutionInput.Deployment == nil {
t.Fatalf("claim guided lifecycle job: claim=%+v err=%v", claim, err)
}
if _, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "installing"}); err != nil {
t.Fatalf("ack guided lifecycle job: %v", err)
}
if _, err := svc.CompleteRunJob(domain.RunJobResult{
RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt,
State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "done"}, Message: "done", ResultRef: "artifact://jobs/guided-success/lifecycle-result",
}); err != nil {
t.Fatalf("guided plugin lifecycle success without receipt should be terminal: %v", err)
}
instance, err := svc.GetServerInstance(created.Instance.ID)
if err != nil {
t.Fatalf("get ready guided instance: %v", err)
}
if instance.State != domain.ServerInstanceStateReady {
t.Fatalf("expected guided plugin lifecycle success to mark ready, got %+v", instance)
}
}
func TestCoreServiceCustomCommandLifecycleSuccessRequiresExecutionReceipt(t *testing.T) {
svc, sessionToken := newLifecycleRunService(t)
createLifecyclePlugin(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "custom-receipt-owner", DisplayName: "Custom Receipt Owner", Email: "custom-receipt@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
endpoint, err := svc.store.RunEndpoints().Get("run-local")
if err != nil {
t.Fatalf("get endpoint: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("enable deployment capability: %v", err)
}
created, err := svc.CreateServerInstanceWorkflowForSession(ownerSession, domain.ServerLifecycleCreate{
ID: "custom-receipt", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Custom Receipt", IdempotencyKey: "custom-receipt-create", ProfileKey: "local",
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "C:\\custom", WorkingDirectory: "C:\\custom", StartCommand: "server.exe"},
})
if err != nil {
t.Fatalf("create custom lifecycle server: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: sessionToken, Capabilities: []string{domain.LifecycleCapabilityInstall}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job.JobID != created.Job.ID || claim.Job.ExecutionInput.Deployment == nil {
t.Fatalf("claim custom lifecycle job: claim=%+v err=%v", claim, err)
}
if _, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "installing"}); err != nil {
t.Fatalf("ack custom lifecycle job: %v", err)
}
_, err = svc.CompleteRunJob(domain.RunJobResult{
RunEndpointID: "run-local", SessionToken: sessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt,
State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "done"}, Message: "done",
})
if err == nil || !strings.Contains(err.Error(), "deployment execution receipt") {
t.Fatalf("expected custom-command success without receipt rejection, got %v", err)
}
}
func TestMergeDeploymentPreservesOmittedShellAndClearsOnlyExplicitFields(t *testing.T) {
current := domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/game", StartCommand: "./start", StopCommand: "./stop", Shell: domain.ServerCommandShellCmd}
preserved := mergeDeploymentDefinition(current, domain.ServerDeploymentUpdate{Mode: domain.ServerDeploymentModeCustom})