672 lines
32 KiB
Go
672 lines
32 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
)
|
|
|
|
func TestCoreServiceRegistersNewRunControlSession(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
|
|
result, err := svc.RegisterRunHello(validRunControlHello())
|
|
if err != nil {
|
|
t.Fatalf("register hello: %v", err)
|
|
}
|
|
if !result.Accepted || result.SessionToken == "" || result.HeartbeatIntervalSeconds <= 0 {
|
|
t.Fatalf("expected accepted hello response, got %+v", result)
|
|
}
|
|
|
|
endpoint, err := svc.GetRunEndpoint("run-local")
|
|
if err != nil {
|
|
t.Fatalf("get registered endpoint: %v", err)
|
|
}
|
|
if endpoint.Status != domain.RunEndpointStatusOnline || !endpoint.LastHeartbeatAt.Equal(fixedTime) {
|
|
t.Fatalf("expected online endpoint with heartbeat time, got %+v", endpoint)
|
|
}
|
|
if len(endpoint.Capabilities) != 3 || endpoint.Capacity.MaxJobs != 4 {
|
|
t.Fatalf("expected capabilities and capacity, got %+v", endpoint)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceDoesNotRevokeSharedLegacyRunEndpoint(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
|
hello := validRunControlHello()
|
|
hello.CapabilityReport.Capabilities = append(hello.CapabilityReport.Capabilities, plugin.RequiredRunCapabilities...)
|
|
registered, err := svc.RegisterRunHello(hello)
|
|
if err != nil || !registered.Accepted {
|
|
t.Fatalf("register shared endpoint: result=%+v err=%v", registered, err)
|
|
}
|
|
first, err := svc.CreateServerInstance(domain.ServerInstance{ID: "shared-first", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Shared First", State: domain.ServerInstanceStateReady})
|
|
if err != nil {
|
|
t.Fatalf("create first legacy server: %v", err)
|
|
}
|
|
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "shared-second", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Shared Second", State: domain.ServerInstanceStateReady}); err != nil {
|
|
t.Fatalf("create second legacy server: %v", err)
|
|
}
|
|
if err := svc.revokeRunControlSessionForInstance(first); err != nil {
|
|
t.Fatalf("revoke first legacy Run: %v", err)
|
|
}
|
|
session, err := svc.store.RunControlSessions().Get(endpoint.ID)
|
|
if err != nil || session.Status != domain.AuthSessionStatusActive {
|
|
t.Fatalf("shared legacy endpoint session must remain active, session=%+v err=%v", session, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceReRegistersExistingRunEndpoint(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
first, err := svc.RegisterRunHello(validRunControlHello())
|
|
if err != nil {
|
|
t.Fatalf("register first hello: %v", err)
|
|
}
|
|
|
|
hello := validRunControlHello()
|
|
hello.DisplayName = "Local Run Updated"
|
|
hello.Version = "0.2.0"
|
|
hello.CapabilityReport.Capabilities = []string{"control.hello", "control.heartbeat", "jobs.claim"}
|
|
hello.CapabilityReport.Fingerprint = "cap-v2"
|
|
second, err := svc.RegisterRunHello(hello)
|
|
if err != nil {
|
|
t.Fatalf("register second hello: %v", err)
|
|
}
|
|
if second.SessionToken == first.SessionToken {
|
|
t.Fatalf("expected re-registration to issue a new token, got %q", second.SessionToken)
|
|
}
|
|
|
|
endpoint, err := svc.GetRunEndpoint("run-local")
|
|
if err != nil {
|
|
t.Fatalf("get re-registered endpoint: %v", err)
|
|
}
|
|
if endpoint.DisplayName != "Local Run Updated" || endpoint.Version != "0.2.0" || len(endpoint.Capabilities) != 3 {
|
|
t.Fatalf("expected endpoint metadata update, got %+v", endpoint)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceAcceptsRunHeartbeat(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
hello, err := svc.RegisterRunHello(validRunControlHello())
|
|
if err != nil {
|
|
t.Fatalf("register hello: %v", err)
|
|
}
|
|
|
|
result, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: hello.SessionToken,
|
|
Version: "0.1.1",
|
|
Status: domain.RunEndpointStatusDegraded,
|
|
CapabilityFingerprint: "cap-v1",
|
|
Capacity: domain.RunCapacity{MaxJobs: 4, RunningJobs: 2, QueuedJobs: 1},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("accept heartbeat: %v", err)
|
|
}
|
|
if !result.Accepted || result.RefreshCapabilities {
|
|
t.Fatalf("expected accepted heartbeat without refresh, got %+v", result)
|
|
}
|
|
|
|
endpoint, err := svc.GetRunEndpoint("run-local")
|
|
if err != nil {
|
|
t.Fatalf("get heartbeat endpoint: %v", err)
|
|
}
|
|
if endpoint.Status != domain.RunEndpointStatusDegraded || endpoint.Version != "0.1.1" || endpoint.Capacity.RunningJobs != 2 {
|
|
t.Fatalf("expected heartbeat metadata update, got %+v", endpoint)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceClearsRunRegistrationAfterHeartbeatTTL(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
hello, err := svc.RegisterRunHello(validRunControlHello())
|
|
if err != nil {
|
|
t.Fatalf("register hello: %v", err)
|
|
}
|
|
|
|
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter - time.Nanosecond) }
|
|
active, err := svc.ListRunEndpoints(domain.RunEndpointFilter{Status: domain.RunEndpointStatusOnline})
|
|
if err != nil || len(active) != 1 || active[0].ID != "run-local" {
|
|
t.Fatalf("expected registration inside TTL, endpoints=%+v err=%v", active, err)
|
|
}
|
|
|
|
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter + time.Second) }
|
|
if _, err := svc.GetRunEndpoint("run-local"); !errors.Is(err, repo.ErrNotFound) {
|
|
t.Fatalf("expected expired registration to be cleared, got %v", err)
|
|
}
|
|
cleared, err := svc.ListRunEndpoints(domain.RunEndpointFilter{})
|
|
if err != nil || len(cleared) != 0 {
|
|
t.Fatalf("expected no stale registrations in list, endpoints=%+v err=%v", cleared, err)
|
|
}
|
|
_, err = svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: hello.SessionToken,
|
|
Version: "0.1.1",
|
|
Status: domain.RunEndpointStatusOnline,
|
|
CapabilityFingerprint: "cap-v1",
|
|
Capacity: domain.RunCapacity{MaxJobs: 4},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "sessionToken is invalid") {
|
|
t.Fatalf("expected stale session to be rejected after registration cleanup, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRejectsInvalidRunHeartbeatToken(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
if _, err := svc.RegisterRunHello(validRunControlHello()); err != nil {
|
|
t.Fatalf("register hello: %v", err)
|
|
}
|
|
|
|
_, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: "stale-token",
|
|
Version: "0.1.1",
|
|
Status: domain.RunEndpointStatusOnline,
|
|
CapabilityFingerprint: "cap-v1",
|
|
Capacity: domain.RunCapacity{MaxJobs: 4, RunningJobs: 3},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "sessionToken is invalid") {
|
|
t.Fatalf("expected invalid token rejection, got %v", err)
|
|
}
|
|
|
|
endpoint, err := svc.GetRunEndpoint("run-local")
|
|
if err != nil {
|
|
t.Fatalf("get endpoint after rejected heartbeat: %v", err)
|
|
}
|
|
if endpoint.Capacity.RunningJobs != 0 || endpoint.Version != "0.1.0" {
|
|
t.Fatalf("heartbeat with invalid token must not update endpoint, got %+v", endpoint)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRejectsInvalidRunControlHello(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
invalid := validRunControlHello()
|
|
invalid.RegistrationToken = ""
|
|
invalid.Capacity.RunningJobs = 8
|
|
|
|
_, err := svc.RegisterRunHello(invalid)
|
|
if err == nil || !strings.Contains(err.Error(), "registrationToken") || !strings.Contains(err.Error(), "runningJobs") {
|
|
t.Fatalf("expected validation errors, got %v", err)
|
|
}
|
|
if _, err := svc.GetRunEndpoint("run-local"); !errors.Is(err, repo.ErrNotFound) {
|
|
t.Fatalf("invalid hello must not create endpoint, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRunHelloRejectsStalePackageKeyAfterReset(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
|
|
ServerInstanceID: instance.ID,
|
|
TargetOS: "linux",
|
|
TargetArch: "amd64",
|
|
IdempotencyKey: "idem-control-auth",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("generate run distribution: %v", err)
|
|
}
|
|
pkg := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = distribution.RunEndpointID
|
|
hello.RegistrationToken = pkg.AuthKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = instance.PluginID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = pkg.KeyGeneration
|
|
|
|
result, err := svc.RegisterRunHello(hello)
|
|
if err != nil {
|
|
t.Fatalf("register current package hello: %v", err)
|
|
}
|
|
if !result.Accepted || result.SessionToken == "" {
|
|
t.Fatalf("expected current package hello to be accepted, got %+v", result)
|
|
}
|
|
|
|
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
|
|
ServerInstanceID: instance.ID,
|
|
ComponentKind: domain.DistributionComponentRun,
|
|
}); err != nil {
|
|
t.Fatalf("reset run key: %v", err)
|
|
}
|
|
result, err = svc.RegisterRunHello(hello)
|
|
if err != nil {
|
|
t.Fatalf("register stale package hello: %v", err)
|
|
}
|
|
if result.Accepted || result.SessionToken != "" {
|
|
t.Fatalf("expected stale package hello to be rejected, got %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRunHelloRejectsGeneratedRunOnPromotedBuildEndpoint(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
instance.State = domain.ServerInstanceStateFailed
|
|
if err := svc.store.ServerInstances().Update(instance); err != nil {
|
|
t.Fatalf("mark legacy server failed: %v", err)
|
|
}
|
|
if _, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "promoted-hello-fence"}); err != nil {
|
|
t.Fatalf("generate promoted Run: %v", err)
|
|
}
|
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("get component key: %v", err)
|
|
}
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = platformDistributionBuilderEndpointID
|
|
hello.RegistrationToken = plainKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = instance.PluginID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = key.Generation
|
|
if _, err := svc.RegisterRunHello(hello); err == nil || !strings.Contains(err.Error(), "platform distribution builder") {
|
|
t.Fatalf("expected shared builder registration rejection, got %v", err)
|
|
}
|
|
|
|
hello.RunEndpointID = generatedRunEndpointID(instance.ID)
|
|
if result, err := svc.RegisterRunHello(hello); err != nil || !result.Accepted {
|
|
t.Fatalf("expected automatically discovered Run registration acceptance, result=%+v err=%v", result, err)
|
|
}
|
|
attached, err := svc.GetServerInstance(instance.ID)
|
|
if err != nil || attached.RunEndpointID != hello.RunEndpointID {
|
|
t.Fatalf("expected heartbeat to attach active Run endpoint, instance=%+v err=%v", attached, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceComponentRunCannotClaimDistributionBuild(t *testing.T) {
|
|
svc, session, instance := newDistributionTestFixture(t)
|
|
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "component-build-claim"})
|
|
if err != nil {
|
|
t.Fatalf("generate legacy Run: %v", err)
|
|
}
|
|
packageConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = instance.RunEndpointID
|
|
hello.RegistrationToken = packageConfig.AuthKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = instance.PluginID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = packageConfig.KeyGeneration
|
|
registered, err := svc.RegisterRunHello(hello)
|
|
if err != nil || !registered.Accepted {
|
|
t.Fatalf("register legacy package: result=%+v err=%v", registered, err)
|
|
}
|
|
storedSession, err := svc.store.RunControlSessions().Get(instance.RunEndpointID)
|
|
if err != nil || !storedSession.RequireSignedRequests {
|
|
t.Fatalf("expected component session to require signatures, session=%+v err=%v", storedSession, err)
|
|
}
|
|
if activeSession := svc.runSessions[instance.RunEndpointID]; !activeSession.RequireSignedRequests {
|
|
t.Fatalf("expected in-memory component session to require signatures, session=%+v", activeSession)
|
|
}
|
|
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, Capabilities: []string{domain.JobCapabilityDistributionBuild}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
|
if err != nil || claim.HasJob {
|
|
t.Fatalf("component Run must not claim distribution builds: claim=%+v err=%v", claim, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceDedicatedRunRegistrationDoesNotDispatchLifecycleBootstrap(t *testing.T) {
|
|
svc, _ := newLifecycleRunService(t)
|
|
plugin := createLifecyclePlugin(t, svc)
|
|
endpoint, err := svc.store.RunEndpoints().Get("run-local")
|
|
if err != nil {
|
|
t.Fatalf("get bootstrap endpoint: %v", err)
|
|
}
|
|
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDistributionBuild, domain.JobCapabilityDeploymentPlan)
|
|
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
|
|
t.Fatalf("enable bootstrap capabilities: %v", err)
|
|
}
|
|
owner := createServiceUserAndLogin(t, svc, domain.User{ID: "managed-deploy-owner", DisplayName: "Managed Deploy Owner", Email: "managed-deploy@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
|
|
|
guided, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-guided", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Guided", IdempotencyKey: "managed-guided-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\scumserver"}})
|
|
if err != nil {
|
|
t.Fatalf("create guided draft: %v", err)
|
|
}
|
|
if guided.Instance.State != domain.ServerInstanceStateDraft {
|
|
t.Fatalf("expected draft before Run registration, got %+v", guided.Instance)
|
|
}
|
|
registerDedicatedRunForTest(t, svc, guided.Instance, plugin.ID)
|
|
stored, err := svc.GetServerInstance(guided.Instance.ID)
|
|
if err != nil || stored.State != domain.ServerInstanceStateDraft {
|
|
t.Fatalf("guided registration must leave lifecycle authority with Run, server=%+v err=%v", stored, err)
|
|
}
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: guided.Instance.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("registration must not enqueue automatic lifecycle jobs, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
registerDedicatedRunForTest(t, svc, guided.Instance, plugin.ID)
|
|
jobs, _ = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: guided.Instance.ID})
|
|
if len(jobs) != 0 {
|
|
t.Fatalf("Run reconnect must not enqueue bootstrap jobs, jobs=%+v", jobs)
|
|
}
|
|
|
|
generatedRunDraft := domain.ServerInstance{
|
|
ID: "managed-generated",
|
|
PluginID: plugin.ID,
|
|
PluginVersion: plugin.Version,
|
|
RunEndpointID: dedicatedRunEndpointID("managed-generated"),
|
|
Name: "Managed Generated",
|
|
State: domain.ServerInstanceStateDraft,
|
|
ConfigVersion: 1,
|
|
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: "C:\\generated-scum", ProfileKey: "local", Revision: 1},
|
|
}
|
|
if err := svc.store.ServerInstances().Create(generatedRunDraft); err != nil {
|
|
t.Fatalf("create generated Run draft: %v", err)
|
|
}
|
|
registerDedicatedRunForTest(t, svc, generatedRunDraft, plugin.ID)
|
|
storedGenerated, err := svc.GetServerInstance(generatedRunDraft.ID)
|
|
if err != nil || storedGenerated.State != domain.ServerInstanceStateDraft {
|
|
t.Fatalf("generated Run registration must not platform-dispatch supervised start, server=%+v err=%v", storedGenerated, err)
|
|
}
|
|
jobs, err = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: generatedRunDraft.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("expected generated Run startup to be autonomous, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
|
|
existing, err := svc.CreateServerInstanceWorkflowForSession(owner, domain.ServerLifecycleCreate{ID: "managed-existing", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Managed Existing", IdempotencyKey: "managed-existing-create", ProfileKey: "local", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeExisting, ServerRoot: "C:\\existing-scum"}})
|
|
if err != nil {
|
|
t.Fatalf("create existing draft: %v", err)
|
|
}
|
|
registerDedicatedRunForTest(t, svc, existing.Instance, plugin.ID)
|
|
jobs, err = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: existing.Instance.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("existing-server registration must not reinstall, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceGeneratedSCUMRunRegistrationDoesNotQueueGuidedStart(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.RequiredRunCapabilities = []string{
|
|
domain.LifecycleCapabilityInstall,
|
|
domain.LifecycleCapabilityStart,
|
|
domain.LifecycleCapabilityStop,
|
|
"process.restart",
|
|
domain.LifecycleCapabilityStatus,
|
|
"files.list",
|
|
domain.JobCapabilityFilesRead,
|
|
"files.patch",
|
|
"logs.read",
|
|
"artifacts.read",
|
|
"artifacts.write",
|
|
"ai.invoke",
|
|
}
|
|
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 fixture: %v", err)
|
|
}
|
|
|
|
instance := domain.ServerInstance{
|
|
ID: "scum-generated-guided",
|
|
PluginID: plugin.ID,
|
|
PluginVersion: plugin.Version,
|
|
RunEndpointID: dedicatedRunEndpointID("scum-generated-guided"),
|
|
Name: "SCUM Generated Guided",
|
|
OwnerUserID: "owner-scum-generated",
|
|
State: domain.ServerInstanceStateDraft,
|
|
ConfigVersion: 1,
|
|
Deployment: domain.ServerDeploymentDefinition{
|
|
Mode: domain.ServerDeploymentModeGuided,
|
|
ProfileKey: "run-local",
|
|
ServerRoot: `D:\scum-e2e-guided`,
|
|
CreateInputs: map[string]string{
|
|
"serverName": "SCUM E2E",
|
|
"gamePort": "27000",
|
|
"queryPort": "27015",
|
|
"maxPlayers": "128",
|
|
},
|
|
Revision: 1,
|
|
},
|
|
}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
t.Fatalf("create generated SCUM draft: %v", err)
|
|
}
|
|
|
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("get generated Run key: %v", err)
|
|
}
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = instance.RunEndpointID
|
|
hello.RegistrationToken = plainKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = plugin.ID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = key.Generation
|
|
hello.Platform = "windows"
|
|
hello.Architecture = "amd64"
|
|
hello.CapabilityReport.Capabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}
|
|
registered, err := svc.RegisterRunHello(hello)
|
|
if err != nil || !registered.Accepted {
|
|
t.Fatalf("register generated SCUM Run: result=%+v err=%v", registered, err)
|
|
}
|
|
|
|
stored, err := svc.GetServerInstance(instance.ID)
|
|
if err != nil || stored.State != domain.ServerInstanceStateDraft {
|
|
t.Fatalf("generated SCUM registration must leave startup to Run, server=%+v err=%v", stored, err)
|
|
}
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("generated SCUM registration must not enqueue start/status jobs, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRunLifecycleReportProjectsGeneratedRunFacts(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin := createGeneratedRunStatusPlugin(t, svc)
|
|
instance := domain.ServerInstance{ID: "managed-autonomous-start", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-autonomous-start"), Name: "Managed Autonomous Start", State: domain.ServerInstanceStateDraft, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-autonomous`, Revision: 1}}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
t.Fatalf("create autonomous server: %v", err)
|
|
}
|
|
registered := registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
|
|
|
reported, err := svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, ServerInstanceID: instance.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "autonomous start complete"}, Message: "autonomous start complete", ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "running", Summary: "private supervised process identity"}})
|
|
if err != nil || !reported.Accepted || reported.ProjectedState != domain.ServerInstanceStateRunning {
|
|
t.Fatalf("expected accepted lifecycle report projected running, result=%+v err=%v", reported, err)
|
|
}
|
|
stored, err := svc.GetServerInstance(instance.ID)
|
|
if err != nil || stored.State != domain.ServerInstanceStateRunning {
|
|
t.Fatalf("expected Run report to project server running, server=%+v err=%v", stored, err)
|
|
}
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("autonomous lifecycle report must not create platform jobs, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
|
|
other := domain.ServerInstance{ID: "managed-autonomous-other", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-autonomous-other"), Name: "Managed Autonomous Other", State: domain.ServerInstanceStateDraft, ConfigVersion: 1}
|
|
if err := svc.store.ServerInstances().Create(other); err != nil {
|
|
t.Fatalf("create other server: %v", err)
|
|
}
|
|
_, err = svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, ServerInstanceID: other.ID, Capability: domain.LifecycleCapabilityStart, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "running"}})
|
|
if err == nil || !strings.Contains(err.Error(), "runEndpointId must match server instance") {
|
|
t.Fatalf("expected report for another server binding to be rejected, err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRejectsStaleManagedProcessObservation(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin := createGeneratedRunStatusPlugin(t, svc)
|
|
instance := domain.ServerInstance{ID: "managed-observation-order", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-observation-order"), Name: "Managed Observation Order", State: domain.ServerInstanceStateDraft, ConfigVersion: 1}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
t.Fatalf("create server: %v", err)
|
|
}
|
|
registered := registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
|
observedAt := time.Date(2026, 8, 7, 10, 0, 0, 0, time.UTC)
|
|
report := func(sequence uint64, state string, classification string) {
|
|
t.Helper()
|
|
if _, err := svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, ServerInstanceID: instance.ID, Capability: domain.LifecycleCapabilityStatus, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ManagedProcessID: "sha256:managed-process", ObservationSeq: sequence, ObservedAt: observedAt.Add(time.Duration(sequence) * time.Second), ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: state, ExitClassification: classification}}); err != nil {
|
|
t.Fatalf("report sequence %d: %v", sequence, err)
|
|
}
|
|
}
|
|
report(1, "running", "")
|
|
report(2, "exited", "unexpected-exit")
|
|
report(1, "running", "")
|
|
stored, err := svc.GetServerInstance(instance.ID)
|
|
if err != nil || stored.State != domain.ServerInstanceStateFailed || stored.LifecycleObservationSeq != 2 {
|
|
t.Fatalf("stale running observation must not regress exit projection: server=%+v err=%v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceGeneratedRunRegistrationDoesNotDispatchStatusReconciliation(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin := createGeneratedRunStatusPlugin(t, svc)
|
|
instance := domain.ServerInstance{ID: "managed-stale-running", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-stale-running"), Name: "Managed Stale Running", State: domain.ServerInstanceStateRunning, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-stale`, Revision: 1}}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
t.Fatalf("create stale running server: %v", err)
|
|
}
|
|
registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 0 {
|
|
t.Fatalf("registration must not enqueue status reconciliation, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
stored, err := svc.GetServerInstance(instance.ID)
|
|
if err != nil || stored.State != domain.ServerInstanceStateRunning {
|
|
t.Fatalf("registration must not mutate projected state without Run report, server=%+v err=%v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceGeneratedRunRegistrationLeavesExistingLifecycleJobUntouched(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
plugin := createGeneratedRunStatusPlugin(t, svc)
|
|
instance := domain.ServerInstance{ID: "managed-active-start", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: dedicatedRunEndpointID("managed-active-start"), Name: "Managed Active Start", State: domain.ServerInstanceStateRunning, ConfigVersion: 1, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "run-local", ServerRoot: `D:\scum-active`, Revision: 1}}
|
|
if err := svc.store.ServerInstances().Create(instance); err != nil {
|
|
t.Fatalf("create active-start server: %v", err)
|
|
}
|
|
if err := svc.store.RunEndpoints().Create(domain.RunEndpoint{ID: instance.RunEndpointID, DisplayName: "Managed Active Start Run", Version: "0.1.0", Platform: "windows", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}, Capacity: domain.RunCapacity{MaxJobs: 1}, LastHeartbeatAt: fixedTime}); err != nil {
|
|
t.Fatalf("create active-start endpoint: %v", err)
|
|
}
|
|
if _, err := svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionStart, "already-active-start"); err != nil {
|
|
t.Fatalf("queue active start job: %v", err)
|
|
}
|
|
registerGeneratedRunForStatusTest(t, svc, instance, plugin.ID)
|
|
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
|
|
if err != nil || len(jobs) != 1 || jobs[0].Capability != domain.LifecycleCapabilityStart {
|
|
t.Fatalf("registration should leave pre-existing lifecycle jobs untouched, jobs=%+v err=%v", jobs, err)
|
|
}
|
|
}
|
|
|
|
func createGeneratedRunStatusPlugin(t *testing.T, svc *CoreService) domain.GamePlugin {
|
|
t.Helper()
|
|
plugin := scumDeploymentTestPlugin()
|
|
plugin.Name = "SCUM"
|
|
plugin.Version = "0.1.1"
|
|
plugin.ServerType = "scum"
|
|
plugin.Status = domain.GamePluginStatusInstalled
|
|
plugin.SupportedOS = []string{"windows"}
|
|
plugin.RequiredRunCapabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}
|
|
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 status plugin fixture: %v", err)
|
|
}
|
|
return plugin
|
|
}
|
|
|
|
func registerGeneratedRunForStatusTest(t *testing.T, svc *CoreService, instance domain.ServerInstance, pluginID string) domain.RunControlHelloResult {
|
|
t.Helper()
|
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("get generated Run key: %v", err)
|
|
}
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = instance.RunEndpointID
|
|
hello.RegistrationToken = plainKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = pluginID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = key.Generation
|
|
hello.Platform = "windows"
|
|
hello.Architecture = "amd64"
|
|
hello.CapabilityReport.Capabilities = []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}
|
|
registered, err := svc.RegisterRunHello(hello)
|
|
if err != nil || !registered.Accepted {
|
|
t.Fatalf("register generated Run: result=%+v err=%v", registered, err)
|
|
}
|
|
return registered
|
|
}
|
|
|
|
func registerDedicatedRunForTest(t *testing.T, svc *CoreService, instance domain.ServerInstance, pluginID string) {
|
|
t.Helper()
|
|
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
|
|
if err != nil {
|
|
t.Fatalf("get dedicated Run key: %v", err)
|
|
}
|
|
hello := validRunControlHello()
|
|
hello.RunEndpointID = instance.RunEndpointID
|
|
hello.RegistrationToken = plainKey
|
|
hello.ServerInstanceID = instance.ID
|
|
hello.PluginID = pluginID
|
|
hello.ComponentKind = domain.DistributionComponentRun
|
|
hello.KeyGeneration = key.Generation
|
|
hello.CapabilityReport.Capabilities = append(hello.CapabilityReport.Capabilities, domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.JobCapabilityDeploymentPlan, "logs.read")
|
|
if result, err := svc.RegisterRunHello(hello); err != nil || !result.Accepted {
|
|
t.Fatalf("register dedicated Run: result=%+v err=%v", result, err)
|
|
}
|
|
}
|
|
|
|
func TestCoreServiceRequestsCapabilityRefreshOnFingerprintDrift(t *testing.T) {
|
|
svc := newTestCoreService()
|
|
hello, err := svc.RegisterRunHello(validRunControlHello())
|
|
if err != nil {
|
|
t.Fatalf("register hello: %v", err)
|
|
}
|
|
|
|
result, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: hello.SessionToken,
|
|
Version: "0.1.0",
|
|
Status: domain.RunEndpointStatusOnline,
|
|
CapabilityFingerprint: "cap-v2",
|
|
Capacity: domain.RunCapacity{MaxJobs: 4},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("accept drift heartbeat: %v", err)
|
|
}
|
|
if !result.RefreshCapabilities {
|
|
t.Fatalf("expected capability refresh request, got %+v", result)
|
|
}
|
|
|
|
result, err = svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
|
|
RunEndpointID: "run-local",
|
|
SessionToken: hello.SessionToken,
|
|
Version: "0.1.0",
|
|
Status: domain.RunEndpointStatusOnline,
|
|
CapabilityFingerprint: "cap-v2",
|
|
Capacity: domain.RunCapacity{MaxJobs: 4},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("accept stable heartbeat: %v", err)
|
|
}
|
|
if result.RefreshCapabilities {
|
|
t.Fatalf("expected refreshed fingerprint to become known, got %+v", result)
|
|
}
|
|
}
|
|
|
|
func validRunControlHello() domain.RunControlHello {
|
|
return domain.RunControlHello{
|
|
RegistrationToken: "registration-token",
|
|
RunEndpointID: "run-local",
|
|
DisplayName: "Local Run",
|
|
Version: "0.1.0",
|
|
Status: domain.RunEndpointStatusOnline,
|
|
Platform: "darwin/arm64",
|
|
CapabilityReport: domain.RunCapabilityReport{
|
|
Capabilities: []string{"control.hello", "control.heartbeat", domain.JobCapabilityDistributionBuild},
|
|
Fingerprint: "cap-v1",
|
|
},
|
|
Capacity: domain.RunCapacity{MaxJobs: 4},
|
|
}
|
|
}
|