Files

458 lines
25 KiB
Go

package repo
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
func TestMemoryRepositoryRejectsDuplicateAndCopiesValues(t *testing.T) {
store := NewMemoryStore()
user := domain.User{
ID: "user-1",
DisplayName: "Mary",
Status: domain.UserStatusActive,
Roles: []string{"admin"},
}
if err := store.Users().Create(user); err != nil {
t.Fatalf("create user: %v", err)
}
if err := store.Users().Create(user); !errors.Is(err, ErrDuplicate) {
t.Fatalf("expected duplicate error, got %v", err)
}
got, err := store.Users().Get(user.ID)
if err != nil {
t.Fatalf("get user: %v", err)
}
got.Roles[0] = "mutated"
again, err := store.Users().Get(user.ID)
if err != nil {
t.Fatalf("get user again: %v", err)
}
if again.Roles[0] != "admin" {
t.Fatalf("expected stored roles to be isolated, got %+v", again.Roles)
}
}
func TestMemoryRepositoryListFiltersAndSorts(t *testing.T) {
store := NewMemoryStore()
users := []domain.User{
{ID: "user-c", DisplayName: "C", Status: domain.UserStatusDisabled},
{ID: "user-b", DisplayName: "B", Status: domain.UserStatusActive},
{ID: "user-a", DisplayName: "A", Status: domain.UserStatusActive},
}
for _, user := range users {
if err := store.Users().Create(user); err != nil {
t.Fatalf("create user %s: %v", user.ID, err)
}
}
active, err := store.Users().List(domain.UserFilter{Status: domain.UserStatusActive})
if err != nil {
t.Fatalf("list users: %v", err)
}
if len(active) != 2 || active[0].ID != "user-a" || active[1].ID != "user-b" {
t.Fatalf("expected sorted active users, got %+v", active)
}
}
func TestMemoryJobRepositoryFindsIdempotencyKey(t *testing.T) {
store := NewMemoryStore()
job := domain.Job{
ID: "job-1",
RunEndpointID: "run-local",
Capability: "process.start",
IdempotencyKey: "idem-1",
State: domain.JobStateQueued,
}
if err := store.Jobs().Create(job); err != nil {
t.Fatalf("create job: %v", err)
}
got, err := store.Jobs().GetByIdempotency("run-local", "idem-1")
if err != nil {
t.Fatalf("get by idempotency: %v", err)
}
if got.ID != job.ID {
t.Fatalf("expected job %q, got %q", job.ID, got.ID)
}
_, err = store.Jobs().GetByIdempotency("run-local", "missing")
if !errors.Is(err, ErrNotFound) {
t.Fatalf("expected not found for missing idempotency key, got %v", err)
}
}
func TestFileStorePersistsAndReloadsResources(t *testing.T) {
path := filepath.Join(t.TempDir(), "metadata.json")
store, err := NewFileStore(path)
if err != nil {
t.Fatalf("create file store: %v", err)
}
user := domain.User{
ID: "user-1",
DisplayName: "Durable User",
Status: domain.UserStatusActive,
Roles: []string{"platform-admin"},
}
if err := store.Users().Create(user); err != nil {
t.Fatalf("create user: %v", err)
}
stamp := time.Date(2026, 7, 17, 12, 0, 0, 0, time.UTC)
job := domain.Job{
ID: "job-1",
RunEndpointID: "run-local",
ServerInstanceID: "server-1",
Capability: "process.start",
IdempotencyKey: "idem-1",
State: domain.JobStateQueued,
RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 4, InitialBackoffSeconds: 3, MaxBackoffSeconds: 30},
Attempt: 2,
QueueEligibleAt: stamp,
NextAttemptAt: stamp.Add(3 * time.Second),
LeaseTokenHash: strings.Repeat("d", 64),
LeaseSessionGen: 2,
AckDeadlineAt: stamp.Add(15 * time.Second),
LeaseExpiresAt: stamp.Add(time.Minute),
LastProgressSeq: 7,
CancelReason: "operator requested",
CancelRequestedAt: stamp,
LastReconciledAt: stamp,
ReconcileCount: 2,
ReconcileOutcome: "confirmed active attempt",
ExecutionInput: domain.JobExecutionInput{
WorkspaceScope: "local",
Content: "name=approved\n",
ExpectedVersion: 1,
ExpectedChecksum: "sha256:" + strings.Repeat("1", 64),
MaxReadBytes: 64 * 1024,
Inputs: map[string]string{"templateKey": "players.by-id", "playerId": "steam-123"},
Deployment: &domain.ServerDeploymentDefinition{
Mode: domain.ServerDeploymentModeGuided,
ProfileKey: "local",
CreateInputs: map[string]string{"gamePort": "27000", "maxPlayers": "128"},
ServerRoot: "D:\\scumserver",
Revision: 2,
},
ServerDeploymentPlan: &domain.ServerDeploymentPlan{SchemaVersion: "1", Operation: "install", PluginID: "game.runtime", Prerequisites: []domain.RuntimeServerPrerequisite{{Key: "steamcmd", Kind: "tool"}}},
},
ExecutionResult: domain.JobExecutionResult{Kind: "file.read", Version: 2, Checksum: "sha256:" + strings.Repeat("2", 64), SizeBytes: 15, Summary: "bounded read", Content: "private-read"},
}
if err := store.Jobs().Create(job); err != nil {
t.Fatalf("create job: %v", err)
}
plugin := domain.GamePlugin{ID: "game.runtime", Name: "Runtime", Version: "1.0.0", RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}}}}
if err := store.GamePlugins().Create(plugin); err != nil {
t.Fatalf("create plugin: %v", err)
}
binding := domain.RuntimeBinding{ID: "runtime-binding-server-1", ServerInstanceID: "server-1", PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: "local", Mode: "local-process", Bindings: map[string]string{"rcon.password": "secret://server-1/rcon"}, Status: domain.RuntimeBindingStatusComplete, CreatedAt: stamp, UpdatedAt: stamp}
if err := store.RuntimeBindings().Create(binding); err != nil {
t.Fatalf("create runtime binding: %v", err)
}
runEndpoint := domain.RunEndpoint{ID: "run-target", DisplayName: "Target Run", Version: "release-1", Platform: "linux", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.JobCapabilityDependenciesInstall, domain.JobCapabilityRunSelfUpdate}, Capacity: domain.RunCapacity{MaxJobs: 2}, LastHeartbeatAt: stamp}
if err := store.RunEndpoints().Create(runEndpoint); err != nil {
t.Fatalf("create target Run endpoint: %v", err)
}
planDigest := "sha256:" + strings.Repeat("e", 64)
dependencyStatus := domain.DependencyStatus{ID: "dependency-server-1-java", ServerInstanceID: "server-1", PluginID: plugin.ID, ProbeKey: "java", TargetOS: "linux", TargetArch: "amd64", State: domain.DependencyStatePresent, Required: true, InstallPlanKey: "java-install", PlanDigest: planDigest, JobID: "job-dependency", Evidence: "OpenJDK 21", CompletedSteps: 1, Message: "dependency execution completed", CheckedAt: stamp, UpdatedAt: stamp}
if err := store.DependencyStatuses().Create(dependencyStatus); err != nil {
t.Fatalf("create dependency status: %v", err)
}
runUpdate := domain.RunUpdateJob{ID: "run-update-1", ServerInstanceID: "server-1", RunEndpointID: runEndpoint.ID, ArtifactID: "artifact-run-2", Checksum: planDigest, TargetOS: "linux", TargetArch: "amd64", TargetRelease: "release-2", PreviousVersion: "release-1", JobID: "job-update", IdempotencyKey: "run-update-idempotent", Status: domain.DistributionJobStatusFailed, Phase: domain.RunUpdatePhaseRolledBack, Message: "previous executable restored", Rollback: true, CreatedAt: stamp, UpdatedAt: stamp}
if err := store.RunUpdateJobs().Create(runUpdate); err != nil {
t.Fatalf("create Run update status: %v", err)
}
authSession := domain.AuthSessionRecord{ID: "auth-session-1", UserID: user.ID, TokenHash: strings.Repeat("a", 64), Status: domain.AuthSessionStatusActive, Generation: 1, IssuedAt: stamp, ExpiresAt: stamp.Add(time.Hour), LastSeenAt: stamp}
if err := store.AuthSessions().Create(authSession); err != nil {
t.Fatalf("create auth session: %v", err)
}
runSession := domain.RunControlSession{RunEndpointID: "run-local", SessionToken: "raw-run-token", SessionTokenHash: strings.Repeat("b", 64), Status: domain.AuthSessionStatusActive, Generation: 2, CapabilityFingerprint: "cap-v2", HeartbeatIntervalSeconds: 15, CreatedAt: stamp, UpdatedAt: stamp, ExpiresAt: stamp.Add(time.Hour), RequireSignedRequests: true, UsedNonces: []string{"nonce-1"}}
if err := store.RunControlSessions().Create(runSession); err != nil {
t.Fatalf("create run session: %v", err)
}
componentKey := domain.EncryptedComponentKey{ID: "component-key-1", ServerInstanceID: "server-1", ComponentKind: domain.DistributionComponentRun, EncryptedKey: "ciphertext-only", KeyHash: strings.Repeat("c", 64), Fingerprint: "fingerprint", SecretRef: "secret://components/server-1/run", Generation: 1, Status: domain.ComponentKeyStatusActive, CreatedAt: stamp, UpdatedAt: stamp}
if err := store.EncryptedComponentKeys().Create(componentKey); err != nil {
t.Fatalf("create component key: %v", err)
}
payload, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read snapshot: %v", err)
}
if strings.Contains(string(payload), "raw-run-token") {
t.Fatalf("snapshot exposed raw Run token: %s", payload)
}
if strings.Contains(string(payload), "raw-job-lease") {
t.Fatalf("snapshot exposed raw job lease: %s", payload)
}
reloaded, err := NewFileStore(path)
if err != nil {
t.Fatalf("reload file store: %v", err)
}
got, err := reloaded.Users().Get("user-1")
if err != nil {
t.Fatalf("get reloaded user: %v", err)
}
if got.DisplayName != user.DisplayName || got.Roles[0] != "platform-admin" {
t.Fatalf("unexpected reloaded user: %+v", got)
}
gotJob, err := reloaded.Jobs().GetByIdempotency("run-local", "idem-1")
if err != nil {
t.Fatalf("get reloaded job by idempotency: %v", err)
}
if gotJob.ID != "job-1" || gotJob.ServerInstanceID != "server-1" || gotJob.Attempt != 2 || gotJob.RetryPolicy.MaxAttempts != 4 || gotJob.LeaseTokenHash != strings.Repeat("d", 64) || gotJob.ReconcileCount != 2 || gotJob.ExecutionInput.Content != "name=approved\n" || gotJob.ExecutionInput.Inputs["templateKey"] != "players.by-id" || gotJob.ExecutionInput.Inputs["playerId"] != "steam-123" || gotJob.ExecutionInput.Deployment == nil || gotJob.ExecutionInput.Deployment.CreateInputs["gamePort"] != "27000" || gotJob.ExecutionInput.ServerDeploymentPlan == nil || gotJob.ExecutionInput.ServerDeploymentPlan.Prerequisites[0].Key != "steamcmd" || gotJob.ExecutionResult.Content != "private-read" {
t.Fatalf("unexpected reloaded job: %+v", gotJob)
}
gotJob.ExecutionInput.Inputs["playerId"] = "mutated"
againJob, err := reloaded.Jobs().GetByIdempotency("run-local", "idem-1")
if err != nil {
t.Fatalf("get reloaded job again: %v", err)
}
if againJob.ExecutionInput.Inputs["playerId"] != "steam-123" {
t.Fatalf("expected reloaded job inputs to be isolated, got %+v", againJob.ExecutionInput.Inputs)
}
gotPlugin, err := reloaded.GamePlugins().Get(plugin.ID)
if err != nil || len(gotPlugin.RuntimeProfiles.LifecycleProfiles) != 1 || gotPlugin.RuntimeProfiles.LifecycleProfiles[0].Key != "local" {
t.Fatalf("unexpected reloaded runtime profiles: plugin=%+v err=%v", gotPlugin, err)
}
gotBindings, err := reloaded.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: "server-1"})
if err != nil || len(gotBindings) != 1 || gotBindings[0].Bindings["rcon.password"] != "secret://server-1/rcon" {
t.Fatalf("unexpected reloaded runtime binding: bindings=%+v err=%v", gotBindings, err)
}
gotEndpoint, err := reloaded.RunEndpoints().Get(runEndpoint.ID)
if err != nil || gotEndpoint.Platform != "linux" || gotEndpoint.Architecture != "amd64" || gotEndpoint.Version != "release-1" {
t.Fatalf("unexpected reloaded Run target: endpoint=%+v err=%v", gotEndpoint, err)
}
gotDependencies, err := reloaded.DependencyStatuses().List(domain.DependencyStatusFilter{ServerInstanceID: "server-1"})
if err != nil || len(gotDependencies) != 1 || gotDependencies[0].PlanDigest != planDigest || gotDependencies[0].Evidence != "OpenJDK 21" {
t.Fatalf("unexpected reloaded dependency status: statuses=%+v err=%v", gotDependencies, err)
}
gotUpdates, err := reloaded.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: "server-1"})
if err != nil || len(gotUpdates) != 1 || gotUpdates[0].Phase != domain.RunUpdatePhaseRolledBack || !gotUpdates[0].Rollback || gotUpdates[0].TargetRelease != "release-2" {
t.Fatalf("unexpected reloaded Run update: updates=%+v err=%v", gotUpdates, err)
}
gotAuth, err := reloaded.AuthSessions().Get(authSession.ID)
if err != nil || gotAuth.TokenHash != authSession.TokenHash || gotAuth.Generation != 1 {
t.Fatalf("unexpected reloaded auth session: session=%+v err=%v", gotAuth, err)
}
gotRun, err := reloaded.RunControlSessions().Get(runSession.RunEndpointID)
if err != nil || gotRun.SessionToken != "" || gotRun.SessionTokenHash != runSession.SessionTokenHash || len(gotRun.UsedNonces) != 1 {
t.Fatalf("unexpected reloaded Run session: session=%+v err=%v", gotRun, err)
}
gotKey, err := reloaded.EncryptedComponentKeys().Get(componentKey.ID)
if err != nil || gotKey.EncryptedKey != componentKey.EncryptedKey || gotKey.SecretRef != componentKey.SecretRef {
t.Fatalf("unexpected reloaded component key metadata: key=%+v err=%v", gotKey, err)
}
}
func TestFileStoreNormalizesPluginVersionsAndPrunesLegacySCUMSnapshotData(t *testing.T) {
path := filepath.Join(t.TempDir(), "metadata.json")
stalePlugin := domain.GamePlugin{ID: "game.scum.codex.20260804095301", Name: "SCUM Server", Version: "0.1.4", ServerType: "scum", ServerDisplayName: "SCUM Dedicated Server", ManifestRef: "plugins/examples/scum-server-plugin/game.scum.codex.20260804095301/manifest.json", Status: domain.GamePluginStatusInstalled}
latestPlugin := domain.GamePlugin{ID: "game.scum", Name: "SCUM Server", Version: "0.1.15", ServerType: "scum", ServerDisplayName: "SCUM Dedicated Server", ManifestRef: "artifact://manifests/game.scum/0.1.15", Status: domain.GamePluginStatusInstalled}
stamp := time.Date(2026, 9, 14, 9, 0, 0, 0, time.UTC)
snapshot := StoreSnapshot{
GamePlugins: []domain.GamePlugin{stalePlugin, latestPlugin},
ServerInstances: []domain.ServerInstance{{ID: "server-scum", PluginID: stalePlugin.ID, PluginVersion: stalePlugin.Version, Name: "SCUM", State: domain.ServerInstanceStateDraft, CreatedAt: stamp, UpdatedAt: stamp}},
RuntimeBindings: []domain.RuntimeBinding{{ID: "runtime-binding-server-scum", ServerInstanceID: "server-scum", PluginID: stalePlugin.ID, PluginVersion: stalePlugin.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: stamp, UpdatedAt: stamp}},
Jobs: []domain.Job{
{ID: "job-scum-sqlite-query", ServerInstanceID: "server-scum", RunEndpointID: "run-local", Capability: domain.JobCapabilityRemoteRunDBSQLiteQuery, IdempotencyKey: "legacy-query", State: domain.JobStateSucceeded, CreatedAt: stamp, UpdatedAt: stamp},
{ID: "job-scum-start", ServerInstanceID: "server-scum", RunEndpointID: "run-local", Capability: domain.JobCapabilityRemoteRunProgram, IdempotencyKey: "start", State: domain.JobStateSucceeded, CreatedAt: stamp, UpdatedAt: stamp},
},
PluginDataRecords: []domain.PluginDataRecord{
{ID: snapshotPluginDataID("server-scum", stalePlugin.ID, "scum_gifts", "starter"), PluginID: stalePlugin.ID, ServerInstanceID: "server-scum", Collection: "scum_gifts", Key: "starter", Value: map[string]any{"name": "Starter"}, CreatedAt: stamp, UpdatedAt: stamp},
{ID: snapshotPluginDataID("server-scum", stalePlugin.ID, "scum_trajectories", "point-1"), PluginID: stalePlugin.ID, ServerInstanceID: "server-scum", Collection: "scum_trajectories", Key: "point-1", Value: map[string]any{"source": "legacy"}, CreatedAt: stamp, UpdatedAt: stamp},
},
}
payload, err := json.Marshal(snapshot)
if err != nil {
t.Fatalf("marshal stale snapshot: %v", err)
}
if err := os.WriteFile(path, payload, 0o600); err != nil {
t.Fatalf("write stale snapshot: %v", err)
}
store, err := NewFileStore(path)
if err != nil {
t.Fatalf("reload stale snapshot: %v", err)
}
plugins, err := store.GamePlugins().List(domain.GamePluginFilter{ServerType: "scum"})
if err != nil || len(plugins) != 1 || plugins[0].ID != latestPlugin.ID || plugins[0].Version != latestPlugin.Version {
t.Fatalf("expected only latest plugin, plugins=%+v err=%v", plugins, err)
}
server, err := store.ServerInstances().Get("server-scum")
if err != nil || server.PluginID != latestPlugin.ID || server.PluginVersion != latestPlugin.Version {
t.Fatalf("expected server reference migration, server=%+v err=%v", server, err)
}
binding, err := store.RuntimeBindings().Get("runtime-binding-server-scum")
if err != nil || binding.PluginID != latestPlugin.ID || binding.PluginVersion != latestPlugin.Version {
t.Fatalf("expected runtime binding migration, binding=%+v err=%v", binding, err)
}
config, err := store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: latestPlugin.ID, ServerInstanceID: "server-scum", Collection: "scum_gifts"})
if err != nil || len(config) != 1 || config[0].Key != "starter" {
t.Fatalf("expected plugin-owned config data to migrate, data=%+v err=%v", config, err)
}
legacy, err := store.PluginDataRecords().List(domain.PluginDataFilter{ServerInstanceID: "server-scum", Collection: "scum_trajectories"})
if err != nil || len(legacy) != 0 {
t.Fatalf("expected legacy SCUM projection data to be pruned, data=%+v err=%v", legacy, err)
}
jobs, err := store.Jobs().List(domain.JobFilter{ServerInstanceID: "server-scum"})
if err != nil || len(jobs) != 2 {
t.Fatalf("expected plugin-declared SCUM sqlite query job to survive the snapshot, jobs=%+v err=%v", jobs, err)
}
}
func TestMySQLSnapshotRoundTripsDurableJobSchedulingMetadata(t *testing.T) {
stamp := time.Date(2026, 7, 18, 12, 0, 0, 0, time.UTC)
source := &MySQLStore{MemoryStore: NewMemoryStore()}
job := domain.Job{
ID: "job-mysql", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-mysql",
State: domain.JobStateRunning, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 2, MaxBackoffSeconds: 60},
Attempt: 2, QueueEligibleAt: stamp, LeaseTokenHash: strings.Repeat("e", 64), LeaseSessionGen: 4,
LeaseExpiresAt: stamp.Add(time.Minute), LastProgressSeq: 8, CancelReason: "stop", CancelRequestedAt: stamp,
LastReconciledAt: stamp, ReconcileCount: 3, ReconcileOutcome: "confirmed active attempt", CreatedAt: stamp, UpdatedAt: stamp,
ExecutionInput: domain.JobExecutionInput{
WorkspaceScope: "local",
Content: "mysql-approved",
ExpectedVersion: 1,
ExpectedChecksum: "sha256:" + strings.Repeat("3", 64),
MaxReadBytes: 64 * 1024,
Inputs: map[string]string{"templateKey": "players.by-id", "playerId": "steam-456"},
Deployment: &domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ProfileKey: "local", CreateInputs: map[string]string{"gamePort": "27000"}, ServerRoot: "D:\\scumserver", Revision: 3},
ServerDeploymentPlan: &domain.ServerDeploymentPlan{
SchemaVersion: "1",
Operation: "install",
PluginID: "game.runtime",
},
},
ExecutionResult: domain.JobExecutionResult{Kind: "file.write", Version: 2, Checksum: "sha256:" + strings.Repeat("4", 64), SizeBytes: 14, Summary: "atomic write"},
}
if err := source.MemoryStore.Jobs().Create(job); err != nil {
t.Fatalf("create source job: %v", err)
}
payload, err := json.Marshal(source.snapshot())
if err != nil {
t.Fatalf("marshal mysql snapshot: %v", err)
}
var snapshot StoreSnapshot
if err := json.Unmarshal(payload, &snapshot); err != nil {
t.Fatalf("unmarshal mysql snapshot: %v", err)
}
target := &MySQLStore{MemoryStore: NewMemoryStore()}
target.loadSnapshot(snapshot)
got, err := target.MemoryStore.Jobs().Get(job.ID)
if err != nil || got.Attempt != job.Attempt || got.LeaseTokenHash != job.LeaseTokenHash || got.LastProgressSeq != job.LastProgressSeq || got.ReconcileCount != job.ReconcileCount || got.ExecutionInput.Content != job.ExecutionInput.Content || got.ExecutionInput.Inputs["templateKey"] != "players.by-id" || got.ExecutionInput.Inputs["playerId"] != "steam-456" || got.ExecutionInput.Deployment == nil || got.ExecutionInput.Deployment.CreateInputs["gamePort"] != "27000" || got.ExecutionInput.ServerDeploymentPlan == nil || got.ExecutionInput.ServerDeploymentPlan.PluginID != "game.runtime" || got.ExecutionResult.Checksum != job.ExecutionResult.Checksum {
t.Fatalf("unexpected MySQL snapshot job: job=%+v err=%v", got, err)
}
got.ExecutionInput.Inputs["playerId"] = "mutated"
again, err := target.MemoryStore.Jobs().Get(job.ID)
if err != nil {
t.Fatalf("get MySQL snapshot job again: %v", err)
}
if again.ExecutionInput.Inputs["playerId"] != "steam-456" {
t.Fatalf("expected MySQL snapshot job inputs to be isolated, got %+v", again.ExecutionInput.Inputs)
}
}
func TestFileStorePersistsRunRuntimeStateSeparately(t *testing.T) {
path := filepath.Join(t.TempDir(), "metadata.json")
store, err := NewFileStore(path)
if err != nil {
t.Fatalf("create file store: %v", err)
}
stamp := time.Date(2026, 7, 18, 10, 0, 0, 0, time.UTC)
if err := store.PluginDataRecords().Create(domain.PluginDataRecord{ID: "plugin-record-1", PluginID: "server.scum", ServerInstanceID: "server-1", Collection: "players", Key: "steam-1", Value: map[string]any{"displayName": "Ada"}, CreatedAt: stamp, UpdatedAt: stamp}); err != nil {
t.Fatalf("create plugin data: %v", err)
}
mainBefore, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read main snapshot before runtime update: %v", err)
}
endpoint := domain.RunEndpoint{ID: "run-runtime", DisplayName: "Runtime Run", Version: "1.0.0", Platform: "linux", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capacity: domain.RunCapacity{MaxJobs: 4}, LastHeartbeatAt: stamp}
if err := store.RunEndpoints().Create(endpoint); err != nil {
t.Fatalf("create runtime endpoint: %v", err)
}
session := domain.RunControlSession{RunEndpointID: endpoint.ID, SessionToken: "raw-runtime-token", SessionTokenHash: strings.Repeat("a", 64), Status: domain.AuthSessionStatusActive, Generation: 1, CapabilityFingerprint: "cap-runtime", HeartbeatIntervalSeconds: 15, CreatedAt: stamp, UpdatedAt: stamp, ExpiresAt: stamp.Add(time.Hour), RequireSignedRequests: true, UsedNonces: []string{"nonce-1"}}
if err := store.RunControlSessions().Create(session); err != nil {
t.Fatalf("create runtime session: %v", err)
}
stream := domain.LogStream{ID: "run.run-runtime.server-1.scum.console.stdout", ServerInstanceID: "server-1", Source: domain.LogStreamSourceProcess, StreamKey: "scum.console.stdout", LatestSeq: 42, StorageBackend: domain.LogStorageBackendLocalSegments, RetentionPolicy: "default", CreatedAt: stamp, UpdatedAt: stamp}
if err := store.LogStreams().Create(stream); err != nil {
t.Fatalf("create runtime log stream: %v", err)
}
mainAfter, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read main snapshot after runtime update: %v", err)
}
if !bytes.Equal(mainBefore, mainAfter) {
t.Fatalf("runtime updates rewrote the main metadata snapshot")
}
runtimePayload, err := os.ReadFile(runtimeMetadataPath(path))
if err != nil {
t.Fatalf("read runtime snapshot: %v", err)
}
if !strings.Contains(string(runtimePayload), endpoint.ID) || !strings.Contains(string(runtimePayload), stream.ID) || strings.Contains(string(runtimePayload), session.SessionToken) {
t.Fatalf("unexpected runtime snapshot payload: %s", runtimePayload)
}
reloaded, err := NewFileStore(path)
if err != nil {
t.Fatalf("reload file store: %v", err)
}
if got, err := reloaded.RunEndpoints().Get(endpoint.ID); err != nil || got.Version != endpoint.Version {
t.Fatalf("runtime endpoint did not reload: %+v err=%v", got, err)
}
if got, err := reloaded.RunControlSessions().Get(endpoint.ID); err != nil || got.SessionTokenHash != session.SessionTokenHash || got.UsedNonces[0] != "nonce-1" {
t.Fatalf("runtime session did not reload: %+v err=%v", got, err)
}
if got, err := reloaded.LogStreams().Get(stream.ID); err != nil || got.LatestSeq != stream.LatestSeq {
t.Fatalf("runtime log stream did not reload: %+v err=%v", got, err)
}
}
func TestFileStorePersistsPluginOperationsStateAcrossRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "plugin-operations.json")
store, err := NewFileStore(path)
if err != nil {
t.Fatalf("create file store: %v", err)
}
stamp := time.Date(2026, 7, 18, 14, 0, 0, 0, time.UTC)
installation := domain.PluginLifecycleInstallation{
ID: "plugin-lifecycle-1", PluginID: "game.scum", ServerInstanceID: "server-1",
CurrentVersion: "1.0.0", TargetVersion: "2.0.0", PreviousVersion: "0.9.0",
DesiredState: domain.PluginLifecycleStateEnabled, CurrentState: domain.PluginLifecycleStateUpgrading,
LastOperation: domain.PluginLifecycleOperationUpgrade, Compatibility: "compatible",
DependencyState: domain.DependencyStatePresent, JobID: "job-upgrade",
IdempotencyKey: "upgrade-once", CreatedAt: stamp.Add(-time.Hour), UpdatedAt: stamp,
}
if err := store.PluginLifecycles().Create(installation); err != nil {
t.Fatalf("create plugin lifecycle: %v", err)
}
restarted, err := NewFileStore(path)
if err != nil {
t.Fatalf("restart file store: %v", err)
}
gotInstallation, lifecycleErr := restarted.PluginLifecycles().Get(installation.ID)
if lifecycleErr != nil || gotInstallation.CurrentState != installation.CurrentState || gotInstallation.TargetVersion != installation.TargetVersion || gotInstallation.JobID != installation.JobID {
t.Fatalf("unexpected durable plugin lifecycle: installation=%+v err=%v", gotInstallation, lifecycleErr)
}
}
func TestMySQLStoreRequiresDSN(t *testing.T) {
_, err := NewMySQLStore("")
if err == nil || !strings.Contains(err.Error(), "PLATFORM_MYSQL_DSN") {
t.Fatalf("expected missing MySQL DSN error, got %v", err)
}
}