Make Run runtime state authoritative

This commit is contained in:
npc0-hue
2026-08-06 10:07:01 +08:00
parent d8d07914da
commit 0875a1b17e
10 changed files with 324 additions and 12 deletions
+87
View File
@@ -443,6 +443,93 @@ func TestCoreServiceGeneratedSCUMRunRegistrationQueuesGuidedStart(t *testing.T)
}
}
func TestCoreServiceGeneratedRunRegistrationReconcilesStaleRunningState(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)
}
registered := 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.LifecycleCapabilityStatus || jobs[0].TargetKey != "actions/status.json" {
t.Fatalf("expected one status reconciliation job, jobs=%+v err=%v", jobs, err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, Capabilities: []string{domain.LifecycleCapabilityStatus}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job.JobID != jobs[0].ID {
t.Fatalf("claim status reconciliation: claim=%+v err=%v", claim, err)
}
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: instance.RunEndpointID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "process status queried"}, Message: "process status queried", ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "not-started", ExitClassification: "not-started", AuditSummary: "bounded process state"}}); err != nil {
t.Fatalf("complete status reconciliation: %v", err)
}
stored, err := svc.GetServerInstance(instance.ID)
if err != nil || stored.State != domain.ServerInstanceStateStopped {
t.Fatalf("Run-reported not-started should correct stale running state, server=%+v err=%v", stored, err)
}
}
func TestCoreServiceGeneratedRunRegistrationSkipsStatusWhenLifecycleJobActive(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("active lifecycle job should suppress status reconciliation, 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, "")