Files
browser/platform/service/resources_test.go
T
npc0-hue 6488f6b31d Keep SCUM poll jobs and drop orphan job log streams
The metadata snapshot deleted every SCUM sqlite query job while persisting, so
the recurring database poll lost its job records as soon as anything wrote the
snapshot and left its stdout/stderr streams behind. Those polls are the only
producer for the platform scum_user and scum_vehicle tables, and the service
already retires older terminal polls with their streams, so the snapshot no
longer drops them.

Startup now removes job log streams whose job no longer exists, keeping the
autonomous lifecycle streams the Run posts without a platform job. That clears
the streams left by the removed poll producers instead of carrying them in
every snapshot.
2026-09-16 18:41:44 +08:00

2467 lines
110 KiB
Go

package service
import (
"context"
"encoding/base64"
"errors"
"fmt"
"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)
}
}
func TestCoreServiceRejectsServerCreationOnStaleRunEndpoint(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter + time.Second) }
_, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-stale-run", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Stale Run Server"})
if err == nil || !strings.Contains(err.Error(), "get run endpoint dependency") || !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected stale Run endpoint to be rejected before binding, got %v", 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 TestCoreServiceStartupPrunesOrphanJobLogStreams(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: "orphan-stream-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Orphan Streams"}); err != nil {
t.Fatalf("create server instance: %v", err)
}
if err := store.Jobs().Create(domain.Job{
ID: "live-terminal-job",
ServerInstanceID: "orphan-stream-server",
RunEndpointID: endpoint.ID,
Capability: domain.JobCapabilityRemoteRunProgram,
TargetKey: "protected-program",
InputRef: "input://protected-program/live-terminal-job",
IdempotencyKey: "live-terminal",
State: domain.JobStateQueued,
CreatedAt: fixedTime,
UpdatedAt: fixedTime,
}); err != nil {
t.Fatalf("seed live job: %v", err)
}
orphanStream := domain.LogStream{
ID: jobLogStreamID("job-plugin-query-poll-gone", "stdout"),
ServerInstanceID: "orphan-stream-server",
Source: domain.LogStreamSourceProcess,
StreamKey: "stdout",
StorageBackend: domain.LogStorageBackendLocalSegments,
RetentionPolicy: "default",
}
autonomousStream := domain.LogStream{
ID: jobLogStreamID("autonomous-bootstrap-start", "scum.console.stdout"),
ServerInstanceID: "orphan-stream-server",
Source: domain.LogStreamSourceProcess,
StreamKey: "scum.console.stdout",
StorageBackend: domain.LogStorageBackendLocalSegments,
RetentionPolicy: "default",
}
for _, stream := range []domain.LogStream{orphanStream, autonomousStream} {
if err := store.LogStreams().Create(stream); err != nil {
t.Fatalf("seed log stream %s: %v", stream.ID, err)
}
}
recovered, err := NewCoreServiceWithDurableStores(store, NewMemoryLogBodyStore(), NewMemoryArtifactBodyStore())
if err != nil {
t.Fatalf("recover durable service: %v", err)
}
streams, err := recovered.ListLogStreams(domain.LogStreamFilter{ServerInstanceID: "orphan-stream-server"})
if err != nil {
t.Fatalf("list recovered streams: %v", err)
}
byID := map[string]domain.LogStream{}
for _, stream := range streams {
byID[stream.ID] = stream
}
if _, exists := byID[orphanStream.ID]; exists {
t.Fatalf("expected orphan job log stream to be pruned, streams=%+v", byID)
}
if _, exists := byID[autonomousStream.ID]; !exists {
t.Fatalf("expected autonomous job log stream to survive, streams=%+v", byID)
}
if _, exists := byID[jobLogStreamID("live-terminal-job", "stdout")]; !exists {
t.Fatalf("expected live job log stream to be recovered, streams=%+v", byID)
}
}
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 TestCoreServiceMergesRecentPartialMetricSamples(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-partial-metrics", DisplayName: "Partial Metrics Owner", Email: "partial-metrics@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-partial-metrics", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Partial Metrics", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
runHello := validRunControlHello()
runHello.RunEndpointID = endpoint.ID
registered, err := svc.RegisterRunHello(runHello)
if err != nil {
t.Fatalf("register run: %v", err)
}
cpu := 37.0
memory := 49.0
players := 8
disk := 78.0
staleMemory := 12.0
if _, err := svc.IngestMetricBatch(domain.MetricBatchIngest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Samples: []domain.MetricSample{
{ServerInstanceID: instance.ID, Online: true, CPUPercent: &cpu, MemoryPercent: &memory, PlayerCount: &players, Source: "run", CollectedAt: fixedTime.Add(-15 * time.Second)},
{ServerInstanceID: instance.ID, Online: true, MemoryPercent: &staleMemory, Source: "run", CollectedAt: fixedTime.Add(-3 * time.Minute)},
{ServerInstanceID: instance.ID, Online: true, DiskPercent: &disk, Source: "run", CollectedAt: fixedTime},
}}); err != nil {
t.Fatalf("ingest partial metrics: %v", err)
}
metrics, err := svc.ListServerMetricsForSession(ownerSession)
if err != nil {
t.Fatalf("list merged metrics: %v", err)
}
if len(metrics) != 1 || metrics[0].CPUPercent == nil || *metrics[0].CPUPercent != cpu || metrics[0].MemoryPercent == nil || *metrics[0].MemoryPercent != memory || metrics[0].PlayerCount == nil || *metrics[0].PlayerCount != players || metrics[0].DiskPercent == nil || *metrics[0].DiskPercent != disk || metrics[0].CollectedAt != fixedTime {
t.Fatalf("expected recent partial metrics to merge, got %+v", metrics)
}
}
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 TestPluginFileWorkspaceDoesNotConstrainServerFileDispatch(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,
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `C:\scumserver`},
})
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 || allowed.Job.ExecutionInput.Deployment == nil || allowed.Job.ExecutionInput.Deployment.ServerRoot != `C:\scumserver` {
t.Fatalf("unexpected declared file dispatch: %+v", allowed)
}
unknown, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
Operation: domain.FileOperationRead,
Key: "logs/latest.log",
IdempotencyKey: "idem-file-workspace-unknown",
})
if err != nil || unknown.Job.TargetKey != "logs/latest.log" || unknown.Job.Capability != domain.JobCapabilityFilesRead {
t.Fatalf("expected undeclared file read to queue, dispatch=%+v err=%v", unknown, err)
}
written, 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",
})
if err != nil || written.Job.TargetKey != "scum-chat-log" || written.Job.Capability != domain.JobCapabilityFilesWrite || written.Job.ExecutionInput.Deployment == nil || written.Job.ExecutionInput.Deployment.ServerRoot != `C:\scumserver` {
t.Fatalf("expected declared log write to queue, dispatch=%+v err=%v", written, err)
}
}
func TestReadRunFileInputChunkReadsRequestedRange(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-chunk", DisplayName: "File Chunk Owner", Email: "file-chunk@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-chunk", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Chunk Server", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
payload := []byte("0123456789abcdef")
upload, err := svc.UploadServerFileForSession(ownerSession, domain.ServerFileUploadRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-logs", Filename: "chunked.log", Payload: payload, Checksum: validator.BytesChecksum(payload), IdempotencyKey: "file-chunk-upload"})
if err != nil {
t.Fatalf("upload server file: %v", err)
}
hello := validRunControlHello()
hello.RunEndpointID = endpoint.ID
hello.CapabilityReport.Capabilities = []string{domain.JobCapabilityFilesWrite}
registered, err := svc.RegisterRunHello(hello)
if err != nil {
t.Fatalf("register run: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Capabilities: []string{domain.JobCapabilityFilesWrite}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job.JobID != upload.Job.ID {
t.Fatalf("claim file write: claim=%+v err=%v", claim, err)
}
chunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 4, Length: 6})
if err != nil || string(chunk.Payload) != string(payload[4:10]) || chunk.Offset != 4 || chunk.Complete {
t.Fatalf("read offset file input chunk: chunk=%+v err=%v", chunk, err)
}
finalChunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 14, Length: 8})
if err != nil || string(finalChunk.Payload) != string(payload[14:]) || !finalChunk.Complete {
t.Fatalf("read final file input chunk: chunk=%+v err=%v", finalChunk, err)
}
}
func TestServerFileListReportsFailedRuntimeRefresh(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityFilesList)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update file list capability: %v", err)
}
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-list-failure", DisplayName: "File List Failure", Email: "file-list-failure@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-failure", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File List Failure Server", State: domain.ServerInstanceStateRunning, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `C:\scumserver`}})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
refresh, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "server-root", IdempotencyKey: "idem-file-list-failure"})
if err != nil || refresh.State != "pending" || refresh.Job.ID == "" || refresh.Job.ExecutionInput.Deployment == nil || refresh.Job.ExecutionInput.Deployment.ServerRoot != `C:\scumserver` {
t.Fatalf("refresh file list: result=%+v err=%v", refresh, err)
}
failedJob := refresh.Job
failedJob.State = domain.JobStateFailed
failedJob.Progress = domain.JobProgress{Percent: 100, Message: "executor does not support files.list"}
failedJob.TerminalAt = fixedTime.Add(2 * time.Minute)
failedJob.UpdatedAt = failedJob.TerminalAt
if err := svc.store.Jobs().Update(failedJob); err != nil {
t.Fatalf("update failed file list job: %v", err)
}
list, err := svc.ListServerFilesForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "server-root"})
if err != nil || list.State != "failed" || list.Job.ID != failedJob.ID || !strings.Contains(list.Reason, "executor does not support files.list") {
t.Fatalf("expected failed file list state, list=%+v err=%v", list, err)
}
}
func TestServerFileManagerUsesDefaultProfileWithoutRuntimeBinding(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityFilesList)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update file list capability: %v", err)
}
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-default-profile", DisplayName: "File Default Profile", Email: "file-default-profile@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-default-profile", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Default Profile Server", State: domain.ServerInstanceStateRunning, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `C:\scumserver`}})
if err != nil {
t.Fatalf("create server: %v", err)
}
if _, err := svc.runtimeBindingForServer(instance.ID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("file manager must not require a runtime binding: %v", err)
}
list, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "server-root", IdempotencyKey: "file-default-profile-list"})
if err != nil || list.State != "pending" || list.Job.ExecutionInput.WorkspaceScope != "local" {
t.Fatalf("expected default-profile file list job, result=%+v err=%v", list, err)
}
read, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{ServerInstanceID: instance.ID, PluginID: plugin.ID, Operation: domain.FileOperationRead, Key: "logs/latest.log", IdempotencyKey: "file-default-profile-read"})
if err != nil || read.Job.ExecutionInput.WorkspaceScope != "local" {
t.Fatalf("expected default-profile file read job, result=%+v err=%v", read, err)
}
}
func TestServerFileBrowseWaitsForFreshRunResultWithoutCachedList(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityFilesList)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update file list capability: %v", err)
}
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-browse", DisplayName: "File Browse", Email: "file-browse@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-browse", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Browse Server", State: domain.ServerInstanceStateRunning, Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `C:\scumserver`}})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
oldJob, err := svc.CreateJob(domain.Job{ID: "job-file-list-old", ServerInstanceID: instance.ID, RunEndpointID: endpoint.ID, Capability: domain.JobCapabilityFilesList, TargetKey: "server-root", IdempotencyKey: "idem-file-list-old"})
if err != nil {
t.Fatalf("create old list job: %v", err)
}
oldJob.State = domain.JobStateSucceeded
oldJob.ExecutionResult = domain.JobExecutionResult{Kind: "file.list", Content: runFileListFixture("server-root", "", ".platform")}
oldJob.TerminalAt = fixedTime.Add(10 * time.Minute)
oldJob.UpdatedAt = oldJob.TerminalAt
if err := svc.store.Jobs().Update(oldJob); err != nil {
t.Fatalf("store old list result: %v", err)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityFilesList)
helloRequest.CapabilityReport.Fingerprint = "cap-file-browse"
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register run hello: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resultCh := make(chan domain.ServerFileListResult, 1)
errCh := make(chan error, 1)
go func() {
result, err := svc.BrowseServerFilesForSession(ctx, ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "server-root", IdempotencyKey: "idem-file-browse-fresh"})
if err != nil {
errCh <- err
return
}
resultCh <- result
}()
job := waitForServerFileListJob(t, svc, instance.ID, "job-file-list-old")
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, Capabilities: []string{domain.JobCapabilityFilesList}, Capacity: domain.RunCapacity{MaxJobs: 4}})
if err != nil || !claim.HasJob || claim.Job.JobID != job.ID {
t.Fatalf("claim fresh file list job: claim=%+v err=%v", claim, err)
}
_, 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: "listed"}, ExecutionResult: domain.JobExecutionResult{Kind: "file.list", Content: runFileListFixture("server-root", "", "SCUM")}})
if err != nil {
t.Fatalf("complete fresh file list job: %v", err)
}
select {
case err := <-errCh:
t.Fatalf("browse failed: %v", err)
case result := <-resultCh:
if result.State != "ready" || len(result.Entries) != 1 || result.Entries[0].Name != "SCUM" || result.Job.ID != job.ID {
t.Fatalf("expected fresh browse result, got %+v", result)
}
case <-ctx.Done():
t.Fatalf("browse timed out: %v", ctx.Err())
}
}
func TestServerFileListFallsBackToPluginWorkspaceWithoutRunListCapability(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-list-fallback", DisplayName: "File List Fallback", Email: "file-list-fallback@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-fallback", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File List Fallback Server", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
createCompleteRuntimeBinding(t, svc, instance, "local")
refresh, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-fallback"})
if err != nil {
t.Fatalf("refresh file list fallback: %v", err)
}
if refresh.State != "ready" || refresh.Job.ID != "" || len(refresh.Entries) != 2 || refresh.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(refresh.Reason, "未声明 files.list") {
t.Fatalf("expected plugin workspace fallback, got %+v", refresh)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil {
t.Fatalf("list fallback jobs: %v", err)
}
if len(jobs) != 0 {
t.Fatalf("unsupported Run capability must not create a failed refresh job, got %+v", jobs)
}
}
func TestServerFileListFallsBackToPluginWorkspaceWhenRunEndpointUnavailable(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-list-run-missing", DisplayName: "File List Run Missing", Email: "file-list-run-missing@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-missing", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File List Missing Run", State: domain.ServerInstanceStateRunning})
if err != nil {
t.Fatalf("create server: %v", err)
}
if err := svc.store.RunEndpoints().Delete(endpoint.ID); err != nil {
t.Fatalf("delete run endpoint: %v", err)
}
refresh, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-missing"})
if err != nil {
t.Fatalf("refresh file list without run endpoint: %v", err)
}
if refresh.State != "ready" || refresh.Job.ID != "" || len(refresh.Entries) != 2 || refresh.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(refresh.Reason, "Run 注册记录不存在") {
t.Fatalf("expected declared workspace fallback without run endpoint, got %+v", refresh)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil {
t.Fatalf("list missing-run jobs: %v", err)
}
if len(jobs) != 0 {
t.Fatalf("missing Run endpoint must not create a failed refresh job, got %+v", jobs)
}
draft, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-list-run-unbound", PluginID: plugin.ID, Name: "File List Unbound Run", State: domain.ServerInstanceStateDraft})
if err != nil {
t.Fatalf("create unbound draft server: %v", err)
}
unbound, err := svc.RefreshServerFileListForSession(ownerSession, domain.ServerFileListRequest{ServerInstanceID: draft.ID, DirectoryKey: "scum-config", IdempotencyKey: "idem-file-list-run-unbound"})
if err != nil {
t.Fatalf("refresh file list without run binding: %v", err)
}
if unbound.State != "ready" || unbound.Job.ID != "" || len(unbound.Entries) != 2 || unbound.Entries[1].LogicalKey != "scum-server-settings" || !strings.Contains(unbound.Reason, "尚未绑定 Run") {
t.Fatalf("expected declared workspace fallback without run binding, got %+v", unbound)
}
}
func TestDeclaredFileReadSnapshotProjectionStatesAndPassThroughContent(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=secret") {
t.Fatalf("expected older successful pass-through 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" || snapshot.Content != "ServerName=New\nApiToken=secret\n" {
t.Fatalf("expected newest successful pass-through result, snapshot=%+v err=%v", snapshot, err)
}
unknownSnapshot, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "logs/latest.log")
if err != nil || unknownSnapshot.State != "not-read" {
t.Fatalf("expected unknown logical key to report not-read, snapshot=%+v err=%v", unknownSnapshot, 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)), Summary: "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}, {Path: "assets/map.bin", Mode: 0o600}}
registration.AssetFiles = []domain.PluginAssetFile{{Path: "actions/install.json", Content: "{}", Mode: 0o600}, {Path: "bin/install-server", Content: "#!/usr/bin/env sh\n"}, {Path: "assets/map.bin", Content: base64.StdEncoding.EncodeToString([]byte{0xff, 0x00, 0x7f}), Encoding: "base64"}}
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) != 3 || plugin.LifecycleAssets[1].Path != "bin/install-server" || plugin.LifecycleAssets[1].Mode != 0o700 || plugin.LifecycleAssets[2].Encoding != "base64" {
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 TestCoreServiceMarketplaceListSkipsHistoricalInvalidCapabilities(t *testing.T) {
svc := newTestCoreService()
if _, err := svc.RegisterGamePluginManifest(validPluginManifestRegistration()); err != nil {
t.Fatalf("register current manifest: %v", err)
}
legacy := gamePluginFromManifestRegistration(validPluginManifestRegistration())
legacy.ID = "game.legacy"
legacy.Name = "Legacy Server"
legacy.RequiredRunCapabilities = append(legacy.RequiredRunCapabilities, "remote.run.protected.sql", "remote.run.protected.rcon")
legacy.RemoteAccess.RunCapabilities = append(legacy.RemoteAccess.RunCapabilities, "remote.run.protected.sql", "remote.run.protected.rcon")
if err := svc.store.GamePlugins().Create(legacy); err != nil {
t.Fatalf("seed historical plugin: %v", err)
}
listed, err := svc.ListMarketplacePlugins(domain.PluginMarketplaceFilter{})
if err != nil {
t.Fatalf("list marketplace plugins with historical record: %v", err)
}
if len(listed) != 1 || listed[0].ID != "game.example" {
t.Fatalf("expected only current marketplace plugin, got %+v", listed)
}
}
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": "game-db-read",
"targetKey": "game-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["sqlRef"] != "sql/players.by-id.sql" || 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: "game-db-read", targetKey: "game-db.player-lookup"},
{name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "game-db.player-lookup"},
{name: "mismatched target", templateKey: "players.by-id", declarationKey: "game-db-read", targetKey: "game-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 TestCoreServiceDispatchesSQLiteExecuteSQLText(t *testing.T) {
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
RequestID: "sqlite-execute-1",
PluginID: plugin.ID,
RouteKey: "remote",
ServerInstanceID: instance.ID,
Action: domain.PluginBridgeActionRemoteAccessRequest,
Payload: map[string]string{
"capability": domain.JobCapabilityRemoteRunDBSQLiteExecute,
"declarationKey": "game-db-read",
"targetKey": "game-db.player-lookup",
"idempotencyKey": "sqlite-execute-1",
"input.mode": "execute",
"input.sqlText": "UPDATE players SET score = 855 WHERE id = 'player-123';",
},
})
if err != nil {
t.Fatalf("execute sqlite SQL bridge input: %v", err)
}
if result.Status != "queued" || result.Result["jobId"] == "" {
t.Fatalf("expected queued sqlite execute job, got %+v", result)
}
job, getErr := svc.store.Jobs().Get(result.Result["jobId"])
if getErr != nil {
t.Fatalf("get sqlite execute job: %v", getErr)
}
if job.Capability != domain.JobCapabilityRemoteRunDBSQLiteExecute || job.ExecutionInput.Inputs["sqlText"] == "" || job.ExecutionInput.Inputs["mode"] != "execute" {
t.Fatalf("expected sqlite execute inputs, got %#v", job)
}
}
func TestCoreServiceUpsertsDuplicateGamePluginManifest(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
if _, err := svc.RegisterGamePluginManifest(registration); err != nil {
t.Fatalf("register first manifest: %v", err)
}
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, Name: "Manifest refresh server"}); err != nil {
t.Fatalf("create server for manifest refresh: %v", err)
}
if err := svc.store.RuntimeBindings().Create(domain.RuntimeBinding{ID: "runtime-binding-manifest-refresh-server", ServerInstanceID: "manifest-refresh-server", PluginID: registration.Manifest.ID, PluginVersion: registration.Manifest.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
t.Fatalf("create runtime binding for manifest refresh: %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)
}
server, err := svc.GetServerInstance("manifest-refresh-server")
if err != nil || server.PluginVersion != "0.1.1" {
t.Fatalf("expected existing server to follow refreshed plugin version, server=%+v err=%v", server, err)
}
binding, err := svc.store.RuntimeBindings().Get("runtime-binding-manifest-refresh-server")
if err != nil || binding.PluginVersion != "0.1.1" {
t.Fatalf("expected runtime binding to follow refreshed plugin version, binding=%+v err=%v", binding, err)
}
}
func TestCoreServiceKeepsOnlyLatestPluginVersionAndMigratesReferences(t *testing.T) {
svc := newTestCoreService()
stale := validPluginManifestRegistration()
stale.Manifest.ID = "game.scum.codex.20260804095301"
stale.Manifest.Name = "SCUM Server"
stale.Manifest.Version = "0.1.4"
stale.Manifest.Server.Type = "scum"
stale.Manifest.Server.DisplayName = "SCUM Dedicated Server"
stale.ManifestRef = "plugins/examples/scum-server-plugin/game.scum.codex.20260804095301/manifest.json"
if _, err := svc.RegisterGamePluginManifest(stale); err != nil {
t.Fatalf("register stale manifest: %v", err)
}
if _, err := svc.CreateServerInstance(domain.ServerInstance{ID: "stale-plugin-server", PluginID: stale.Manifest.ID, PluginVersion: stale.Manifest.Version, Name: "SCUM Old Plugin"}); err != nil {
t.Fatalf("create stale plugin server: %v", err)
}
if err := svc.store.RuntimeBindings().Create(domain.RuntimeBinding{ID: "runtime-binding-stale-plugin-server", ServerInstanceID: "stale-plugin-server", PluginID: stale.Manifest.ID, PluginVersion: stale.Manifest.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
t.Fatalf("create stale runtime binding: %v", err)
}
if err := svc.store.PluginDataRecords().Create(domain.PluginDataRecord{ID: pluginDataID("stale-plugin-server", stale.Manifest.ID, "scum_gifts", "starter"), PluginID: stale.Manifest.ID, ServerInstanceID: "stale-plugin-server", Collection: "scum_gifts", Key: "starter", Value: map[string]any{"name": "Starter"}, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
t.Fatalf("seed plugin config data: %v", err)
}
if err := svc.store.PluginDataRecords().Create(domain.PluginDataRecord{ID: pluginDataID("stale-plugin-server", stale.Manifest.ID, "scum_trajectories", "point-1"), PluginID: stale.Manifest.ID, ServerInstanceID: "stale-plugin-server", Collection: "scum_trajectories", Key: "point-1", Value: map[string]any{"source": "legacy"}, CreatedAt: fixedTime, UpdatedAt: fixedTime}); err != nil {
t.Fatalf("seed legacy projection data: %v", err)
}
latest := validPluginManifestRegistration()
latest.Manifest.ID = "game.scum"
latest.Manifest.Name = "SCUM Server"
latest.Manifest.Version = "0.1.15"
latest.Manifest.Server.Type = "scum"
latest.Manifest.Server.DisplayName = "SCUM Dedicated Server"
latest.ManifestRef = "artifact://manifests/game.scum/0.1.15"
registered, err := svc.RegisterGamePluginManifest(latest)
if err != nil {
t.Fatalf("register latest manifest: %v", err)
}
if registered.ID != "game.scum" || registered.Version != "0.1.15" {
t.Fatalf("expected latest plugin to win, got %+v", registered)
}
if _, err := svc.GetGamePlugin(stale.Manifest.ID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("expected stale plugin record to be removed, got %v", err)
}
plugins, err := svc.ListGamePlugins(domain.GamePluginFilter{ServerType: "scum", Status: domain.GamePluginStatusInstalled})
if err != nil || len(plugins) != 1 || plugins[0].ID != "game.scum" || plugins[0].Version != "0.1.15" {
t.Fatalf("expected only latest SCUM plugin, plugins=%+v err=%v", plugins, err)
}
server, err := svc.GetServerInstance("stale-plugin-server")
if err != nil || server.PluginID != "game.scum" || server.PluginVersion != "0.1.15" {
t.Fatalf("expected server to migrate to latest plugin, server=%+v err=%v", server, err)
}
binding, err := svc.store.RuntimeBindings().Get("runtime-binding-stale-plugin-server")
if err != nil || binding.PluginID != "game.scum" || binding.PluginVersion != "0.1.15" {
t.Fatalf("expected runtime binding to migrate to latest plugin, binding=%+v err=%v", binding, err)
}
configRecords, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: "game.scum", ServerInstanceID: "stale-plugin-server", Collection: "scum_gifts"})
if err != nil || len(configRecords) != 1 || configRecords[0].Key != "starter" {
t.Fatalf("expected plugin config data to migrate, records=%+v err=%v", configRecords, err)
}
legacyRecords, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{ServerInstanceID: "stale-plugin-server", Collection: "scum_trajectories"})
if err != nil || len(legacyRecords) != 0 {
t.Fatalf("expected legacy SCUM projection data to be dropped, records=%+v err=%v", legacyRecords, 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
executeCapability := domain.JobCapabilityRemoteRunDBSQLiteExecute
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability, executeCapability)
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, executeCapability},
DatabaseEngines: []string{"sqlite"},
}
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{
Key: "game-db-read",
Kind: "sqlite",
TargetKey: "game-db.player-lookup",
Capabilities: []string{capability, executeCapability},
})
plugin.GameClientBridge = domain.GameClientBridgeManifest{
QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{
{
Key: "players.by-id",
Title: "Player lookup",
Permission: "server.remote.access",
Engine: "sqlite",
TransportKey: "game-db-read",
TargetKey: "game-db.player-lookup",
ParameterSchemaRef: "schemas/queries/players.by-id.parameters.schema.json",
ResultSchemaRef: "schemas/queries/players.by-id.result.schema.json",
SQLRef: "sql/players.by-id.sql",
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, executeCapability)
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 waitForServerFileListJob(t *testing.T, svc *CoreService, serverInstanceID string, excludedJobID string) domain.Job {
t.Helper()
deadline := time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: serverInstanceID})
if err != nil {
t.Fatalf("list server jobs: %v", err)
}
for _, job := range jobs {
if job.ID != excludedJobID && job.Capability == domain.JobCapabilityFilesList {
return job
}
}
time.Sleep(time.Millisecond)
}
t.Fatalf("fresh file list job was not created")
return domain.Job{}
}
func runFileListFixture(directoryKey string, relativePath string, name string) string {
return fmt.Sprintf(`{"directoryKey":%q,"path":%q,"entries":[{"name":%q,"kind":"directory","directoryKey":%q,"relativePath":%q}]}`, directoryKey, relativePath, name, directoryKey, name)
}
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"},
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",
}
}