diff --git a/platform/service/distribution_build_execution.go b/platform/service/distribution_build_execution.go index 13d37e9..1791122 100644 --- a/platform/service/distribution_build_execution.go +++ b/platform/service/distribution_build_execution.go @@ -234,6 +234,7 @@ func (svc *CoreService) runDistributionPackageContext(distribution domain.RunDis } else if !errors.Is(bindingErr, repo.ErrNotFound) { return runDistributionPackageContext{}, bindingErr } + profileKey = defaultRunDistributionProfileKey(plugin, profileKey) plan, err := runAutonomousLifecyclePlan(distribution, instance, plugin, profileKey, bindings) if err != nil { return runDistributionPackageContext{}, err @@ -245,6 +246,20 @@ func (svc *CoreService) runDistributionPackageContext(distribution domain.RunDis return runDistributionPackageContext{profileKey: profileKey, workspaceSeed: seed, autonomousLifecycle: plan}, nil } +func defaultRunDistributionProfileKey(plugin domain.GamePlugin, profileKey string) string { + profileKey = strings.TrimSpace(profileKey) + if profileKey != "" { + if profile, ok := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey); ok { + return profile.Key + } + return profileKey + } + if len(plugin.RuntimeProfiles.LifecycleProfiles) == 0 { + return "" + } + return plugin.RuntimeProfiles.LifecycleProfiles[0].Key +} + func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance domain.ServerInstance, plugin domain.GamePlugin, profileKey string, bindings map[string]string) (*domain.RunAutonomousLifecyclePlan, error) { plan := &domain.RunAutonomousLifecyclePlan{ SchemaVersion: "1", diff --git a/platform/service/distribution_build_execution_test.go b/platform/service/distribution_build_execution_test.go index 90903c1..a21cb69 100644 --- a/platform/service/distribution_build_execution_test.go +++ b/platform/service/distribution_build_execution_test.go @@ -214,6 +214,55 @@ func hasAutonomousLogSource(sources []domain.RunAutonomousLogSource, kind string return false } +func TestCoreServiceRunDistributionDefaultsEmptyDeploymentProfileToPluginLifecycleProfile(t *testing.T) { + svc := newTestCoreService() + plugin := scumDeploymentTestPlugin() + plugin.Name = "SCUM" + plugin.Version = "0.1.1" + plugin.ServerType = "scum" + plugin.Status = domain.GamePluginStatusInstalled + plugin.SupportedOS = []string{"windows"} + plugin.DeclaredPermissions = []string{"server.run.distribution"} + plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"} + plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}, ActionRefs: domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}, Platforms: []string{"windows"}}} + requireManualSCUMRuntimeBindings(&plugin, "run-local") + if err := svc.store.GamePlugins().Create(plugin); err != nil { + t.Fatalf("create SCUM plugin: %v", err) + } + session := createServiceUserAndLogin(t, svc, domain.User{ID: "scum-default-profile-owner", DisplayName: "SCUM Default Profile Owner", Email: "scum-default-profile@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"}) + created, err := svc.CreateServerInstanceWorkflowForSession(session, domain.ServerLifecycleCreate{ + ID: "scum-default-profile-package", PluginID: plugin.ID, Name: "SCUM Default Profile Package", IdempotencyKey: "scum-default-profile-package-create", + Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `D:\scum-default-profile`, CreateInputs: map[string]string{"serverName": "Moon", "gamePort": "27000", "queryPort": "27015", "maxPlayers": "128"}}, + }) + if err != nil { + t.Fatalf("create SCUM guided draft without explicit profile: %v", err) + } + if created.Instance.Deployment.ProfileKey != "run-local" { + t.Fatalf("expected create defaults to persist plugin lifecycle profile, got %+v", created.Instance.Deployment) + } + + legacy := created.Instance + legacy.Deployment.ProfileKey = "" + if err := svc.store.ServerInstances().Update(legacy); err != nil { + t.Fatalf("simulate legacy empty deployment profile: %v", err) + } + inputs := make(chan domain.DistributionBuildInput, 1) + svc.ConfigureDistributionBuilder(captureDistributionBuilder{inputs: inputs, payload: []byte("profile-default-build")}) + if _, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: legacy.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "scum-default-profile-package"}); err != nil { + t.Fatalf("generate Run distribution with empty deployment profile: %v", err) + } + var platformInput domain.DistributionBuildInput + select { + case platformInput = <-inputs: + case <-time.After(time.Second): + t.Fatal("platform builder did not receive profile-defaulted input") + } + plan := platformInput.AutonomousLifecycle + if platformInput.ProfileKey != "run-local" || plan == nil || plan.ProfileKey != "run-local" || plan.Deployment == nil || plan.Deployment.ProfileKey != "run-local" { + t.Fatalf("expected package context to default empty deployment profile to run-local, input=%+v plan=%+v", platformInput, plan) + } +} + func TestCoreServiceBuildsWithoutRegisteredDistributionWorker(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) bootstrapEndpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID) diff --git a/platform/service/log_ingest.go b/platform/service/log_ingest.go index 1e016fa..3532bdc 100644 --- a/platform/service/log_ingest.go +++ b/platform/service/log_ingest.go @@ -150,7 +150,7 @@ func (svc *CoreService) ensureRunLogStreamForBatch(batch domain.LogBatchIngest, expectedStreamID = runSessionLogStreamID(batch.RunEndpointID, batch.ServerInstanceID, batch.LogSessionID, batch.StreamKey) } if batch.LogStreamID != expectedStreamID { - if !legacyAutonomousLogStream(batch) { + if !legacySessionRunLogStream(batch) && !legacyAutonomousLogStream(batch) { return repo.ErrNotFound } } @@ -168,6 +168,10 @@ func (svc *CoreService) ensureRunLogStreamForBatch(batch domain.LogBatchIngest, return err } +func legacySessionRunLogStream(batch domain.LogBatchIngest) bool { + return batch.LogSessionID != "" && batch.LogStreamID == runLogStreamID(batch.RunEndpointID, batch.ServerInstanceID, batch.StreamKey) +} + func legacyAutonomousLogStream(batch domain.LogBatchIngest) bool { jobID, ok := jobIDFromLogBatch(batch) return ok && strings.HasPrefix(jobID, "autonomous-") diff --git a/platform/service/log_ingest_test.go b/platform/service/log_ingest_test.go index 8bf1a88..c43441a 100644 --- a/platform/service/log_ingest_test.go +++ b/platform/service/log_ingest_test.go @@ -463,6 +463,28 @@ func TestCoreServiceAcceptsSessionMetadataOnLegacyAutonomousStreamID(t *testing. } } +func TestCoreServiceAcceptsSessionMetadataOnLegacyRunStreamID(t *testing.T) { + svc, sessionToken := newRegisteredLogIngestService(t) + batch := validLogBatch(t, sessionToken, 1, 1) + batch.LogStreamID = runLogStreamID("run-local", "server-1", "stdout") + batch.LogSessionID = "session-a" + batch.SessionStartedAt = time.Date(2026, 7, 3, 12, 30, 0, 0, time.UTC) + ack, err := svc.IngestLogBatch(batch) + if err != nil { + t.Fatalf("ingest session-scoped legacy run stream: %v", err) + } + if !ack.Accepted || ack.LogStreamID != batch.LogStreamID || ack.LatestSeq != batch.LastSeq { + t.Fatalf("unexpected legacy run session ack: %+v", ack) + } + stream, err := svc.GetLogStream(batch.LogStreamID) + if err != nil { + t.Fatalf("get session-scoped legacy run stream: %v", err) + } + if stream.LogSessionID != batch.LogSessionID || !stream.SessionStartedAt.Equal(batch.SessionStartedAt) { + t.Fatalf("session metadata was not persisted: %+v", stream) + } +} + func TestCoreServiceRejectsOutOfOrderAndConflictingLogBatches(t *testing.T) { svc, sessionToken := newRegisteredLogIngestService(t) createLogStreamFixture(t, svc) diff --git a/platform/service/server_lifecycle.go b/platform/service/server_lifecycle.go index 6da2fb7..0d2f7af 100644 --- a/platform/service/server_lifecycle.go +++ b/platform/service/server_lifecycle.go @@ -57,7 +57,9 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc instance.State = domain.ServerInstanceStateDraft } if instance.Deployment.Mode != "" { - instance.Deployment.ProfileKey = create.ProfileKey + if strings.TrimSpace(create.ProfileKey) != "" { + instance.Deployment.ProfileKey = create.ProfileKey + } instance.Deployment.RuntimeBindings = domain.CopyStringMap(create.Bindings) instance.Deployment.Revision = maxInt(1, instance.Deployment.Revision) instance.Deployment.UpdatedAt = stamp