feat: 完整游戏运维功能
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"browser.local/platform/domain"
|
||||
"browser.local/platform/repo"
|
||||
"browser.local/platform/validator"
|
||||
)
|
||||
|
||||
var fixedTime = time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)
|
||||
@@ -351,10 +352,10 @@ func TestCoreServiceScopesServerAccessAndMembership(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("create owned server: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
if instance.OwnerUserID != "user-owner" {
|
||||
t.Fatalf("expected owner to be recorded, got %+v", instance)
|
||||
}
|
||||
|
||||
ownerServers, err := svc.ListServerInstancesForSession(ownerSession, domain.ServerInstanceFilter{})
|
||||
if err != nil || len(ownerServers) != 1 {
|
||||
t.Fatalf("expected owner server visibility, len=%d err=%v", len(ownerServers), err)
|
||||
@@ -513,6 +514,7 @@ func TestCoreServiceConfigWriteAndFileDispatchAreScoped(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
current, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get config: %v", err)
|
||||
@@ -603,6 +605,51 @@ func TestCoreServiceConfigWriteAndFileDispatchAreScoped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigWriteTerminalResultAppliesDurableTypedProjection(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "typed-config-owner", DisplayName: "Typed Config Owner", Email: "typed-config-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "typed-config-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Typed Config", State: domain.ServerInstanceStateRunning})
|
||||
if err != nil {
|
||||
t.Fatalf("create typed config server: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
current, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("read typed config: %v", err)
|
||||
}
|
||||
proposed := current.Content + "motd=typed\n"
|
||||
dispatch, err := svc.ApproveServerConfigWriteForSession(ownerSession, domain.ServerConfigWriteApproval{ServerInstanceID: instance.ID, ExpectedConfigVersion: current.ConfigVersion, ExpectedChecksum: current.Checksum, Key: current.Key, ProposedContent: proposed, IdempotencyKey: "typed-config-write"})
|
||||
if err != nil {
|
||||
t.Fatalf("queue typed config write: %v", err)
|
||||
}
|
||||
helloRequest := validRunControlHello()
|
||||
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityConfigWrite)
|
||||
hello, err := svc.RegisterRunHello(helloRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("register typed config Run: %v", err)
|
||||
}
|
||||
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityConfigWrite}, Capacity: domain.RunCapacity{MaxJobs: 2}})
|
||||
if err != nil || !claim.HasJob {
|
||||
t.Fatalf("claim typed config job: claim=%+v err=%v", claim, err)
|
||||
}
|
||||
checksum := validator.BytesChecksum([]byte(proposed))
|
||||
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "config write completed"}, Message: "config write completed", ExecutionResult: domain.JobExecutionResult{Kind: "file.write", Version: dispatch.Job.ExecutionInput.ExpectedVersion + 1, Checksum: checksum, SizeBytes: int64(len(proposed)), AuditSummary: "atomic compare-and-swap file write"}}); err != nil {
|
||||
t.Fatalf("complete typed config job: %v", err)
|
||||
}
|
||||
updated, err := svc.GetServerConfigForSession(ownerSession, instance.ID)
|
||||
if err != nil || updated.Content != proposed || updated.ConfigVersion != current.ConfigVersion+1 || updated.Checksum != checksum {
|
||||
t.Fatalf("expected durable typed config projection, config=%+v err=%v", updated, err)
|
||||
}
|
||||
stored, err := svc.GetJobForSession(ownerSession, dispatch.Job.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("read typed config job: %v", err)
|
||||
}
|
||||
if stored.ExecutionResult.Content != "" || stored.ExecutionResult.Checksum != checksum || stored.ExecutionResult.Version != current.ConfigVersion+1 {
|
||||
t.Fatalf("unexpected safe/private job result projection: %+v", stored.ExecutionResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceUpdatesUsersProfileAndTheme(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
if _, err := svc.CreateUser(domain.User{
|
||||
@@ -1145,6 +1192,7 @@ func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlug
|
||||
Files: true,
|
||||
Jobs: true,
|
||||
},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create plugin fixture: %v", err)
|
||||
@@ -1164,6 +1212,22 @@ func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlug
|
||||
return plugin, endpoint
|
||||
}
|
||||
|
||||
func createCompleteRuntimeBinding(t *testing.T, svc *CoreService, instance domain.ServerInstance, profileKey string) domain.RuntimeBinding {
|
||||
t.Helper()
|
||||
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
||||
if err != nil {
|
||||
t.Fatalf("get plugin for runtime binding: %v", err)
|
||||
}
|
||||
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: profileKey, Bindings: map[string]string{}}, true)
|
||||
if err != nil {
|
||||
t.Fatalf("build runtime binding: %v", err)
|
||||
}
|
||||
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
|
||||
t.Fatalf("create runtime binding: %v", err)
|
||||
}
|
||||
return binding
|
||||
}
|
||||
|
||||
func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
|
||||
return domain.GamePluginManifestRegistration{
|
||||
ManifestRef: "artifact://manifests/game.example/0.1.0",
|
||||
@@ -1205,7 +1269,8 @@ func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
|
||||
BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)},
|
||||
},
|
||||
},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
|
||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user