Files
browser/platform/service/scum_live_data_negotiation_test.go
T

122 lines
10 KiB
Go

package service
import (
"strings"
"testing"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
func TestSCUMCapabilityNegotiationEvaluatesActiveBindingEvidenceIndependently(t *testing.T) {
store := repo.NewMemoryStore()
svc := NewCoreService(store)
now := time.Date(2026, 8, 13, 7, 0, 0, 0, time.UTC)
svc.now = func() time.Time { return now }
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
binding := scumCapabilityNegotiationBinding()
schema := scumNegotiationDigest("a")
playerAsset := scumNegotiationDigest("b")
if err := store.Jobs().Create(domain.Job{ID: "job-scum-probe", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSchemaProbeExecutionKind, SQLiteSchemaProbe: &domain.SCUMSchemaProbeResult{RequestID: "job-scum-probe", JobID: "job-scum-probe", Binding: binding, Status: domain.SCUMSchemaProbeStatusSucceeded, SourceFingerprint: scumNegotiationDigest("c"), SchemaFingerprint: schema, ObservedAt: now.Add(-2 * time.Minute), ResultDigest: scumNegotiationDigest("d"), Limits: domain.DefaultSCUMSchemaProbeBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
t.Fatalf("create probe evidence job: %v", err)
}
if err := store.Jobs().Create(domain.Job{ID: "job-scum-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-scum-player-query", JobID: "job-scum-player-query", Binding: binding, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: schema, AssetDigest: playerAsset, ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now.Add(-time.Minute), ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
t.Fatalf("create player evidence job: %v", err)
}
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
if err != nil {
t.Fatalf("negotiate capabilities: %v", err)
}
gates := map[domain.SCUMDataCapability]domain.SCUMCapabilityGate{}
for _, gate := range negotiation.Gates {
gates[gate.Capability] = gate
}
if !negotiation.ProbeExecutorAvailable || negotiation.RunBindingID != binding.RunBindingID || negotiation.DatabaseIdentity != binding.DatabaseIdentity {
t.Fatalf("unexpected negotiation identity: %+v", negotiation)
}
if gate := gates[domain.SCUMDataCapabilitySchemaProbe]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
t.Fatalf("schema probe should be enabled from accepted probe evidence, got %+v", gate)
}
if gate := gates[domain.SCUMDataCapabilityPlayerRead]; !gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorNone {
t.Fatalf("players.read should be enabled from matching template evidence, got %+v", gate)
}
if gate := gates[domain.SCUMDataCapabilitySquadRead]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeMissing {
t.Fatalf("squads.read should remain independently disabled without template evidence, got %+v", gate)
}
if gate := gates[domain.SCUMDataCapabilityEconomyCommand]; gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorProbeExecutorAbsent {
t.Fatalf("economy-command.write should remain disabled when Run lacks protected RCON, got %+v", gate)
}
}
func TestSCUMCapabilityNegotiationRejectsEvidenceFromAnotherBinding(t *testing.T) {
store := repo.NewMemoryStore()
svc := NewCoreService(store)
now := time.Date(2026, 8, 13, 7, 30, 0, 0, time.UTC)
svc.now = func() time.Time { return now }
sessionID := seedSCUMCapabilityNegotiationFixture(t, svc, store)
binding := scumCapabilityNegotiationBinding()
stale := binding
stale.RunBindingID = "runtime-binding-old"
if err := store.Jobs().Create(domain.Job{ID: "job-stale-player-query", ServerInstanceID: binding.ServerInstanceID, RunEndpointID: binding.RunEndpointID, State: domain.JobStateSucceeded, ExecutionResult: domain.JobExecutionResult{Kind: scumSQLiteTemplateExecutionKind, SQLiteTemplate: &domain.SCUMSQLiteTemplateResult{RequestID: "job-stale-player-query", JobID: "job-stale-player-query", Binding: stale, Status: domain.SCUMTerminalResultSucceeded, Capability: domain.SCUMDataCapabilityPlayerRead, TargetKey: "scum-database", TemplateKey: "players-read", AdapterVersion: binding.AdapterVersion, SchemaFingerprint: scumNegotiationDigest("a"), AssetDigest: scumNegotiationDigest("b"), ParameterDigest: scumNegotiationDigest("e"), SourceFingerprint: scumNegotiationDigest("f"), ObservedAt: now, ResultDigest: scumNegotiationDigest("1"), RowCount: 0, Limits: domain.DefaultSCUMSQLiteTemplateBounds(), SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}}}); err != nil {
t.Fatalf("create stale evidence job: %v", err)
}
negotiation, err := svc.NegotiateSCUMCapabilitiesForSession(sessionID, binding.ServerInstanceID)
if err != nil {
t.Fatalf("negotiate capabilities: %v", err)
}
for _, gate := range negotiation.Gates {
if gate.Capability == domain.SCUMDataCapabilityPlayerRead {
if gate.Enabled || gate.ReasonCode != domain.SCUMSafeErrorBindingMismatch {
t.Fatalf("players.read should reject stale binding evidence, got %+v", gate)
}
return
}
}
t.Fatal("players.read gate missing")
}
func seedSCUMCapabilityNegotiationFixture(t *testing.T, svc *CoreService, store *repo.MemoryStore) string {
t.Helper()
if _, err := svc.CreateUser(domain.User{ID: "scum-negotiation-owner", DisplayName: "SCUM Negotiation Owner", Email: "scum-negotiation@example.test", Status: domain.UserStatusActive, Roles: []string{"server-owner"}, PasswordHash: "secret-password"}); err != nil {
t.Fatalf("create user: %v", err)
}
auth, err := svc.LoginUser(domain.UserLogin{Account: "scum-negotiation@example.test", Password: "secret-password"})
if err != nil {
t.Fatalf("login: %v", err)
}
plugin := domain.GamePlugin{ID: "game.scum", Version: "1.0.0", Status: domain.GamePluginStatusInstalled, SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{
{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "schema probe evidence is compatible"},
{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("b")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "players query evidence is compatible"},
{Capability: domain.SCUMDataCapabilitySquadRead, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("2")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "squad query evidence is compatible"},
{Capability: domain.SCUMDataCapabilityEconomyCommand, Gate: domain.SCUMCapabilityGateEnabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), RequiredAssetDigests: []string{scumNegotiationDigest("3")}, EvidenceStatus: domain.SCUMCapabilityEvidenceCompatible, SafeReason: "economy command evidence is compatible"},
{Capability: domain.SCUMDataCapabilityGiftCommand, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "gift command evidence is missing"},
{Capability: domain.SCUMDataCapabilityProfileXMLWrite, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "adapter-1", RequiredSchemaFingerprint: scumNegotiationDigest("a"), EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "XML mutation evidence is missing"},
}}, RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process"}}}}
if err := store.GamePlugins().Create(plugin); err != nil {
t.Fatalf("create plugin: %v", err)
}
endpoint := domain.RunEndpoint{ID: "run-scum-negotiation", DisplayName: "Run SCUM Negotiation", Version: "0.1.0", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery}}
if err := store.RunEndpoints().Create(endpoint); err != nil {
t.Fatalf("create endpoint: %v", err)
}
if err := store.ServerInstances().Create(domain.ServerInstance{ID: "server-scum-negotiation", PluginID: plugin.ID, PluginVersion: plugin.Version, RunEndpointID: endpoint.ID, Name: "SCUM Negotiation", OwnerUserID: "scum-negotiation-owner", State: domain.ServerInstanceStateRunning}); err != nil {
t.Fatalf("create server: %v", err)
}
if _, err := svc.UpdateServerRuntimeBindingForSession(auth.SessionID, "server-scum-negotiation", domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{}}); err != nil {
t.Fatalf("create runtime binding: %v", err)
}
return auth.SessionID
}
func scumCapabilityNegotiationBinding() domain.SCUMBindingIdentity {
return domain.SCUMBindingIdentity{ServerInstanceID: "server-scum-negotiation", RunBindingID: "runtime-binding-server-scum-negotiation", RunEndpointID: "run-scum-negotiation", PluginID: "game.scum", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", DatabaseIdentity: "scum-database"}
}
func scumNegotiationDigest(char string) string {
return "sha256:" + strings.Repeat(char, 64)
}