feat: 完整游戏运维功能

This commit is contained in:
npc0-hue
2026-07-18 09:04:01 +08:00
parent f3b14b7945
commit 48b8ad8d6c
187 changed files with 16607 additions and 1140 deletions
@@ -0,0 +1,58 @@
package repo
import (
"path/filepath"
"testing"
"time"
"browser.local/platform/domain"
)
func TestClientManagerLifecycleRepositoriesPersistAndFilter(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, 3, 0, 0, 0, time.UTC)
installation := domain.ClientManagerInstallation{ID: "cm-install-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client-manager", RunEndpointID: "run-1", TargetOS: "linux", TargetArch: "amd64", Status: domain.ClientManagerLifecycleOnline, Phase: "healthy", ActiveVersion: "1.0.0", ActiveArtifactID: "artifact-1", KeyGeneration: 2, DeploymentGeneration: 3, Health: domain.ClientManagerHealthHealthy, LastSeenAt: stamp, CreatedAt: stamp, UpdatedAt: stamp}
if err := store.ClientManagerInstallations().Create(installation); err != nil {
t.Fatalf("persist installation: %v", err)
}
session := domain.ClientManagerSession{ID: "cm-session-1", InstallationID: installation.ID, ServerInstanceID: installation.ServerInstanceID, ProfileKey: installation.ProfileKey, RunEndpointID: installation.RunEndpointID, ArtifactID: installation.ActiveArtifactID, KeyGeneration: 2, DeploymentGeneration: 3, TokenHash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Capabilities: []string{"component.register", "component.heartbeat"}, Status: domain.ClientManagerSessionActive, LastSeenAt: stamp, ExpiresAt: stamp.Add(15 * time.Minute), CreatedAt: stamp, UpdatedAt: stamp}
if err := store.ClientManagerSessions().Create(session); err != nil {
t.Fatalf("persist session: %v", err)
}
nonce := domain.ClientManagerRegistrationNonce{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", InstallationID: installation.ID, CreatedAt: stamp, ExpiresAt: stamp.Add(5 * time.Minute)}
if err := store.ClientManagerNonces().Create(nonce); err != nil {
t.Fatalf("persist nonce: %v", err)
}
restarted, err := NewFileStore(path)
if err != nil {
t.Fatalf("reload file store: %v", err)
}
installations, err := restarted.ClientManagerInstallations().List(domain.ClientManagerInstallationFilter{ServerInstanceID: "server-1", ProfileKey: "scum-client-manager", Status: domain.ClientManagerLifecycleOnline})
if err != nil || len(installations) != 1 || installations[0].DeploymentGeneration != 3 {
t.Fatalf("unexpected persisted installations: items=%+v err=%v", installations, err)
}
sessions, err := restarted.ClientManagerSessions().List(domain.ClientManagerSessionFilter{InstallationID: installation.ID, Status: domain.ClientManagerSessionActive})
if err != nil || len(sessions) != 1 {
t.Fatalf("unexpected persisted sessions: items=%+v err=%v", sessions, err)
}
sessions[0].Capabilities[0] = "mutated"
stored, _ := restarted.ClientManagerSessions().Get(session.ID)
if stored.Capabilities[0] != "component.register" {
t.Fatalf("repository returned shared capability storage: %+v", stored)
}
expired, err := restarted.ClientManagerNonces().List(domain.ClientManagerNonceFilter{ExpiresBefore: stamp.Add(6 * time.Minute)})
if err != nil || len(expired) != 1 {
t.Fatalf("unexpected nonce expiry filter: items=%+v err=%v", expired, err)
}
if err := restarted.ClientManagerNonces().Delete(nonce.ID); err != nil {
t.Fatalf("delete expired nonce: %v", err)
}
if _, err := restarted.ClientManagerNonces().Get(nonce.ID); err != ErrNotFound {
t.Fatalf("expected deleted nonce, got %v", err)
}
}
+124 -18
View File
@@ -13,15 +13,29 @@ import (
)
type StoreSnapshot struct {
Users []domain.User `json:"users"`
AIProviders []domain.AIProvider `json:"aiProviders"`
GamePlugins []domain.GamePlugin `json:"gamePlugins"`
ServerInstances []domain.ServerInstance `json:"serverInstances"`
RunEndpoints []domain.RunEndpoint `json:"runEndpoints"`
Jobs []domain.Job `json:"jobs"`
Artifacts []domain.Artifact `json:"artifacts"`
LogStreams []domain.LogStream `json:"logStreams"`
AuditEvents []domain.AuditEvent `json:"auditEvents"`
Users []domain.User `json:"users"`
AuthSessions []domain.AuthSessionRecord `json:"authSessions"`
RunControlSessions []domain.RunControlSession `json:"runControlSessions"`
AIProviders []domain.AIProvider `json:"aiProviders"`
GamePlugins []domain.GamePlugin `json:"gamePlugins"`
ServerInstances []domain.ServerInstance `json:"serverInstances"`
RunEndpoints []domain.RunEndpoint `json:"runEndpoints"`
Jobs []domain.Job `json:"jobs"`
Artifacts []domain.Artifact `json:"artifacts"`
RuntimeBindings []domain.RuntimeBinding `json:"runtimeBindings"`
EncryptedComponentKeys []domain.EncryptedComponentKey `json:"encryptedComponentKeys"`
RunDistributions []domain.RunDistribution `json:"runDistributions"`
ClientManagerDistributions []domain.ClientManagerDistribution `json:"clientManagerDistributions"`
ClientManagerInstallations []domain.ClientManagerInstallation `json:"clientManagerInstallations"`
ClientManagerSessions []domain.ClientManagerSession `json:"clientManagerSessions"`
ClientManagerNonces []domain.ClientManagerRegistrationNonce `json:"clientManagerNonces"`
DependencyStatuses []domain.DependencyStatus `json:"dependencyStatuses"`
ClientManagerBuildJobs []domain.ClientManagerBuildJob `json:"clientManagerBuildJobs"`
RunUpdateJobs []domain.RunUpdateJob `json:"runUpdateJobs"`
LogStreams []domain.LogStream `json:"logStreams"`
AuditEvents []domain.AuditEvent `json:"auditEvents"`
MetricSamples []domain.MetricSample `json:"metricSamples"`
Backups []domain.BackupRecord `json:"backups"`
}
type FileStore struct {
@@ -53,6 +67,14 @@ func (store *FileStore) Users() UserRepository {
return &persistentRepository[domain.User, domain.UserFilter]{repository: store.MemoryStore.users, persist: store.persist}
}
func (store *FileStore) AuthSessions() AuthSessionRepository {
return &persistentRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]{repository: store.MemoryStore.authSessions, persist: store.persist}
}
func (store *FileStore) RunControlSessions() RunControlSessionRepository {
return &persistentRepository[domain.RunControlSession, struct{}]{repository: store.MemoryStore.runSessions, persist: store.persist}
}
func (store *FileStore) AIProviders() AIProviderRepository {
return &persistentRepository[domain.AIProvider, domain.AIProviderFilter]{repository: store.MemoryStore.aiProviders, persist: store.persist}
}
@@ -80,6 +102,46 @@ func (store *FileStore) Artifacts() ArtifactRepository {
return &persistentRepository[domain.Artifact, domain.ArtifactFilter]{repository: store.MemoryStore.artifacts, persist: store.persist}
}
func (store *FileStore) RuntimeBindings() RuntimeBindingRepository {
return &persistentRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]{repository: store.MemoryStore.runtimeBindings, persist: store.persist}
}
func (store *FileStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
return &persistentRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]{repository: store.MemoryStore.componentKeys, persist: store.persist}
}
func (store *FileStore) RunDistributions() RunDistributionRepository {
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
}
func (store *FileStore) ClientManagerDistributions() ClientManagerDistributionRepository {
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
}
func (store *FileStore) ClientManagerInstallations() ClientManagerInstallationRepository {
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
}
func (store *FileStore) ClientManagerSessions() ClientManagerSessionRepository {
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
}
func (store *FileStore) ClientManagerNonces() ClientManagerNonceRepository {
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
}
func (store *FileStore) DependencyStatuses() DependencyStatusRepository {
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
}
func (store *FileStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
}
func (store *FileStore) RunUpdateJobs() RunUpdateJobRepository {
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
}
func (store *FileStore) LogStreams() LogStreamRepository {
return &persistentRepository[domain.LogStream, domain.LogStreamFilter]{repository: store.MemoryStore.logStreams, persist: store.persist}
}
@@ -88,6 +150,14 @@ func (store *FileStore) AuditEvents() AuditEventRepository {
return &persistentRepository[domain.AuditEvent, domain.AuditEventFilter]{repository: store.MemoryStore.auditEvents, persist: store.persist}
}
func (store *FileStore) MetricSamples() MetricSampleRepository {
return &persistentRepository[domain.MetricSample, domain.MetricSampleFilter]{repository: store.MemoryStore.metricSamples, persist: store.persist}
}
func (store *FileStore) Backups() BackupRepository {
return &persistentRepository[domain.BackupRecord, domain.BackupFilter]{repository: store.MemoryStore.backups, persist: store.persist}
}
func (store *FileStore) load() error {
data, err := os.ReadFile(store.path)
if err != nil {
@@ -131,28 +201,56 @@ func (store *FileStore) persist() error {
func (store *FileStore) snapshot() StoreSnapshot {
return StoreSnapshot{
Users: snapshotRepository(store.MemoryStore.users),
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
Users: snapshotRepository(store.MemoryStore.users),
AuthSessions: snapshotRepository(store.MemoryStore.authSessions),
RunControlSessions: snapshotRepository(store.MemoryStore.runSessions),
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
Backups: snapshotRepository(store.MemoryStore.backups),
}
}
func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
loadRepository(store.MemoryStore.users, snapshot.Users)
loadRepository(store.MemoryStore.authSessions, snapshot.AuthSessions)
loadRepository(store.MemoryStore.runSessions, snapshot.RunControlSessions)
loadRepository(store.MemoryStore.aiProviders, snapshot.AIProviders)
loadRepository(store.MemoryStore.gamePlugins, snapshot.GamePlugins)
loadRepository(store.MemoryStore.serverInstances, snapshot.ServerInstances)
loadRepository(store.MemoryStore.runEndpoints, snapshot.RunEndpoints)
loadRepository(store.MemoryStore.jobs.memoryRepository, snapshot.Jobs)
loadRepository(store.MemoryStore.artifacts, snapshot.Artifacts)
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
loadRepository(store.MemoryStore.auditEvents, snapshot.AuditEvents)
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
loadRepository(store.MemoryStore.backups, snapshot.Backups)
}
type mutableRepository[T any, F any] interface {
@@ -160,6 +258,7 @@ type mutableRepository[T any, F any] interface {
Get(string) (T, error)
List(F) ([]T, error)
Update(T) error
Delete(string) error
}
type persistentRepository[T any, F any] struct {
@@ -189,6 +288,13 @@ func (repository *persistentRepository[T, F]) Update(value T) error {
return repository.persist()
}
func (repository *persistentRepository[T, F]) Delete(id string) error {
if err := repository.repository.Delete(id); err != nil {
return err
}
return repository.persist()
}
type persistentJobRepository struct {
*persistentRepository[domain.Job, domain.JobFilter]
repository JobRepository
+93 -9
View File
@@ -54,6 +54,14 @@ func (store *MySQLStore) Users() UserRepository {
return &persistentRepository[domain.User, domain.UserFilter]{repository: store.MemoryStore.users, persist: store.persist}
}
func (store *MySQLStore) AuthSessions() AuthSessionRepository {
return &persistentRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]{repository: store.MemoryStore.authSessions, persist: store.persist}
}
func (store *MySQLStore) RunControlSessions() RunControlSessionRepository {
return &persistentRepository[domain.RunControlSession, struct{}]{repository: store.MemoryStore.runSessions, persist: store.persist}
}
func (store *MySQLStore) AIProviders() AIProviderRepository {
return &persistentRepository[domain.AIProvider, domain.AIProviderFilter]{repository: store.MemoryStore.aiProviders, persist: store.persist}
}
@@ -81,6 +89,46 @@ func (store *MySQLStore) Artifacts() ArtifactRepository {
return &persistentRepository[domain.Artifact, domain.ArtifactFilter]{repository: store.MemoryStore.artifacts, persist: store.persist}
}
func (store *MySQLStore) RuntimeBindings() RuntimeBindingRepository {
return &persistentRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]{repository: store.MemoryStore.runtimeBindings, persist: store.persist}
}
func (store *MySQLStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
return &persistentRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]{repository: store.MemoryStore.componentKeys, persist: store.persist}
}
func (store *MySQLStore) RunDistributions() RunDistributionRepository {
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
}
func (store *MySQLStore) ClientManagerDistributions() ClientManagerDistributionRepository {
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
}
func (store *MySQLStore) ClientManagerInstallations() ClientManagerInstallationRepository {
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
}
func (store *MySQLStore) ClientManagerSessions() ClientManagerSessionRepository {
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
}
func (store *MySQLStore) ClientManagerNonces() ClientManagerNonceRepository {
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
}
func (store *MySQLStore) DependencyStatuses() DependencyStatusRepository {
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
}
func (store *MySQLStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
}
func (store *MySQLStore) RunUpdateJobs() RunUpdateJobRepository {
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
}
func (store *MySQLStore) LogStreams() LogStreamRepository {
return &persistentRepository[domain.LogStream, domain.LogStreamFilter]{repository: store.MemoryStore.logStreams, persist: store.persist}
}
@@ -89,6 +137,14 @@ func (store *MySQLStore) AuditEvents() AuditEventRepository {
return &persistentRepository[domain.AuditEvent, domain.AuditEventFilter]{repository: store.MemoryStore.auditEvents, persist: store.persist}
}
func (store *MySQLStore) MetricSamples() MetricSampleRepository {
return &persistentRepository[domain.MetricSample, domain.MetricSampleFilter]{repository: store.MemoryStore.metricSamples, persist: store.persist}
}
func (store *MySQLStore) Backups() BackupRepository {
return &persistentRepository[domain.BackupRecord, domain.BackupFilter]{repository: store.MemoryStore.backups, persist: store.persist}
}
func (store *MySQLStore) initialize() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -149,26 +205,54 @@ ON DUPLICATE KEY UPDATE snapshot_json = ?, updated_at = CURRENT_TIMESTAMP`, mysq
func (store *MySQLStore) snapshot() StoreSnapshot {
return StoreSnapshot{
Users: snapshotRepository(store.MemoryStore.users),
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
Users: snapshotRepository(store.MemoryStore.users),
AuthSessions: snapshotRepository(store.MemoryStore.authSessions),
RunControlSessions: snapshotRepository(store.MemoryStore.runSessions),
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
Backups: snapshotRepository(store.MemoryStore.backups),
}
}
func (store *MySQLStore) loadSnapshot(snapshot StoreSnapshot) {
loadRepository(store.MemoryStore.users, snapshot.Users)
loadRepository(store.MemoryStore.authSessions, snapshot.AuthSessions)
loadRepository(store.MemoryStore.runSessions, snapshot.RunControlSessions)
loadRepository(store.MemoryStore.aiProviders, snapshot.AIProviders)
loadRepository(store.MemoryStore.gamePlugins, snapshot.GamePlugins)
loadRepository(store.MemoryStore.serverInstances, snapshot.ServerInstances)
loadRepository(store.MemoryStore.runEndpoints, snapshot.RunEndpoints)
loadRepository(store.MemoryStore.jobs.memoryRepository, snapshot.Jobs)
loadRepository(store.MemoryStore.artifacts, snapshot.Artifacts)
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
loadRepository(store.MemoryStore.auditEvents, snapshot.AuditEvents)
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
loadRepository(store.MemoryStore.backups, snapshot.Backups)
}
+168 -8
View File
@@ -20,6 +20,20 @@ type UserRepository interface {
Update(domain.User) error
}
type AuthSessionRepository interface {
Create(domain.AuthSessionRecord) error
Get(id string) (domain.AuthSessionRecord, error)
List(domain.AuthSessionFilter) ([]domain.AuthSessionRecord, error)
Update(domain.AuthSessionRecord) error
}
type RunControlSessionRepository interface {
Create(domain.RunControlSession) error
Get(id string) (domain.RunControlSession, error)
List(struct{}) ([]domain.RunControlSession, error)
Update(domain.RunControlSession) error
}
type AIProviderRepository interface {
Create(domain.AIProvider) error
Get(id string) (domain.AIProvider, error)
@@ -91,6 +105,28 @@ type ClientManagerDistributionRepository interface {
Update(domain.ClientManagerDistribution) error
}
type ClientManagerInstallationRepository interface {
Create(domain.ClientManagerInstallation) error
Get(id string) (domain.ClientManagerInstallation, error)
List(domain.ClientManagerInstallationFilter) ([]domain.ClientManagerInstallation, error)
Update(domain.ClientManagerInstallation) error
}
type ClientManagerSessionRepository interface {
Create(domain.ClientManagerSession) error
Get(id string) (domain.ClientManagerSession, error)
List(domain.ClientManagerSessionFilter) ([]domain.ClientManagerSession, error)
Update(domain.ClientManagerSession) error
}
type ClientManagerNonceRepository interface {
Create(domain.ClientManagerRegistrationNonce) error
Get(id string) (domain.ClientManagerRegistrationNonce, error)
List(domain.ClientManagerNonceFilter) ([]domain.ClientManagerRegistrationNonce, error)
Update(domain.ClientManagerRegistrationNonce) error
Delete(id string) error
}
type DependencyStatusRepository interface {
Create(domain.DependencyStatus) error
Get(id string) (domain.DependencyStatus, error)
@@ -126,8 +162,26 @@ type AuditEventRepository interface {
Update(domain.AuditEvent) error
}
type MetricSampleRepository interface {
Create(domain.MetricSample) error
Get(id string) (domain.MetricSample, error)
List(domain.MetricSampleFilter) ([]domain.MetricSample, error)
Update(domain.MetricSample) error
Delete(id string) error
}
type BackupRepository interface {
Create(domain.BackupRecord) error
Get(id string) (domain.BackupRecord, error)
List(domain.BackupFilter) ([]domain.BackupRecord, error)
Update(domain.BackupRecord) error
Delete(id string) error
}
type Store interface {
Users() UserRepository
AuthSessions() AuthSessionRepository
RunControlSessions() RunControlSessionRepository
AIProviders() AIProviderRepository
GamePlugins() GamePluginRepository
ServerInstances() ServerInstanceRepository
@@ -138,15 +192,22 @@ type Store interface {
EncryptedComponentKeys() EncryptedComponentKeyRepository
RunDistributions() RunDistributionRepository
ClientManagerDistributions() ClientManagerDistributionRepository
ClientManagerInstallations() ClientManagerInstallationRepository
ClientManagerSessions() ClientManagerSessionRepository
ClientManagerNonces() ClientManagerNonceRepository
DependencyStatuses() DependencyStatusRepository
ClientManagerBuildJobs() ClientManagerBuildJobRepository
RunUpdateJobs() RunUpdateJobRepository
LogStreams() LogStreamRepository
AuditEvents() AuditEventRepository
MetricSamples() MetricSampleRepository
Backups() BackupRepository
}
type MemoryStore struct {
users *memoryRepository[domain.User, domain.UserFilter]
authSessions *memoryRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]
runSessions *memoryRepository[domain.RunControlSession, struct{}]
aiProviders *memoryRepository[domain.AIProvider, domain.AIProviderFilter]
gamePlugins *memoryRepository[domain.GamePlugin, domain.GamePluginFilter]
serverInstances *memoryRepository[domain.ServerInstance, domain.ServerInstanceFilter]
@@ -157,11 +218,16 @@ type MemoryStore struct {
componentKeys *memoryRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]
runDists *memoryRepository[domain.RunDistribution, domain.RunDistributionFilter]
clientDists *memoryRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]
clientInstalls *memoryRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]
clientSessions *memoryRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]
clientNonces *memoryRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]
dependencies *memoryRepository[domain.DependencyStatus, domain.DependencyStatusFilter]
buildJobs *memoryRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]
updateJobs *memoryRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]
logStreams *memoryRepository[domain.LogStream, domain.LogStreamFilter]
auditEvents *memoryRepository[domain.AuditEvent, domain.AuditEventFilter]
metricSamples *memoryRepository[domain.MetricSample, domain.MetricSampleFilter]
backups *memoryRepository[domain.BackupRecord, domain.BackupFilter]
}
func NewMemoryStore() *MemoryStore {
@@ -171,6 +237,16 @@ func NewMemoryStore() *MemoryStore {
domain.CopyUser,
matchUser,
),
authSessions: newMemoryRepository(
func(session domain.AuthSessionRecord) string { return session.ID },
domain.CopyAuthSessionRecord,
matchAuthSession,
),
runSessions: newMemoryRepository(
func(session domain.RunControlSession) string { return session.RunEndpointID },
domain.CopyRunControlSession,
func(domain.RunControlSession, struct{}) bool { return true },
),
aiProviders: newMemoryRepository(
func(provider domain.AIProvider) string { return provider.ID },
domain.CopyAIProvider,
@@ -217,6 +293,21 @@ func NewMemoryStore() *MemoryStore {
domain.CopyClientManagerDistribution,
matchClientManagerDistribution,
),
clientInstalls: newMemoryRepository(
func(installation domain.ClientManagerInstallation) string { return installation.ID },
domain.CopyClientManagerInstallation,
matchClientManagerInstallation,
),
clientSessions: newMemoryRepository(
func(session domain.ClientManagerSession) string { return session.ID },
domain.CopyClientManagerSession,
matchClientManagerSession,
),
clientNonces: newMemoryRepository(
func(nonce domain.ClientManagerRegistrationNonce) string { return nonce.ID },
domain.CopyClientManagerRegistrationNonce,
matchClientManagerNonce,
),
dependencies: newMemoryRepository(
func(status domain.DependencyStatus) string { return status.ID },
domain.CopyDependencyStatus,
@@ -242,17 +333,29 @@ func NewMemoryStore() *MemoryStore {
domain.CopyAuditEvent,
matchAuditEvent,
),
metricSamples: newMemoryRepository(
func(sample domain.MetricSample) string { return sample.ID },
domain.CopyMetricSample,
matchMetricSample,
),
backups: newMemoryRepository(
func(record domain.BackupRecord) string { return record.ID },
domain.CopyBackupRecord,
matchBackup,
),
}
}
func (store *MemoryStore) Users() UserRepository { return store.users }
func (store *MemoryStore) AIProviders() AIProviderRepository { return store.aiProviders }
func (store *MemoryStore) GamePlugins() GamePluginRepository { return store.gamePlugins }
func (store *MemoryStore) ServerInstances() ServerInstanceRepository { return store.serverInstances }
func (store *MemoryStore) RunEndpoints() RunEndpointRepository { return store.runEndpoints }
func (store *MemoryStore) Jobs() JobRepository { return store.jobs }
func (store *MemoryStore) Artifacts() ArtifactRepository { return store.artifacts }
func (store *MemoryStore) RuntimeBindings() RuntimeBindingRepository { return store.runtimeBindings }
func (store *MemoryStore) Users() UserRepository { return store.users }
func (store *MemoryStore) AuthSessions() AuthSessionRepository { return store.authSessions }
func (store *MemoryStore) RunControlSessions() RunControlSessionRepository { return store.runSessions }
func (store *MemoryStore) AIProviders() AIProviderRepository { return store.aiProviders }
func (store *MemoryStore) GamePlugins() GamePluginRepository { return store.gamePlugins }
func (store *MemoryStore) ServerInstances() ServerInstanceRepository { return store.serverInstances }
func (store *MemoryStore) RunEndpoints() RunEndpointRepository { return store.runEndpoints }
func (store *MemoryStore) Jobs() JobRepository { return store.jobs }
func (store *MemoryStore) Artifacts() ArtifactRepository { return store.artifacts }
func (store *MemoryStore) RuntimeBindings() RuntimeBindingRepository { return store.runtimeBindings }
func (store *MemoryStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
return store.componentKeys
}
@@ -260,6 +363,15 @@ func (store *MemoryStore) RunDistributions() RunDistributionRepository { return
func (store *MemoryStore) ClientManagerDistributions() ClientManagerDistributionRepository {
return store.clientDists
}
func (store *MemoryStore) ClientManagerInstallations() ClientManagerInstallationRepository {
return store.clientInstalls
}
func (store *MemoryStore) ClientManagerSessions() ClientManagerSessionRepository {
return store.clientSessions
}
func (store *MemoryStore) ClientManagerNonces() ClientManagerNonceRepository {
return store.clientNonces
}
func (store *MemoryStore) DependencyStatuses() DependencyStatusRepository { return store.dependencies }
func (store *MemoryStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
return store.buildJobs
@@ -267,6 +379,8 @@ func (store *MemoryStore) ClientManagerBuildJobs() ClientManagerBuildJobReposito
func (store *MemoryStore) RunUpdateJobs() RunUpdateJobRepository { return store.updateJobs }
func (store *MemoryStore) LogStreams() LogStreamRepository { return store.logStreams }
func (store *MemoryStore) AuditEvents() AuditEventRepository { return store.auditEvents }
func (store *MemoryStore) MetricSamples() MetricSampleRepository { return store.metricSamples }
func (store *MemoryStore) Backups() BackupRepository { return store.backups }
type memoryRepository[T any, F any] struct {
mu sync.RWMutex
@@ -341,6 +455,16 @@ func (repository *memoryRepository[T, F]) Update(value T) error {
return nil
}
func (repository *memoryRepository[T, F]) Delete(id string) error {
repository.mu.Lock()
defer repository.mu.Unlock()
if _, exists := repository.byID[id]; !exists {
return ErrNotFound
}
delete(repository.byID, id)
return nil
}
type memoryJobRepository struct {
*memoryRepository[domain.Job, domain.JobFilter]
}
@@ -371,6 +495,12 @@ func matchUser(user domain.User, filter domain.UserFilter) bool {
return filter.Status == "" || user.Status == filter.Status
}
func matchAuthSession(session domain.AuthSessionRecord, filter domain.AuthSessionFilter) bool {
return (filter.UserID == "" || session.UserID == filter.UserID) &&
(filter.TokenHash == "" || session.TokenHash == filter.TokenHash) &&
(filter.Status == "" || session.Status == filter.Status)
}
func matchAIProvider(provider domain.AIProvider, filter domain.AIProviderFilter) bool {
return (filter.Kind == "" || provider.Kind == filter.Kind) &&
(filter.Status == "" || provider.Status == filter.Status)
@@ -444,6 +574,25 @@ func matchClientManagerDistribution(distribution domain.ClientManagerDistributio
(filter.Status == "" || distribution.Status == filter.Status)
}
func matchClientManagerInstallation(installation domain.ClientManagerInstallation, filter domain.ClientManagerInstallationFilter) bool {
return (filter.ServerInstanceID == "" || installation.ServerInstanceID == filter.ServerInstanceID) &&
(filter.ProfileKey == "" || installation.ProfileKey == filter.ProfileKey) &&
(filter.RunEndpointID == "" || installation.RunEndpointID == filter.RunEndpointID) &&
(filter.Status == "" || installation.Status == filter.Status)
}
func matchClientManagerSession(session domain.ClientManagerSession, filter domain.ClientManagerSessionFilter) bool {
return (filter.InstallationID == "" || session.InstallationID == filter.InstallationID) &&
(filter.ServerInstanceID == "" || session.ServerInstanceID == filter.ServerInstanceID) &&
(filter.ProfileKey == "" || session.ProfileKey == filter.ProfileKey) &&
(filter.Status == "" || session.Status == filter.Status)
}
func matchClientManagerNonce(nonce domain.ClientManagerRegistrationNonce, filter domain.ClientManagerNonceFilter) bool {
return (filter.InstallationID == "" || nonce.InstallationID == filter.InstallationID) &&
(filter.ExpiresBefore.IsZero() || nonce.ExpiresAt.Before(filter.ExpiresBefore) || nonce.ExpiresAt.Equal(filter.ExpiresBefore))
}
func matchDependencyStatus(status domain.DependencyStatus, filter domain.DependencyStatusFilter) bool {
return (filter.ServerInstanceID == "" || status.ServerInstanceID == filter.ServerInstanceID) &&
(filter.ProbeKey == "" || status.ProbeKey == filter.ProbeKey) &&
@@ -472,3 +621,14 @@ func matchAuditEvent(event domain.AuditEvent, filter domain.AuditEventFilter) bo
(filter.ResourceID == "" || event.ResourceID == filter.ResourceID) &&
(filter.Result == "" || event.Result == filter.Result)
}
func matchMetricSample(sample domain.MetricSample, filter domain.MetricSampleFilter) bool {
return (filter.ServerInstanceID == "" || sample.ServerInstanceID == filter.ServerInstanceID) &&
(filter.After.IsZero() || sample.CollectedAt.After(filter.After)) &&
(filter.Before.IsZero() || !sample.CollectedAt.After(filter.Before))
}
func matchBackup(record domain.BackupRecord, filter domain.BackupFilter) bool {
return (filter.ServerInstanceID == "" || record.ServerInstanceID == filter.ServerInstanceID) &&
(filter.State == "" || record.State == filter.State)
}
+133 -8
View File
@@ -1,10 +1,13 @@
package repo
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
@@ -75,7 +78,6 @@ func TestMemoryJobRepositoryFindsIdempotencyKey(t *testing.T) {
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)
@@ -105,17 +107,77 @@ func TestFileStorePersistsAndReloadsResources(t *testing.T) {
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,
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},
ExecutionResult: domain.JobExecutionResult{Kind: "file.read", Version: 2, Checksum: "sha256:" + strings.Repeat("2", 64), SizeBytes: 15, AuditSummary: "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 {
@@ -132,9 +194,72 @@ func TestFileStorePersistsAndReloadsResources(t *testing.T) {
if err != nil {
t.Fatalf("get reloaded job by idempotency: %v", err)
}
if gotJob.ID != "job-1" || gotJob.ServerInstanceID != "server-1" {
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.ExecutionResult.Content != "private-read" {
t.Fatalf("unexpected reloaded job: %+v", gotJob)
}
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 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},
ExecutionResult: domain.JobExecutionResult{Kind: "file.write", Version: 2, Checksum: "sha256:" + strings.Repeat("4", 64), SizeBytes: 14, AuditSummary: "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.ExecutionResult.Checksum != job.ExecutionResult.Checksum {
t.Fatalf("unexpected MySQL snapshot job: job=%+v err=%v", got, err)
}
}
func TestMySQLStoreRequiresDSN(t *testing.T) {