package service import ( "errors" "strings" "testing" "time" "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) func TestCoreServiceCreateListGetWorkflows(t *testing.T) { svc := newTestCoreService() user, err := svc.CreateUser(domain.User{ ID: "user-1", DisplayName: "Operator", Roles: []string{"admin"}, }) if err != nil { t.Fatalf("create user: %v", err) } if user.Status != domain.UserStatusActive || !user.CreatedAt.Equal(fixedTime) { t.Fatalf("expected user defaults, got %+v", user) } if _, err := svc.GetUser(user.ID); err != nil { t.Fatalf("get user: %v", err) } users, err := svc.ListUsers(domain.UserFilter{Status: domain.UserStatusActive}) if err != nil || len(users) != 1 { t.Fatalf("list users: len=%d err=%v", len(users), err) } generatedUser, err := svc.CreateUser(domain.User{ DisplayName: "Generated User", Email: "generated@example.test", Roles: []string{"server-admin"}, }) if err != nil { t.Fatalf("create generated user: %v", err) } if generatedUser.ID != "user-generated-example-test" { t.Fatalf("expected generated user id from email, got %q", generatedUser.ID) } provider, err := svc.CreateAIProvider(validProvider()) if err != nil { t.Fatalf("create provider: %v", err) } if provider.APIKeyRef != "secret://providers/openai" { t.Fatalf("expected provider key reference only, got %+v", provider) } if _, err := svc.GetAIProvider(provider.ID); err != nil { t.Fatalf("get provider: %v", err) } providers, err := svc.ListAIProviders(domain.AIProviderFilter{Status: domain.AIProviderStatusActive}) if err != nil || len(providers) != 1 { t.Fatalf("list providers: len=%d err=%v", len(providers), err) } plugin, endpoint := createPluginAndRunEndpoint(t, svc) if _, err := svc.GetGamePlugin(plugin.ID); err != nil { t.Fatalf("get plugin: %v", err) } plugins, err := svc.ListGamePlugins(domain.GamePluginFilter{Status: domain.GamePluginStatusInstalled}) if err != nil || len(plugins) != 1 { t.Fatalf("list plugins: len=%d err=%v", len(plugins), err) } if _, err := svc.GetRunEndpoint(endpoint.ID); err != nil { t.Fatalf("get endpoint: %v", err) } endpoints, err := svc.ListRunEndpoints(domain.RunEndpointFilter{Status: domain.RunEndpointStatusOnline}) if err != nil || len(endpoints) != 1 { t.Fatalf("list endpoints: len=%d err=%v", len(endpoints), err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM #1", }) if err != nil { t.Fatalf("create server instance: %v", err) } if instance.PluginVersion != plugin.Version || instance.ConfigVersion != 1 || instance.State != domain.ServerInstanceStateDraft { t.Fatalf("expected server defaults, got %+v", instance) } if _, err := svc.GetServerInstance(instance.ID); err != nil { t.Fatalf("get server instance: %v", err) } instances, err := svc.ListServerInstances(domain.ServerInstanceFilter{PluginID: plugin.ID}) if err != nil || len(instances) != 1 { t.Fatalf("list server instances: len=%d err=%v", len(instances), err) } job, err := svc.CreateJob(domain.Job{ ID: "job-1", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: "process.start", IdempotencyKey: "idem-start", }) if err != nil { t.Fatalf("create job: %v", err) } if job.State != domain.JobStateQueued || !job.CreatedAt.Equal(fixedTime) { t.Fatalf("expected job defaults, got %+v", job) } if _, err := svc.GetJob(job.ID); err != nil { t.Fatalf("get job: %v", err) } streams, err := svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID}) if err != nil || len(streams) != 2 { t.Fatalf("expected default job stdout/stderr streams, len=%d err=%v streams=%+v", len(streams), err, streams) } jobs, err := svc.ListJobs(domain.JobFilter{RunEndpointID: endpoint.ID}) if err != nil || len(jobs) != 1 { t.Fatalf("list jobs: len=%d err=%v", len(jobs), err) } artifact, err := svc.CreateArtifact(domain.Artifact{ ID: "artifact-1", OwnerKind: domain.ArtifactOwnerKindJob, OwnerID: job.ID, SizeBytes: 128, Checksum: "sha256:abc", }) if err != nil { t.Fatalf("create artifact: %v", err) } if artifact.State != domain.ArtifactStateUploading { t.Fatalf("expected artifact default state, got %+v", artifact) } if _, err := svc.GetArtifact(artifact.ID); err != nil { t.Fatalf("get artifact: %v", err) } artifacts, err := svc.ListArtifacts(domain.ArtifactFilter{OwnerID: job.ID}) if err != nil || len(artifacts) != 1 { t.Fatalf("list artifacts: len=%d err=%v", len(artifacts), err) } stream, err := svc.CreateLogStream(domain.LogStream{ ID: "log-1", ServerInstanceID: instance.ID, Source: domain.LogStreamSourceProcess, StreamKey: "stdout", StorageBackend: domain.LogStorageBackendLocalSegments, RetentionPolicy: "default", }) if err != nil { t.Fatalf("create log stream: %v", err) } if _, err := svc.GetLogStream(stream.ID); err != nil { t.Fatalf("get log stream: %v", err) } streams, err = svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID}) if err != nil || len(streams) != 3 { t.Fatalf("list log streams: len=%d err=%v", len(streams), err) } audit, err := svc.CreateAuditEvent(domain.AuditEvent{ ID: "audit-1", ActorID: user.ID, Action: "server.create", ResourceKind: "server-instance", ResourceID: instance.ID, Result: domain.AuditResultSuccess, Summary: "created server instance", }) if err != nil { t.Fatalf("create audit event: %v", err) } if !audit.CreatedAt.Equal(fixedTime) { t.Fatalf("expected audit timestamp default, got %+v", audit) } if _, err := svc.GetAuditEvent(audit.ID); err != nil { t.Fatalf("get audit event: %v", err) } auditEvents, err := svc.ListAuditEvents(domain.AuditEventFilter{ResourceID: instance.ID}) if err != nil || len(auditEvents) != 1 { t.Fatalf("list audit events: len=%d err=%v", len(auditEvents), err) } } func TestCoreServiceCreateRemoteProgramJobCreatesManagementLogStreams(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProgram) if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin capabilities: %v", err) } endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProgram) if err := svc.store.RunEndpoints().Update(endpoint); err != nil { t.Fatalf("update endpoint capabilities: %v", err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ ID: "server-terminal", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM Terminal", }) if err != nil { t.Fatalf("create server instance: %v", err) } job, err := svc.CreateJob(domain.Job{ ID: "job-terminal", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: domain.JobCapabilityRemoteRunProgram, TargetKey: "protected-program", InputRef: "input://protected-program/job-terminal", IdempotencyKey: "idem-terminal", }) if err != nil { t.Fatalf("create remote program job: %v", err) } streams, err := svc.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: instance.ID}) if err != nil { t.Fatalf("list log streams: %v", err) } if len(streams) != 4 { t.Fatalf("expected stdout/stderr plus management program streams, got %+v", streams) } want := map[string]domain.LogStreamSource{ "stdout": domain.LogStreamSourceProcess, "stderr": domain.LogStreamSourceProcess, "management-program.stdout": domain.LogStreamSourceManagementProgram, "management-program.stderr": domain.LogStreamSourceManagementProgram, } for _, stream := range streams { source, ok := want[stream.StreamKey] if !ok { t.Fatalf("unexpected stream key: %+v", stream) } if stream.Source != source || stream.ID != jobLogStreamID(job.ID, stream.StreamKey) { t.Fatalf("unexpected stream metadata: %+v", stream) } delete(want, stream.StreamKey) } if len(want) != 0 { t.Fatalf("missing streams: %+v", want) } } func TestCoreServiceStartupRecoversLegacyJobLogStreams(t *testing.T) { store := repo.NewMemoryStore() seed := newCoreService(store, func() time.Time { return fixedTime }) plugin, endpoint := createPluginAndRunEndpoint(t, seed) plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProgram) if err := seed.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin capabilities: %v", err) } endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProgram) if err := seed.store.RunEndpoints().Update(endpoint); err != nil { t.Fatalf("update endpoint capabilities: %v", err) } if _, err := seed.CreateServerInstance(domain.ServerInstance{ID: "legacy-terminal-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Legacy Terminal"}); err != nil { t.Fatalf("create server instance: %v", err) } if err := store.Jobs().Create(domain.Job{ ID: "legacy-terminal-job", ServerInstanceID: "legacy-terminal-server", RunEndpointID: endpoint.ID, Capability: domain.JobCapabilityRemoteRunProgram, TargetKey: "protected-program", InputRef: "input://protected-program/legacy-terminal-job", IdempotencyKey: "legacy-terminal", State: domain.JobStateQueued, CreatedAt: fixedTime, UpdatedAt: fixedTime, }); err != nil { t.Fatalf("seed legacy job: %v", err) } before, err := seed.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: "legacy-terminal-server"}) if err != nil || len(before) != 0 { t.Fatalf("expected no seeded streams before recovery, len=%d err=%v streams=%+v", len(before), err, before) } recovered, err := NewCoreServiceWithDurableStores(store, NewMemoryLogBodyStore(), NewMemoryArtifactBodyStore()) if err != nil { t.Fatalf("recover durable service: %v", err) } after, err := recovered.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: "legacy-terminal-server"}) if err != nil || len(after) != 4 { t.Fatalf("expected recovered job log streams, len=%d err=%v streams=%+v", len(after), err, after) } } func TestCoreServiceRejectsInvalidServerDependencies(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) disabledPlugin := plugin disabledPlugin.ID = "server.disabled" disabledPlugin.Status = domain.GamePluginStatusDisabled if _, err := svc.CreateGamePlugin(disabledPlugin); err != nil { t.Fatalf("create disabled plugin fixture: %v", err) } _, err := svc.CreateServerInstance(domain.ServerInstance{ ID: "server-disabled", PluginID: disabledPlugin.ID, RunEndpointID: endpoint.ID, Name: "Disabled Plugin Server", }) if err == nil || !strings.Contains(err.Error(), "plugin must be installed") { t.Fatalf("expected disabled plugin rejection, got %v", err) } weakEndpoint := endpoint weakEndpoint.ID = "run-weak" weakEndpoint.Capabilities = []string{"process.start"} if _, err := svc.CreateRunEndpoint(weakEndpoint); err != nil { t.Fatalf("create weak endpoint fixture: %v", err) } _, err = svc.CreateServerInstance(domain.ServerInstance{ ID: "server-weak", PluginID: plugin.ID, RunEndpointID: weakEndpoint.ID, Name: "Weak Endpoint Server", }) if err == nil || !strings.Contains(err.Error(), "logs.read") { t.Fatalf("expected missing capability rejection, got %v", err) } } func TestCoreServiceRejectsRawAIProviderSecret(t *testing.T) { svc := newTestCoreService() provider := validProvider() provider.APIKeyRef = "sk-raw-secret" _, err := svc.CreateAIProvider(provider) if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") { t.Fatalf("expected raw secret rejection, got %v", err) } } func TestCoreServiceAuthenticatesActiveUsers(t *testing.T) { svc := newTestCoreService() created, err := svc.CreateUser(domain.User{ ID: "user-auth", DisplayName: "Auth User", Email: "auth@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password", }) if err != nil { t.Fatalf("create auth user: %v", err) } if created.PasswordHash == "secret-password" || created.PasswordHash == "" { t.Fatalf("expected password to be hashed, got %q", created.PasswordHash) } session, err := svc.LoginUser(domain.UserLogin{Account: "auth@example.test", Password: "secret-password"}) if err != nil { t.Fatalf("login: %v", err) } if session.SessionID == "" || session.Status != "authenticated" || session.User.ID != created.ID { t.Fatalf("unexpected auth session: %+v", session) } current, err := svc.GetCurrentUser(session.SessionID) if err != nil { t.Fatalf("current user: %v", err) } if current.ID != created.ID { t.Fatalf("expected current user %q, got %+v", created.ID, current) } if err := svc.LogoutUser(session.SessionID); err != nil { t.Fatalf("logout: %v", err) } if _, err := svc.GetCurrentUser(session.SessionID); !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected logged out session to be unauthorized, got %v", err) } } func TestCoreServiceFirstRegistrationBootstrapsPlatformAdmin(t *testing.T) { svc := newTestCoreService() session, err := svc.RegisterUser(domain.UserRegistration{ DisplayName: "Bootstrap Admin", Email: "bootstrap@example.test", Password: "secret-password", Profile: domain.UserProfile{Phone: "13800000000", QQ: "10001"}, }) if err != nil { t.Fatalf("register: %v", err) } if session.Status != "authenticated" || session.SessionID == "" { t.Fatalf("expected authenticated bootstrap registration, got %+v", session) } if session.User.Status != domain.UserStatusActive || len(session.User.Roles) != 1 || session.User.Roles[0] != "platform-admin" { t.Fatalf("expected active platform admin user, got %+v", session.User) } } func TestCoreServiceRegistersLaterUsersAsPendingLowPrivilege(t *testing.T) { svc := newTestCoreService() if _, err := svc.CreateUser(domain.User{ID: "user-existing", DisplayName: "Existing Admin", Roles: []string{"platform-admin"}}); err != nil { t.Fatalf("create existing user: %v", err) } session, err := svc.RegisterUser(domain.UserRegistration{ DisplayName: "Pending Player", Email: "pending@example.test", Password: "secret-password", Profile: domain.UserProfile{Phone: "13800000000", QQ: "10001"}, }) if err != nil { t.Fatalf("register: %v", err) } if session.Status != "pending" || session.SessionID != "" { t.Fatalf("expected pending registration without session token, got %+v", session) } if session.User.Status != domain.UserStatusPending || len(session.User.Roles) != 1 || session.User.Roles[0] != "server-admin" { t.Fatalf("expected low-privilege pending user, got %+v", session.User) } if _, err := svc.LoginUser(domain.UserLogin{Account: "pending@example.test", Password: "secret-password"}); !errors.Is(err, ErrForbidden) { t.Fatalf("expected pending login to be forbidden, got %v", err) } } func TestCoreServiceScopesServerAccessAndMembership(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-owner", DisplayName: "Server Owner", Email: "owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) helperSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-helper", DisplayName: "Server Helper", Email: "helper@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password", }) adminSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-platform", DisplayName: "Platform Admin", Email: "platform@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-owned", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Owned Server", State: domain.ServerInstanceStateReady, }) 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) } helperServers, err := svc.ListServerInstancesForSession(helperSession, domain.ServerInstanceFilter{}) if err != nil || len(helperServers) != 0 { t.Fatalf("expected helper to see no servers before invite, len=%d err=%v", len(helperServers), err) } adminServers, err := svc.ListServerInstancesForSession(adminSession, domain.ServerInstanceFilter{}) if err != nil || len(adminServers) != 1 { t.Fatalf("expected platform admin to see all servers, len=%d err=%v", len(adminServers), err) } candidates, err := svc.ListServerAdministratorCandidates(ownerSession, instance.ID) if err != nil { t.Fatalf("list candidates: %v", err) } if len(candidates) != 1 || candidates[0].ID != "user-helper" { t.Fatalf("expected only helper candidate, got %+v", candidates) } if _, err := svc.AddServerAdministrator(helperSession, instance.ID, "user-owner"); !errors.Is(err, ErrForbidden) { t.Fatalf("expected non-owner add to be forbidden, got %v", err) } if _, err := svc.AddServerAdministrator(ownerSession, instance.ID, "user-platform"); !errors.Is(err, ErrForbidden) { t.Fatalf("expected platform admin invite to be forbidden, got %v", err) } updated, err := svc.AddServerAdministrator(ownerSession, instance.ID, "user-helper") if err != nil { t.Fatalf("add helper admin: %v", err) } if len(updated.AdminUserIDs) != 1 || updated.AdminUserIDs[0] != "user-helper" { t.Fatalf("expected helper membership, got %+v", updated) } helperServers, err = svc.ListServerInstancesForSession(helperSession, domain.ServerInstanceFilter{}) if err != nil || len(helperServers) != 1 { t.Fatalf("expected helper to see invited server, len=%d err=%v", len(helperServers), err) } if _, err := svc.StartServerInstanceForSession(helperSession, domain.ServerLifecycleCommand{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, IdempotencyKey: "idem-helper-start", }); err != nil { t.Fatalf("expected helper lifecycle access: %v", err) } removed, err := svc.RemoveServerAdministrator(ownerSession, instance.ID, "user-helper") if err != nil { t.Fatalf("remove helper admin: %v", err) } if len(removed.AdminUserIDs) != 0 { t.Fatalf("expected helper membership removed, got %+v", removed) } if _, err := svc.GetServerInstanceForSession(helperSession, instance.ID); !errors.Is(err, ErrForbidden) { t.Fatalf("expected helper access to be revoked, got %v", err) } } func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-delete-owner", DisplayName: "Delete Owner", Email: "delete-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) adminSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-delete-admin", DisplayName: "Delete Admin", Email: "delete-admin@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password", }) otherSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-delete-other", DisplayName: "Delete Other", Email: "delete-other@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password", }) ownerInstance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-delete-owner", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Delete Owner Server", State: domain.ServerInstanceStateReady, }) if err != nil { t.Fatalf("create owner instance: %v", err) } adminTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-delete-admin", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Delete Admin Target", State: domain.ServerInstanceStateReady, }) if err != nil { t.Fatalf("create admin target: %v", err) } runningTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-delete-running", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Delete Running Target", State: domain.ServerInstanceStateReady, }) if err != nil { t.Fatalf("create running target: %v", err) } runningTarget.State = domain.ServerInstanceStateRunning if err := svc.store.ServerInstances().Update(runningTarget); err != nil { t.Fatalf("set running target state: %v", err) } installingTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-delete-installing", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Delete Installing Target", State: domain.ServerInstanceStateInstalling, }) if err != nil { t.Fatalf("create installing target: %v", err) } draftTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-delete-draft", PluginID: plugin.ID, Name: "Delete Draft Target", State: domain.ServerInstanceStateDraft, }) if err != nil { t.Fatalf("create draft target: %v", err) } if _, err := svc.DeleteServerInstanceForSession(otherSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"}); !errors.Is(err, ErrForbidden) { t.Fatalf("expected non-owner delete to be forbidden, got %v", err) } if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{}); err == nil { t.Fatalf("expected missing password to fail") } else { var validationErr validator.ValidationError if !errors.As(err, &validationErr) { t.Fatalf("expected missing password to be validation error, got %v", err) } } if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "wrong-password"}); !errors.Is(err, ErrForbidden) { t.Fatalf("expected wrong password to be forbidden, got %v", err) } deletedOwner, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"}) if err != nil { t.Fatalf("delete owner instance: %v", err) } if deletedOwner.State != domain.ServerInstanceStateDeleted { t.Fatalf("expected deleted owner state, got %+v", deletedOwner) } deletedAdmin, err := svc.DeleteServerInstanceForSession(adminSession, adminTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"}) if err != nil { t.Fatalf("delete admin target: %v", err) } if deletedAdmin.State != domain.ServerInstanceStateDeleted { t.Fatalf("expected deleted admin state, got %+v", deletedAdmin) } deletedDraft, err := svc.DeleteServerInstanceForSession(ownerSession, draftTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"}) if err != nil { t.Fatalf("delete draft target without run endpoint: %v", err) } if deletedDraft.State != domain.ServerInstanceStateDeleted || deletedDraft.RunEndpointID != "" { t.Fatalf("expected deleted draft target without run endpoint, got %+v", deletedDraft) } if _, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"}); err == nil { t.Fatalf("expected running instance delete to fail") } else { var validationErr validator.ValidationError if !errors.As(err, &validationErr) { t.Fatalf("expected running delete to be validation error, got %v", err) } } if _, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: "not enough"}); err == nil { t.Fatalf("expected installing force delete without exact confirmation to fail") } forcedRunning, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation}) if err != nil { t.Fatalf("force delete running target: %v", err) } if forcedRunning.State != domain.ServerInstanceStateDeleted { t.Fatalf("expected forced running target deleted, got %+v", forcedRunning) } forcedInstalling, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation}) if err != nil { t.Fatalf("force delete installing target: %v", err) } if forcedInstalling.State != domain.ServerInstanceStateDeleted { t.Fatalf("expected forced installing target deleted, got %+v", forcedInstalling) } } func TestCoreServiceMetricsAndConfigReadAreRoleScoped(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-owner-metrics", DisplayName: "Metrics Owner", Email: "owner-metrics@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) otherSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-other-metrics", DisplayName: "Metrics Other", Email: "other-metrics@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-metrics", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Metrics Server", State: domain.ServerInstanceStateRunning, }) if err != nil { t.Fatalf("create server: %v", err) } if _, err := svc.CreateJob(domain.Job{ID: "job-metrics", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: "process.start", IdempotencyKey: "idem-metrics"}); err != nil { t.Fatalf("create job: %v", err) } usage, err := svc.GetPlatformResourceUsage() if err != nil { t.Fatalf("get platform usage: %v", err) } if usage.Source != "platform-derived" || usage.CollectedAt.IsZero() || usage.CPUPercent < 0 || usage.CPUPercent > 100 { t.Fatalf("unexpected platform usage: %+v", usage) } ownerMetrics, err := svc.ListServerMetricsForSession(ownerSession) if err != nil { t.Fatalf("list owner metrics: %v", err) } if len(ownerMetrics) != 1 || ownerMetrics[0].ServerInstanceID != instance.ID || !ownerMetrics[0].Online || ownerMetrics[0].CPUPercent != nil || ownerMetrics[0].Source != "run-metrics-pending" { t.Fatalf("expected pending metrics without fabricated resource values, got %+v", ownerMetrics) } runHello := validRunControlHello() runHello.RunEndpointID = endpoint.ID registered, err := svc.RegisterRunHello(runHello) if err != nil { t.Fatalf("register run: %v", err) } oldCPU := 28.0 latestCPU := 64.0 latestMemory := 52.0 latestDisk := 31.0 players := 11 maxPlayers := 40 tps := 19.7 latency := 48.0 if _, err := svc.IngestMetricBatch(domain.MetricBatchIngest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Samples: []domain.MetricSample{ {ServerInstanceID: instance.ID, Online: true, CPUPercent: &oldCPU, Source: "run", CollectedAt: fixedTime.Add(-time.Minute)}, {ServerInstanceID: instance.ID, Online: true, PlayerCount: &players, MaxPlayers: &maxPlayers, TPS: &tps, LatencyMS: &latency, CPUPercent: &latestCPU, MemoryPercent: &latestMemory, DiskPercent: &latestDisk, Source: "run", CollectedAt: fixedTime.Add(time.Minute)}, }}); err != nil { t.Fatalf("ingest metrics: %v", err) } ownerMetrics, err = svc.ListServerMetricsForSession(ownerSession) if err != nil { t.Fatalf("list owner metrics after ingest: %v", err) } if len(ownerMetrics) != 1 || ownerMetrics[0].CPUPercent == nil || *ownerMetrics[0].CPUPercent != latestCPU || ownerMetrics[0].PlayerCount == nil || *ownerMetrics[0].PlayerCount != players || ownerMetrics[0].CollectedAt != fixedTime.Add(time.Minute) { t.Fatalf("expected latest persisted sample metrics, got %+v", ownerMetrics) } otherMetrics, err := svc.ListServerMetricsForSession(otherSession) if err != nil { t.Fatalf("list other metrics: %v", err) } if len(otherMetrics) != 0 { t.Fatalf("expected other user to see no metrics, got %+v", otherMetrics) } config, err := svc.GetServerConfigForSession(ownerSession, instance.ID) if err != nil { t.Fatalf("get config: %v", err) } if config.ServerInstanceID != instance.ID || config.ConfigVersion != instance.ConfigVersion || !strings.Contains(config.Content, "server.name=Metrics Server") { t.Fatalf("unexpected config: %+v", config) } for _, forbidden := range []string{"/Users/", "unix://", "Bearer ", "sk-", "password="} { if strings.Contains(config.Content, forbidden) { t.Fatalf("config content exposed forbidden fragment %q: %s", forbidden, config.Content) } } if _, err := svc.GetServerConfigForSession(otherSession, instance.ID); !errors.Is(err, ErrForbidden) { t.Fatalf("expected other config access to be forbidden, got %v", err) } } func TestCoreServiceConfigWriteAndFileDispatchAreScoped(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-owner-config", DisplayName: "Config Owner", Email: "owner-config@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) otherSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-other-config", DisplayName: "Config Other", Email: "other-config@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-config", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Config Server", State: domain.ServerInstanceStateRunning, }) 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) } proposed := strings.Replace(current.Content, "state=running", "state=running\nmotd=Approved", 1) preview, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, Key: current.Key, ProposedContent: proposed, }) if err != nil { t.Fatalf("preview config write: %v", err) } if !preview.HasChanges || preview.Source != "platform-review" || preview.ProposedContent != proposed { t.Fatalf("unexpected preview: %+v", preview) } jobs, err := svc.ListJobs(domain.JobFilter{}) if err != nil || len(jobs) != 0 { t.Fatalf("preview must not create jobs, jobs=%+v err=%v", jobs, err) } dispatch, err := svc.ApproveServerConfigWriteForSession(ownerSession, domain.ServerConfigWriteApproval{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, Key: current.Key, ProposedContent: proposed, IdempotencyKey: "idem-config-approve", }) if err != nil { t.Fatalf("approve config write: %v", err) } if dispatch.Status != "queued" || dispatch.Job.Capability != domain.JobCapabilityConfigWrite || dispatch.Job.TargetKey != current.Key || !strings.HasPrefix(dispatch.Job.InputRef, "input://server-config/") { t.Fatalf("unexpected config dispatch: %+v", dispatch) } if _, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion + 1, Key: current.Key, ProposedContent: proposed, }); err == nil || !strings.Contains(err.Error(), "expectedConfigVersion") { t.Fatalf("expected stale config version rejection, got %v", err) } if _, err := svc.ApproveServerConfigWriteForSession(otherSession, domain.ServerConfigWriteApproval{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, Key: current.Key, ProposedContent: proposed, IdempotencyKey: "idem-config-forbidden", }); !errors.Is(err, ErrForbidden) { t.Fatalf("expected unauthorized approval rejection, got %v", err) } if _, err := svc.PreviewServerConfigWriteForSession(ownerSession, domain.ServerConfigDiffRequest{ ServerInstanceID: instance.ID, ExpectedConfigVersion: instance.ConfigVersion, Key: "/Users/tasia/secret.properties", ProposedContent: proposed, }); err == nil || !strings.Contains(err.Error(), "key") { t.Fatalf("expected unsafe key rejection, got %v", err) } if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ ServerInstanceID: instance.ID, Operation: domain.FileOperationRead, Key: "../secrets.env", IdempotencyKey: "idem-file-unsafe", }); err == nil || !strings.Contains(err.Error(), "key") { t.Fatalf("expected unsafe file key rejection, got %v", err) } fileDispatch, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ ServerInstanceID: instance.ID, PluginID: plugin.ID, Operation: domain.FileOperationRead, Key: "logs/latest.log", IdempotencyKey: "idem-file-read", }) if err != nil { t.Fatalf("dispatch file read: %v", err) } if fileDispatch.Job.Capability != domain.JobCapabilityFilesRead || fileDispatch.Job.TargetKey != "logs/latest.log" { t.Fatalf("unexpected file dispatch: %+v", fileDispatch) } jobs, err = svc.ListJobs(domain.JobFilter{}) if err != nil || len(jobs) != 2 { t.Fatalf("expected only approved config and file jobs, jobs=%+v err=%v", jobs, err) } } func TestDeclaredPluginFileWorkspaceConstrainsFileDispatch(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.FileWorkspace = scumTestFileWorkspace() if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin workspace: %v", err) } ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-owner-file-workspace", DisplayName: "File Workspace Owner", Email: "file-workspace-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-file-workspace", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Workspace Server", State: domain.ServerInstanceStateRunning, }) if err != nil { t.Fatalf("create server: %v", err) } createCompleteRuntimeBinding(t, svc, instance, "local") allowed, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ ServerInstanceID: instance.ID, PluginID: plugin.ID, Operation: domain.FileOperationRead, Key: "scum-server-settings", IdempotencyKey: "idem-file-workspace-read", }) if err != nil { t.Fatalf("dispatch declared file read: %v", err) } if allowed.Job.TargetKey != "scum-server-settings" || allowed.Job.Capability != domain.JobCapabilityFilesRead { t.Fatalf("unexpected declared file dispatch: %+v", allowed) } if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ ServerInstanceID: instance.ID, PluginID: plugin.ID, Operation: domain.FileOperationRead, Key: "logs/latest.log", IdempotencyKey: "idem-file-workspace-unknown", }); err == nil || !strings.Contains(err.Error(), "plugin-declared file") { t.Fatalf("expected undeclared file key rejection, got %v", err) } if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ ServerInstanceID: instance.ID, PluginID: plugin.ID, Operation: domain.FileOperationWrite, Key: "scum-chat-log", InputRef: "input://file-workspace/update", Content: "line", IdempotencyKey: "idem-file-workspace-log-write", }); err == nil || !strings.Contains(err.Error(), "not writable") { t.Fatalf("expected log write rejection, got %v", err) } } func TestDeclaredFileReadSnapshotProjectionStatesAndRedaction(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.FileWorkspace = scumTestFileWorkspace() if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin workspace: %v", err) } ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-owner", DisplayName: "File Snapshot Owner", Email: "file-snapshot-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"}) otherSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-other", DisplayName: "File Snapshot Other", Email: "file-snapshot-other@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password"}) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-snapshot", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Snapshot Server", State: domain.ServerInstanceStateRunning}) if err != nil { t.Fatalf("create server: %v", err) } snapshot, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings") if err != nil || snapshot.State != "not-read" { t.Fatalf("expected not-read without jobs, snapshot=%+v err=%v", snapshot, err) } queued := createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-queued", domain.JobStateQueued, 1, "") snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings") if err != nil || snapshot.State != "pending" || snapshot.JobID != queued.ID { t.Fatalf("expected pending queued job, snapshot=%+v err=%v", snapshot, err) } queued.State = domain.JobStateFailed queued.UpdatedAt = fixedTime.Add(2 * time.Minute) queued.TerminalAt = fixedTime.Add(2 * time.Minute) if err := svc.store.Jobs().Update(queued); err != nil { t.Fatalf("update failed read job: %v", err) } createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-cancelled", domain.JobStateCancelled, 3, "") snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings") if err != nil || snapshot.State != "not-read" { t.Fatalf("failed/cancelled reads must not mask not-read, snapshot=%+v err=%v", snapshot, err) } createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-old", domain.JobStateSucceeded, 4, "ServerName=Old\nRconPassword=secret\n") createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-failed-newer", domain.JobStateFailed, 5, "") snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings") if err != nil || snapshot.State != "ready" || snapshot.JobID != "job-file-snapshot-success-old" || !strings.Contains(snapshot.Content, "RconPassword=") { t.Fatalf("expected older successful redacted result, snapshot=%+v err=%v", snapshot, err) } createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-new", domain.JobStateSucceeded, 6, "ServerName=New\nApiToken=secret\n") snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings") if err != nil || snapshot.JobID != "job-file-snapshot-success-new" || !strings.Contains(snapshot.Content, "ServerName=New") || strings.Contains(snapshot.Content, "secret") { t.Fatalf("expected newest successful redacted result, snapshot=%+v err=%v", snapshot, err) } if _, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "logs/latest.log"); err == nil || !strings.Contains(err.Error(), "plugin-declared file") { t.Fatalf("expected unknown logical key rejection, got %v", err) } if _, err := svc.GetDeclaredFileReadSnapshotForSession(otherSession, instance.ID, "scum-server-settings"); !errors.Is(err, ErrForbidden) { t.Fatalf("expected unrelated session forbidden, got %v", err) } } func scumTestFileWorkspace() domain.PluginFileWorkspace { return domain.PluginFileWorkspace{ DefaultDirectoryKey: "scum-config", Directories: []domain.PluginLogicalDirectory{ {Key: "scum-config", Label: "服务器配置", Scope: "config"}, {Key: "scum-logs", Label: "日志文件", Scope: "logs"}, }, Files: []domain.PluginLogicalFile{ {Key: "scum-server-settings", DirectoryKey: "scum-config", Label: "ServerSettings.ini", Kind: "config", Editable: true}, {Key: "scum-chat-log", DirectoryKey: "scum-logs", Label: "Chat.log", Kind: "log", StreamKey: "scum.chat"}, }, ConfigFields: []domain.PluginConfigField{ {Key: "max-players", FileKey: "scum-server-settings", ConfigKey: "MaxPlayers", Label: "最大玩家数", Description: "玩家上限", Control: "number", Minimum: 1, Maximum: 128, DefaultValue: "128", RestartImpact: "restart-required"}, }, } } func createDeclaredFileReadJob(t *testing.T, svc *CoreService, instance domain.ServerInstance, endpoint domain.RunEndpoint, id string, state domain.JobState, minuteOffset int, content string) domain.Job { t.Helper() job, err := svc.CreateJob(domain.Job{ ID: id, ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: domain.JobCapabilityFilesRead, TargetKey: "scum-server-settings", IdempotencyKey: id, }) if err != nil { t.Fatalf("create declared file read job: %v", err) } stamp := fixedTime.Add(time.Duration(minuteOffset) * time.Minute) job.State = state job.UpdatedAt = stamp if state == domain.JobStateSucceeded || state == domain.JobStateFailed || state == domain.JobStateCancelled { job.TerminalAt = stamp } if state == domain.JobStateSucceeded { job.ExecutionResult = domain.JobExecutionResult{Kind: "file.read", Version: minuteOffset, Checksum: validator.BytesChecksum([]byte(content)), SizeBytes: int64(len(content)), Content: content} } if err := svc.store.Jobs().Update(job); err != nil { t.Fatalf("update declared file read job: %v", err) } return job } 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{ ID: "user-profile", DisplayName: "Profile User", Email: "profile@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password", }); err != nil { t.Fatalf("create user: %v", err) } session, err := svc.LoginUser(domain.UserLogin{Account: "profile@example.test", Password: "secret-password"}) if err != nil { t.Fatalf("login: %v", err) } updated, err := svc.UpdateCurrentUserProfile(session.SessionID, domain.UserProfile{AvatarURL: "avatar://profile", Phone: "13900000000", ContactNote: "primary contact"}) if err != nil { t.Fatalf("update profile: %v", err) } if updated.Profile.Phone != "13900000000" || updated.Profile.ContactNote != "primary contact" { t.Fatalf("unexpected profile: %+v", updated.Profile) } theme, err := svc.UpdateCurrentUserTheme(session.SessionID, domain.UserThemePreference{PaletteID: "crystal-moonlight", BackgroundPresetID: "moon"}) if err != nil { t.Fatalf("update theme: %v", err) } if theme.UserID != "user-profile" || theme.Persistence != "api" || !theme.UpdatedAt.Equal(fixedTime) { t.Fatalf("unexpected theme preference: %+v", theme) } adminUpdate := updated adminUpdate.Status = domain.UserStatusDisabled adminUpdate.Roles = []string{"server-owner"} adminUpdate.DisplayName = "Profile User Updated" saved, err := svc.UpdateUser(updated.ID, adminUpdate) if err != nil { t.Fatalf("admin update user: %v", err) } if saved.Status != domain.UserStatusDisabled || saved.Roles[0] != "server-owner" || saved.DisplayName != "Profile User Updated" { t.Fatalf("unexpected updated user: %+v", saved) } } func createServiceUserAndLogin(t *testing.T, svc *CoreService, user domain.User) string { t.Helper() if _, err := svc.CreateUser(user); err != nil { t.Fatalf("create %s: %v", user.ID, err) } session, err := svc.LoginUser(domain.UserLogin{Account: user.Email, Password: "secret-password"}) if err != nil { t.Fatalf("login %s: %v", user.ID, err) } return session.SessionID } func TestCoreServiceManagesAIProviderMetadata(t *testing.T) { svc := newTestCoreService() created, err := svc.CreateAIProvider(validProvider()) if err != nil { t.Fatalf("create provider: %v", err) } updated := created updated.Name = "OpenAI Primary" updated.BaseURL = "https://relay.example.test/v1" updated.Models = []string{"gpt-4.1-mini"} updated.DefaultModel = "gpt-4.1-mini" updated.RelayMode = domain.AIRelayModeRelay updated.APIKeyRef = "vault://providers/openai-primary" got, err := svc.UpdateAIProvider(created.ID, updated) if err != nil { t.Fatalf("update provider: %v", err) } if got.Name != "OpenAI Primary" || got.Status != domain.AIProviderStatusActive || got.APIKeyRef != "vault://providers/openai-primary" { t.Fatalf("unexpected updated provider: %+v", got) } disabled, err := svc.SetAIProviderStatus(created.ID, domain.AIProviderStatusDisabled) if err != nil { t.Fatalf("disable provider: %v", err) } if disabled.Status != domain.AIProviderStatusDisabled { t.Fatalf("expected disabled provider, got %+v", disabled) } testResult, err := svc.TestAIProvider(created.ID) if err != nil { t.Fatalf("test provider: %v", err) } if testResult.Success || !strings.Contains(strings.Join(testResult.Violations, ","), "provider must be active") { t.Fatalf("expected disabled provider test failure, got %+v", testResult) } enabled, err := svc.SetAIProviderStatus(created.ID, domain.AIProviderStatusActive) if err != nil { t.Fatalf("enable provider: %v", err) } if enabled.Status != domain.AIProviderStatusActive { t.Fatalf("expected active provider, got %+v", enabled) } testResult, err = svc.TestAIProvider(created.ID) if err != nil { t.Fatalf("test enabled provider: %v", err) } if !testResult.Success || testResult.Mode != "provider" { t.Fatalf("expected metadata test success, got %+v", testResult) } models, err := svc.ListAIProviderModels(created.ID) if err != nil { t.Fatalf("list provider models: %v", err) } if models.ProviderID != created.ID || models.DefaultModel != "gpt-4.1-mini" || len(models.Models) != 1 || models.Models[0] != "gpt-4.1-mini" { t.Fatalf("unexpected provider models: %+v", models) } } func TestCoreServiceRegistersGamePluginManifest(t *testing.T) { svc := newTestCoreService() registration := validPluginManifestRegistration() registration.Manifest.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Mode: 0o600}, {Path: "bin/install-server", Mode: 0o700}} registration.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Content: "{}", Mode: 0o600}, {Path: "bin/install-server", Content: "#!/usr/bin/env sh\n"}} plugin, err := svc.RegisterGamePluginManifest(registration) if err != nil { t.Fatalf("register manifest: %v", err) } if plugin.ID != "game.example" || plugin.ServerType != "example" || plugin.ServerDisplayName != "Example Server" { t.Fatalf("unexpected registered plugin metadata: %+v", plugin) } if plugin.Status != domain.GamePluginStatusInstalled { t.Fatalf("expected installed status, got %+v", plugin) } if !plugin.Permissions.AI || !plugin.Permissions.Logs || !plugin.Permissions.Files || !plugin.Permissions.Artifacts || !plugin.Permissions.Jobs { t.Fatalf("expected aggregate permissions from manifest, got %+v", plugin.Permissions) } if len(plugin.Pages) != 1 || plugin.Pages[0].Permissions[0] != "server.logs.read" { t.Fatalf("expected page metadata, got %+v", plugin.Pages) } if len(plugin.AIPurposes) != 1 || plugin.AIPurposes[0] != "logs.diagnose" { t.Fatalf("expected AI purposes, got %+v", plugin.AIPurposes) } if len(plugin.BridgeActions) != 4 || plugin.BridgeActions[0] != string(domain.PluginBridgeActionServerInstancesRead) { t.Fatalf("expected bridge actions, got %+v", plugin.BridgeActions) } if len(plugin.LifecycleAssets) != 2 || plugin.LifecycleAssets[1].Path != "bin/install-server" || plugin.LifecycleAssets[1].Mode != 0o700 { t.Fatalf("expected declared lifecycle assets with manifest mode defaults, got %+v", plugin.LifecycleAssets) } listed, err := svc.ListGamePlugins(domain.GamePluginFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled}) if err != nil || len(listed) != 1 { t.Fatalf("list registered plugins: len=%d err=%v", len(listed), err) } } func TestCoreServiceMarketplacePluginsAreFilteredSafeAndStateful(t *testing.T) { svc := newTestCoreService() if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil { t.Fatalf("register manifest: %v", err) } listed, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled, Capability: "logs.read", Keyword: "development"}) if err != nil { t.Fatalf("list marketplace plugins: %v", err) } if len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Source != "platform-registry" { t.Fatalf("unexpected marketplace list: %+v", listed) } if len(listed[0].Capabilities) == 0 || listed[0].Capabilities[0] != "process.install" || len(listed[0].Pages) != 1 || listed[0].AIPurposes[0] != "logs.diagnose" { t.Fatalf("expected manifest-backed projection, got %+v", listed[0]) } detail, err := svc.GetMarketplacePlugin("game.example") if err != nil { t.Fatalf("get marketplace plugin: %v", err) } if detail.ManifestRef != "artifact://manifests/game.example/0.1.0" || detail.CreateFormSchemaRef != "schemas/create-form.schema.json" { t.Fatalf("unexpected marketplace detail refs: %+v", detail) } disabled, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateActionDisable) if err != nil { t.Fatalf("disable marketplace plugin: %v", err) } if disabled.Status != domain.GamePluginStatusDisabled { t.Fatalf("expected disabled status, got %+v", disabled) } enabled, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateActionEnable) if err != nil { t.Fatalf("enable marketplace plugin: %v", err) } if enabled.Status != domain.GamePluginStatusInstalled { t.Fatalf("expected installed status after enable, got %+v", enabled) } missing, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{Keyword: "missing"}) if err != nil || len(missing) != 0 { t.Fatalf("expected empty keyword result, len=%d err=%v", len(missing), err) } if _, err := svc.GetMarketplacePlugin("missing"); !errors.Is(err, repo.ErrNotFound) { t.Fatalf("expected missing plugin error, got %v", err) } if _, err := svc.SetMarketplacePluginState("game.example", domain.PluginMarketplaceStateAction("download")); err == nil || !strings.Contains(err.Error(), "action is not supported") { t.Fatalf("expected unsupported action validation, got %v", err) } if _, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{Keyword: "sk-raw-secret"}); err == nil || !strings.Contains(err.Error(), "raw credential") { t.Fatalf("expected unsafe keyword validation, got %v", err) } } func TestCoreServiceAuthorizesPluginBridgeActions(t *testing.T) { svc := newTestCoreService() if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil { t.Fatalf("register manifest: %v", err) } allowed, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{ PluginID: "game.example", RouteKey: "logs", Action: domain.PluginBridgeActionLogsQuery, }) if err != nil { t.Fatalf("authorize logs query: %v", err) } if !allowed.Allowed || allowed.RequiredPermissions[0] != "server.logs.read" { t.Fatalf("expected allowed logs bridge action, got %+v", allowed) } missingPermission, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{ PluginID: "game.example", RouteKey: "logs", Action: domain.PluginBridgeActionFilesRequest, }) if err != nil { t.Fatalf("authorize files request: %v", err) } if missingPermission.Allowed || !strings.Contains(missingPermission.Reason, "required permission") { t.Fatalf("expected missing permission denial, got %+v", missingPermission) } aiAllowed, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{ PluginID: "game.example", RouteKey: "logs", Action: domain.PluginBridgeActionAIInvoke, AIPurpose: "logs.diagnose", }) if err != nil { t.Fatalf("authorize AI request: %v", err) } if !aiAllowed.Allowed { t.Fatalf("expected allowed AI bridge action, got %+v", aiAllowed) } aiDenied, err := svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{ PluginID: "game.example", RouteKey: "logs", Action: domain.PluginBridgeActionAIInvoke, AIPurpose: "config.suggest", }) if err != nil { t.Fatalf("authorize undeclared AI request: %v", err) } if aiDenied.Allowed || !strings.Contains(aiDenied.Reason, "ai purpose") { t.Fatalf("expected undeclared AI purpose denial, got %+v", aiDenied) } _, err = svc.AuthorizePluginBridgeAction(domain.PluginBridgeAuthorizeRequest{ PluginID: "game.example", RouteKey: "logs", Action: domain.PluginBridgeAction("direct.run.socket"), }) if err == nil || !strings.Contains(err.Error(), "action is not supported") { t.Fatalf("expected unsupported action validation error, got %v", err) } } func TestCoreServiceRemoteAccessRequiresPluginDeclaration(t *testing.T) { svc := newTestCoreService() registration := validPluginManifestRegistration() registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand, ) registration.Manifest.Permissions = append(registration.Manifest.Permissions, "server.remote.access") registration.Manifest.Bridge.Actions = append(registration.Manifest.Bridge.Actions, string(domain.PluginBridgeActionRemoteAccessRequest)) registration.Manifest.Pages = append(registration.Manifest.Pages, domain.GamePluginPage{ Key: "remote", Title: "Remote", Path: "/remote", Permissions: []string{"server.remote.access"}, BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)}, }) registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{ Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteQuery, domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand}, DatabaseEngines: []string{"sqlite"}, RCON: true, LogTransfer: true, } plugin, err := svc.RegisterGamePluginManifest(registration) if err != nil { t.Fatalf("register remote manifest: %v", err) } if !plugin.Permissions.RemoteAccess || !plugin.RemoteAccess.RCON || plugin.RemoteAccess.DatabaseEngines[0] != "sqlite" { t.Fatalf("expected remote access metadata from manifest, got %+v", plugin) } marketplace, err := svc.GetMarketplacePlugin(plugin.ID) if err != nil || !marketplace.RemoteAccess.LogTransfer || marketplace.RemoteAccess.Methods[0] != "run" { t.Fatalf("expected marketplace remote access projection, got %+v err=%v", marketplace, err) } endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{ ID: "run-remote", DisplayName: "Remote Run", Version: "0.1.0", Capabilities: append([]string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"}, plugin.RemoteAccess.RunCapabilities...), Capacity: domain.RunCapacity{MaxJobs: 2}, }) if err != nil { t.Fatalf("create remote endpoint: %v", err) } ownerSession := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-remote-owner", DisplayName: "Remote Owner", Email: "remote-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ ID: "server-remote", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Remote Server", State: domain.ServerInstanceStateRunning, }) if err != nil { t.Fatalf("create remote server: %v", err) } queued, err := svc.ExecutePluginBridgeAction(ownerSession, domain.PluginBridgeExecuteRequest{ RequestID: "remote-rcon-1", PluginID: plugin.ID, RouteKey: "remote", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionRemoteAccessRequest, Payload: map[string]string{ "capability": domain.JobCapabilityRemoteRunRCONCommand, "targetKey": "rcon/command", "inputRef": "input://server-remote/rcon/command/1", "idempotencyKey": "idem-remote-rcon", }, }) if err != nil { t.Fatalf("execute remote bridge action: %v", err) } if queued.Status != "queued" || queued.Result["capability"] != domain.JobCapabilityRemoteRunRCONCommand { t.Fatalf("expected queued remote bridge job, got %+v", queued) } plainPlugin, plainEndpoint := createPluginAndRunEndpoint(t, svc) plainEndpoint.Capabilities = append(plainEndpoint.Capabilities, domain.JobCapabilityRemoteRunRCONCommand) if err := svc.store.RunEndpoints().Update(plainEndpoint); err != nil { t.Fatalf("extend plain endpoint: %v", err) } plainInstance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-plain", PluginID: plainPlugin.ID, RunEndpointID: plainEndpoint.ID, Name: "Plain Server"}) if err != nil { t.Fatalf("create plain server: %v", err) } _, err = svc.CreateJob(domain.Job{ID: "job-remote-denied", ServerInstanceID: plainInstance.ID, RunEndpointID: plainEndpoint.ID, Capability: domain.JobCapabilityRemoteRunRCONCommand, TargetKey: "rcon/command", InputRef: "input://plain/rcon/command/1", IdempotencyKey: "idem-denied"}) if err == nil || !strings.Contains(err.Error(), "plugin missing required capability") { t.Fatalf("expected undeclared remote job denial, got %v", err) } } func TestCoreServiceDispatchesDeclaredSQLiteQueryTemplate(t *testing.T) { svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t) queued, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{ RequestID: "query-template-dispatch-1", PluginID: plugin.ID, RouteKey: "remote", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionRemoteAccessRequest, Payload: map[string]string{ "capability": domain.JobCapabilityRemoteRunDBSQLiteQuery, "declarationKey": "scum-db-read", "targetKey": "scum-db.player-lookup", "idempotencyKey": "query-template-dispatch-1", "input.templateKey": "players.by-id", "input.playerId": "steam-123", "input.maxRows": "100", }, }) if err != nil { t.Fatalf("execute declared sqlite query template: %v", err) } if queued.Status != "queued" || queued.Result["jobId"] == "" { t.Fatalf("expected queued query template job, got %+v", queued) } job, err := svc.store.Jobs().Get(queued.Result["jobId"]) if err != nil { t.Fatalf("get query template job: %v", err) } if job.ExecutionInput.TimeoutSeconds != 20 { t.Fatalf("expected template timeout 20, got %+v", job.ExecutionInput) } if job.ExecutionInput.Inputs["templateKey"] != "players.by-id" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["maxRows"] != "25" { t.Fatalf("expected typed bounded query template inputs, got %#v", job.ExecutionInput.Inputs) } } func TestCoreServiceDeniesUndeclaredOrMismatchedSQLiteQueryTemplateBeforeJob(t *testing.T) { tests := []struct { name string templateKey string declarationKey string targetKey string }{ {name: "undeclared template", templateKey: "players.unknown", declarationKey: "scum-db-read", targetKey: "scum-db.player-lookup"}, {name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "scum-db.player-lookup"}, {name: "mismatched target", templateKey: "players.by-id", declarationKey: "scum-db-read", targetKey: "scum-db.other"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t) result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{ RequestID: "query-template-denied-1", PluginID: plugin.ID, RouteKey: "remote", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionRemoteAccessRequest, Payload: map[string]string{ "capability": domain.JobCapabilityRemoteRunDBSQLiteQuery, "declarationKey": test.declarationKey, "targetKey": test.targetKey, "idempotencyKey": "query-template-denied-1", "input.templateKey": test.templateKey, "input.playerId": "steam-123", }, }) if err != nil { t.Fatalf("execute denied sqlite query template: %v", err) } if result.Status != "denied" || result.Error == nil || result.Error.Code != "query_template_denied" { t.Fatalf("expected query template denial, got %+v", result) } jobs, err := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID}) if err != nil { t.Fatalf("list jobs after denial: %v", err) } if len(jobs) != 0 { t.Fatalf("query template denial created jobs: %+v", jobs) } }) } } func TestFindBridgeQueryTemplateRequiresPagePermissionAndRemoteAction(t *testing.T) { _, plugin, _, _, _ := createSQLiteQueryBridgeFixture(t) plugin.GameClientBridge.QueryTemplates[0].Permission = "server.game-client.read" for index := range plugin.Pages { if plugin.Pages[index].Key == "remote" { plugin.Pages[index].Permissions = []string{"server.remote.access"} } } if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "permission") { t.Fatalf("expected query template permission denial, got %q", reason) } for index := range plugin.Pages { if plugin.Pages[index].Key == "remote" { plugin.Pages[index].Permissions = []string{"server.remote.access"} plugin.Pages[index].BridgeActions = nil } } plugin.GameClientBridge.QueryTemplates[0].Permission = "server.remote.access" if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "remote access") { t.Fatalf("expected query template remote access denial, got %q", reason) } } func TestCoreServiceRejectsArbitrarySQLBridgeInputBeforeJob(t *testing.T) { svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t) result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{ RequestID: "query-template-sql-rejected-1", PluginID: plugin.ID, RouteKey: "remote", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionRemoteAccessRequest, Payload: map[string]string{ "capability": domain.JobCapabilityRemoteRunDBSQLiteQuery, "declarationKey": "scum-db-read", "targetKey": "scum-db.player-lookup", "idempotencyKey": "query-template-sql-rejected-1", "input.templateKey": "players.by-id", "input.sqlText": "SELECT * FROM users", }, }) if err != nil { t.Fatalf("execute arbitrary SQL bridge input: %v", err) } if result.Status != "error" || result.Error == nil || !strings.Contains(strings.ToLower(result.Error.Message), "unsafe") { t.Fatalf("expected arbitrary SQL input rejection, got %+v", result) } jobs, listErr := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID}) if listErr != nil { t.Fatalf("list jobs after arbitrary SQL rejection: %v", listErr) } if len(jobs) != 0 { t.Fatalf("arbitrary SQL rejection created jobs: %+v", jobs) } } func TestCoreServiceUpsertsDuplicateGamePluginManifest(t *testing.T) { svc := newTestCoreService() registration := validPluginManifestRegistration() if _, err := svc.RegisterGamePluginManifest(registration); err != nil { t.Fatalf("register first manifest: %v", err) } registration.Manifest.Version = "0.1.1" registration.Manifest.Description = "Development plugin refreshed" registration.ManifestRef = "artifact://manifests/game.example/0.1.1" updated, err := svc.RegisterGamePluginManifest(registration) if err != nil { t.Fatalf("upsert manifest: %v", err) } if updated.ID != "game.example" || updated.Version != "0.1.1" || updated.ManifestRef != "artifact://manifests/game.example/0.1.1" { t.Fatalf("expected existing plugin to update in place, got %+v", updated) } listed, err := svc.ListGamePlugins(domain.GamePluginFilter{ServerType: "example", Status: domain.GamePluginStatusInstalled}) if err != nil || len(listed) != 1 || listed[0].ID != "game.example" || listed[0].Version != "0.1.1" { t.Fatalf("expected one refreshed plugin after upsert, listed=%+v err=%v", listed, err) } } func TestCoreServiceRejectsUnsafeGamePluginManifest(t *testing.T) { svc := newTestCoreService() registration := validPluginManifestRegistration() registration.Manifest.Description = "requires direct run socket and raw AI key" _, err := svc.RegisterGamePluginManifest(registration) if err == nil || !strings.Contains(err.Error(), "direct run access") || !strings.Contains(err.Error(), "raw credential") { t.Fatalf("expected unsafe manifest rejection, got %v", err) } } func TestCoreServiceRejectsInvalidAIProviderManagement(t *testing.T) { svc := newTestCoreService() provider := validProvider() if _, err := svc.CreateAIProvider(provider); err != nil { t.Fatalf("create provider: %v", err) } provider.APIKeyRef = "sk-raw-secret" _, err := svc.UpdateAIProvider(provider.ID, provider) if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") { t.Fatalf("expected raw secret rejection, got %v", err) } _, err = svc.SetAIProviderStatus(provider.ID, domain.AIProviderStatusError) if err == nil || !strings.Contains(err.Error(), "status must be active or disabled") { t.Fatalf("expected invalid status rejection, got %v", err) } _, err = svc.UpdateAIProvider("missing", provider) if !errors.Is(err, repo.ErrNotFound) { t.Fatalf("expected missing update target, got %v", err) } _, err = svc.TestAIProvider("missing") if !errors.Is(err, repo.ErrNotFound) { t.Fatalf("expected missing test target, got %v", err) } _, err = svc.ListAIProviderModels("missing") if !errors.Is(err, repo.ErrNotFound) { t.Fatalf("expected missing models target, got %v", err) } } func TestCoreServiceReturnsExistingJobForDuplicateIdempotencyKey(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) instance, err := svc.CreateServerInstance(domain.ServerInstance{ ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM #1", }) if err != nil { t.Fatalf("create server instance: %v", err) } first, err := svc.CreateJob(domain.Job{ ID: "job-1", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: "process.start", IdempotencyKey: "idem-start", }) if err != nil { t.Fatalf("create first job: %v", err) } second, err := svc.CreateJob(domain.Job{ ID: "job-2", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: "process.start", IdempotencyKey: "idem-start", }) if err != nil { t.Fatalf("create second job: %v", err) } if second.ID != first.ID { t.Fatalf("expected idempotent job %q, got %q", first.ID, second.ID) } jobs, err := svc.ListJobs(domain.JobFilter{RunEndpointID: endpoint.ID}) if err != nil { t.Fatalf("list jobs: %v", err) } if len(jobs) != 1 { t.Fatalf("expected one stored job, got %+v", jobs) } } func TestCoreServiceRejectsJobTargetMismatch(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) otherEndpoint := endpoint otherEndpoint.ID = "run-other" if _, err := svc.CreateRunEndpoint(otherEndpoint); err != nil { t.Fatalf("create other endpoint: %v", err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ ID: "server-1", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM #1", }) if err != nil { t.Fatalf("create server instance: %v", err) } _, err = svc.CreateJob(domain.Job{ ID: "job-1", ServerInstanceID: instance.ID, RunEndpointID: otherEndpoint.ID, Capability: "process.start", IdempotencyKey: "idem-start", }) if err == nil || !strings.Contains(err.Error(), "job runEndpointId must match server instance") { t.Fatalf("expected target mismatch rejection, got %v", err) } } func TestCoreServicePropagatesDuplicateErrors(t *testing.T) { svc := newTestCoreService() user := domain.User{ID: "user-1", DisplayName: "Operator", Status: domain.UserStatusActive} if _, err := svc.CreateUser(user); err != nil { t.Fatalf("create user: %v", err) } _, err := svc.CreateUser(user) if !errors.Is(err, repo.ErrDuplicate) { t.Fatalf("expected duplicate error, got %v", err) } } func newTestCoreService() *CoreService { svc := newCoreService(repo.NewMemoryStore(), func() time.Time { return fixedTime }) svc.ConfigureDistributionBuilder(staticDistributionBuilder{payload: []byte("platform-built-distribution")}) return svc } type staticDistributionBuilder struct { payload []byte err error } func (builder staticDistributionBuilder) Readiness() (bool, string) { if builder.err != nil { return false, builder.err.Error() } return true, "" } func (builder staticDistributionBuilder) Build(domain.DistributionBuildInput) ([]byte, error) { if builder.err != nil { return nil, builder.err } return domain.CopyBytes(builder.payload), nil } func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlugin, domain.RunEndpoint) { t.Helper() plugin, err := svc.CreateGamePlugin(domain.GamePlugin{ ID: "server.scum", Name: "SCUM", Version: "1.0.0", ServerType: "scum", ManifestRef: "artifact://manifests/server.scum/1.0.0", CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0", RequiredRunCapabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "config.write", "files.read", "files.write"}, DeclaredPermissions: []string{"server.files.read", "server.files.write"}, LifecycleActions: domain.PluginLifecycleActions{ Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", }, Permissions: domain.PluginPermissions{ Logs: true, 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) } endpoint, err := svc.CreateRunEndpoint(domain.RunEndpoint{ ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "config.write", "files.read", "files.write"}, Capacity: domain.RunCapacity{MaxJobs: 4}, LastHeartbeatAt: svc.now(), }) if err != nil { t.Fatalf("create run endpoint fixture: %v", err) } return plugin, endpoint } func createSQLiteQueryBridgeFixture(t *testing.T) (*CoreService, domain.GamePlugin, domain.RunEndpoint, string, domain.ServerInstance) { t.Helper() svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) capability := domain.JobCapabilityRemoteRunDBSQLiteQuery plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability) plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access") plugin.Permissions.RemoteAccess = true plugin.BridgeActions = append(plugin.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest)) plugin.Pages = append(plugin.Pages, domain.GamePluginPage{ Key: "remote", Title: "Remote", Path: "/remote", Permissions: []string{"server.remote.access"}, BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)}, }) plugin.RemoteAccess = domain.GamePluginRemoteAccess{ Methods: []string{"run"}, RunCapabilities: []string{capability}, DatabaseEngines: []string{"sqlite"}, } plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{ Key: "scum-db-read", Kind: "sqlite", TargetKey: "scum-db.player-lookup", Capabilities: []string{capability}, }) plugin.GameClientBridge = domain.GameClientBridgeManifest{ QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{ { Key: "players.by-id", Title: "Player lookup", Permission: "server.remote.access", Engine: "sqlite", TransportKey: "scum-db-read", TargetKey: "scum-db.player-lookup", ParameterSchemaRef: "schemas/queries/players.by-id.parameters.schema.json", ResultSchemaRef: "schemas/queries/players.by-id.result.schema.json", MaxRows: 25, TimeoutSeconds: 20, }, }, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}, Pages: []domain.GameClientBridgePageContract{ {PageKey: "remote", QueryTemplateKeys: []string{"players.by-id"}}, }, } if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update sqlite query plugin fixture: %v", err) } endpoint.Capabilities = append(endpoint.Capabilities, capability) if err := svc.store.RunEndpoints().Update(endpoint); err != nil { t.Fatalf("update sqlite query endpoint fixture: %v", err) } session := createServiceUserAndLogin(t, svc, domain.User{ ID: "user-query-owner", DisplayName: "Query Owner", Email: "query-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password", }) instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ ID: "server-query-template", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Query Template Server", State: domain.ServerInstanceStateRunning, }) if err != nil { t.Fatalf("create sqlite query server fixture: %v", err) } return svc, plugin, endpoint, session, instance } 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) } profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, profileKey) if !ok { t.Fatalf("runtime profile %s missing", profileKey) } required, _ := runtimeBindingKeys(plugin.RuntimeProfiles, profile) bindings := map[string]string{} for _, key := range required { if runtimeBindingTestKeyIsSensitive(key) { bindings[key] = "secret://" + instance.ID + "/" + strings.ReplaceAll(key, "/", "-") } else { bindings[key] = "runtime." + strings.ReplaceAll(key, "/", ".") } } binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: profileKey, Bindings: bindings}, 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 runtimeBindingTestKeyIsSensitive(key string) bool { normalized := strings.ToLower(key) return strings.Contains(normalized, "password") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "secret") || strings.Contains(normalized, "token") || strings.Contains(normalized, "dsn") } func validPluginManifestRegistration() domain.GamePluginManifestRegistration { return domain.GamePluginManifestRegistration{ ManifestRef: "artifact://manifests/game.example/0.1.0", Manifest: domain.GamePluginManifest{ ID: "game.example", Name: "Example Server", Description: "Development plugin", Version: "0.1.0", Kind: "game-plugin", Tags: []string{"example", "development"}, Server: domain.GamePluginManifestServer{ Type: "example", DisplayName: "Example Server", SupportedOS: []string{"linux", "darwin"}, CreateFormSchema: "schemas/create-form.schema.json", }, Bridge: domain.GamePluginBridge{ Actions: []string{ string(domain.PluginBridgeActionServerInstancesRead), string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke), }, }, Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"}, Permissions: []string{"server.read", "server.lifecycle", "server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"}, Actions: domain.PluginLifecycleActions{ Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Restart: "actions/restart.json", }, Pages: []domain.GamePluginPage{ { Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read", "ai.invoke"}, BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)}, }, }, AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"}, ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}}, }, } } func validProvider() domain.AIProvider { return domain.AIProvider{ ID: "ai.openai", Name: "OpenAI", Kind: domain.AIProviderKindOpenAI, BaseURL: "https://api.openai.com/v1", APIKeyRef: "secret://providers/openai", Models: []string{"gpt-4.1", "gpt-4.1-mini"}, DefaultModel: "gpt-4.1", RelayMode: domain.AIRelayModeDirect, TimeoutMS: 30000, RedactionPolicy: "default", } }