package dto import ( "encoding/json" "reflect" "strings" "testing" "browser.local/platform/domain" ) func TestAIProviderResponseExposesOnlySecretPresence(t *testing.T) { responseType := reflect.TypeOf(AIProviderResponse{}) if _, ok := responseType.FieldByName("BaseURL"); ok { t.Fatal("AI provider response must not expose the provider base URL") } if _, ok := responseType.FieldByName("APIKey"); ok { t.Fatal("AI provider response must not expose raw API key") } if _, ok := responseType.FieldByName("RawAPIKey"); ok { t.Fatal("AI provider response must not expose raw API key") } if _, ok := responseType.FieldByName("APIKeyRef"); ok { t.Fatal("AI provider response must not expose internal API key reference") } if _, ok := responseType.FieldByName("APIKeyConfigured"); !ok { t.Fatal("AI provider response must expose API key presence") } if _, ok := responseType.FieldByName("BaseURLConfigured"); !ok { t.Fatal("AI provider response must expose base URL presence") } } func TestRunJobResultRequestParsesSQLiteSchemaProbeEnvelope(t *testing.T) { payload := `{ "runEndpointId":"run-local", "sessionToken":"run-session", "jobId":"job-probe", "leaseToken":"lease-probe", "attempt":1, "state":"succeeded", "progress":{"percent":100,"message":"done"}, "executionResult":{ "kind":"sqlite.schema-probe", "sqliteSchemaProbe":{ "requestId":"job-probe", "jobId":"job-probe", "binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"}, "status":"compatible", "sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `", "schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `", "observedAt":"2026-08-12T00:00:00Z", "resultDigest":"sha256:` + strings.Repeat("b", 64) + `", "limits":{"maxObjects":256,"maxColumnsPerObject":128,"maxIndexesPerObject":64,"maxForeignKeys":64,"maxCardinalityReads":64,"maxSampleRows":3,"timeoutMs":5000,"maxResultBytes":524288} } } }` var request RunJobResultRequest if err := json.Unmarshal([]byte(payload), &request); err != nil { t.Fatalf("unmarshal Run job result: %v", err) } domainRequest := request.ToDomain() probe := domainRequest.ExecutionResult.SQLiteSchemaProbe if probe == nil || probe.JobID != "job-probe" || probe.Binding.DatabaseIdentity != "scum-database" || probe.SourceFingerprint != "sha256:"+strings.Repeat("c", 64) || probe.ResultDigest != "sha256:"+strings.Repeat("b", 64) { t.Fatalf("sqliteSchemaProbe envelope did not parse: %+v", probe) } } func TestRunJobResultRequestParsesSQLiteTemplateEnvelope(t *testing.T) { payload := `{ "runEndpointId":"run-local", "sessionToken":"run-session", "jobId":"job-query", "leaseToken":"lease-query", "attempt":1, "state":"succeeded", "progress":{"percent":100,"message":"done"}, "executionResult":{ "kind":"sqlite.template-query", "sqliteTemplate":{ "requestId":"request-query", "jobId":"job-query", "binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"}, "status":"succeeded", "capability":"players.read", "targetKey":"scum-database", "templateKey":"players.active.v1", "adapterVersion":"scum-live-data-v0", "schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `", "assetDigest":"sha256:` + strings.Repeat("d", 64) + `", "parameterDigest":"sha256:` + strings.Repeat("e", 64) + `", "sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `", "observedAt":"2026-08-13T00:00:00Z", "resultDigest":"sha256:` + strings.Repeat("b", 64) + `", "rowCount":1, "rows":[{"externalPlayerId":"player-redacted","fame":12.5,"online":true,"squadId":null}], "limits":{"maxParameters":64,"maxRows":500,"timeoutMs":5000,"busyTimeoutMs":250,"maxResultBytes":1048576} } } }` var request RunJobResultRequest if err := json.Unmarshal([]byte(payload), &request); err != nil { t.Fatalf("unmarshal Run job result: %v", err) } domainRequest := request.ToDomain() result := domainRequest.ExecutionResult.SQLiteTemplate if result == nil || result.TemplateKey != "players.active.v1" || result.AssetDigest != "sha256:"+strings.Repeat("d", 64) || result.RowCount != 1 || result.Rows[0]["fame"].(float64) != 12.5 { t.Fatalf("sqliteTemplate envelope did not parse: %+v", result) } } func TestRunJobResultRequestParsesTypedRCONTemplateEnvelope(t *testing.T) { payload := `{ "runEndpointId":"run-local", "sessionToken":"run-session", "jobId":"job-rcon", "leaseToken":"lease-rcon", "attempt":1, "state":"succeeded", "progress":{"percent":100,"message":"done"}, "executionResult":{ "kind":"rcon.template-command", "rconTemplate":{ "requestId":"request-rcon", "jobId":"job-rcon", "binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"}, "status":"succeeded", "capability":"economy-command.write", "transportKey":"scum-rcon", "targetKey":"scum-rcon", "templateKey":"economy.fame.set.v1", "adapterVersion":"scum-live-data-v0", "schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `", "assetDigest":"sha256:` + strings.Repeat("d", 64) + `", "payloadDigest":"sha256:` + strings.Repeat("e", 64) + `", "confirmationDigest":"sha256:` + strings.Repeat("f", 64) + `", "targetIdentityDigest":"sha256:` + strings.Repeat("c", 64) + `", "observedAt":"2026-08-13T00:00:00Z", "resultDigest":"sha256:` + strings.Repeat("b", 64) + `", "responseDigest":"sha256:` + strings.Repeat("1", 64) + `", "confirmationStatus":"confirmed", "confirmationDigestId":"sha256:` + strings.Repeat("2", 64) + `", "safeSummary":"confirmed by declared readback", "limits":{"maxPayloadBytes":2048,"timeoutMs":5000,"maxResponseBytes":16384,"maxConfirmRecords":16} } } }` var request RunJobResultRequest if err := json.Unmarshal([]byte(payload), &request); err != nil { t.Fatalf("unmarshal Run job result: %v", err) } domainRequest := request.ToDomain() result := domainRequest.ExecutionResult.RCONTemplate if result == nil || result.TemplateKey != "economy.fame.set.v1" || result.PayloadDigest != "sha256:"+strings.Repeat("e", 64) || result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed { t.Fatalf("rconTemplate envelope did not parse: %+v", result) } } func TestRunJobResultRequestParsesGuardedMutationEnvelope(t *testing.T) { payload := `{ "runEndpointId":"run-local", "sessionToken":"run-session", "jobId":"job-mutation", "leaseToken":"lease-mutation", "attempt":1, "state":"succeeded", "progress":{"percent":100,"message":"done"}, "executionResult":{ "kind":"sqlite.guarded-mutation", "guardedMutation":{ "requestId":"request-mutation", "jobId":"job-mutation", "binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"}, "status":"succeeded", "capability":"profile-xml.write", "targetKey":"scum-mutation-db", "templateKey":"profile.attributes.patch.v1", "adapterVersion":"scum-live-data-v0", "schemaFingerprint":"sha256:` + strings.Repeat("a", 64) + `", "assetDigest":"sha256:` + strings.Repeat("d", 64) + `", "sourceFingerprint":"sha256:` + strings.Repeat("c", 64) + `", "targetIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `", "expectedRowDigest":"sha256:` + strings.Repeat("2", 64) + `", "expectedValueDigest":"sha256:` + strings.Repeat("3", 64) + `", "expectedXmlDigest":"sha256:` + strings.Repeat("4", 64) + `", "patchDigest":"sha256:` + strings.Repeat("5", 64) + `", "backupEvidenceDigest":"sha256:` + strings.Repeat("6", 64) + `", "offlineEvidenceDigest":"sha256:` + strings.Repeat("7", 64) + `", "dangerConfirmationDigest":"sha256:` + strings.Repeat("8", 64) + `", "readbackExpectationDigest":"sha256:` + strings.Repeat("9", 64) + `", "observedAt":"2026-08-13T00:00:00Z", "resultDigest":"sha256:` + strings.Repeat("b", 64) + `", "beforeDigest":"sha256:` + strings.Repeat("a", 64) + `", "afterDigest":"sha256:` + strings.Repeat("b", 64) + `", "readbackDigest":"sha256:` + strings.Repeat("c", 64) + `", "affectedRows":1, "readbackStatus":"confirmed", "safeSummary":"confirmed by declared readback", "limits":{"maxPayloadBytes":4096,"timeoutMs":5000,"busyTimeoutMs":250,"maxReadbackBytes":16384,"maxAffectedRows":1} } } }` var request RunJobResultRequest if err := json.Unmarshal([]byte(payload), &request); err != nil { t.Fatalf("unmarshal Run job result: %v", err) } domainRequest := request.ToDomain() result := domainRequest.ExecutionResult.GuardedMutation if result == nil || result.TemplateKey != "profile.attributes.patch.v1" || result.PatchDigest != "sha256:"+strings.Repeat("5", 64) || result.AffectedRows != 1 || result.ReadbackStatus != domain.SCUMMutationReadbackConfirmed { t.Fatalf("guardedMutation envelope did not parse: %+v", result) } } func TestRunJobResultRequestParsesParsedLogBatchEnvelope(t *testing.T) { payload := `{ "runEndpointId":"run-local", "sessionToken":"run-session", "jobId":"job-log", "leaseToken":"lease-log", "attempt":1, "state":"succeeded", "progress":{"percent":100,"message":"done"}, "executionResult":{ "kind":"log.parsed-events", "parsedLogBatch":{ "requestId":"request-log", "jobId":"job-log", "binding":{"serverInstanceId":"server-scum","runBindingId":"runtime-binding-server-scum","runEndpointId":"run-local","pluginId":"server.scum","pluginVersion":"1.0.0","adapterVersion":"scum-live-data-v0","gameVersion":"1.0.0","databaseIdentity":"scum-database"}, "status":"succeeded", "sourceKey":"scum-login-events", "streamKey":"scum.login", "parserKey":"scum-login-log-login-parser", "parserVersion":"scum-login-log-v1", "adapterVersion":"scum-live-data-v0", "assetDigest":"sha256:` + strings.Repeat("d", 64) + `", "parserDigest":"sha256:` + strings.Repeat("e", 64) + `", "observedAt":"2026-08-13T00:00:00Z", "resultDigest":"sha256:` + strings.Repeat("b", 64) + `", "firstCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7}, "lastCursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7}, "tailState":"rotated", "replay":true, "eventCount":1, "events":[{"eventType":"scum.login","occurredAt":"2026-08-13T00:00:00Z","cursor":{"sourceIdentityDigest":"sha256:` + strings.Repeat("1", 64) + `","streamGeneration":"sha256:` + strings.Repeat("2", 64) + `","sequence":7},"logicalEventDigest":"sha256:` + strings.Repeat("3", 64) + `","eventDigest":"sha256:` + strings.Repeat("4", 64) + `","payloadDigest":"sha256:` + strings.Repeat("5", 64) + `","payload":{"externalPlayerId":"player-redacted","displayName":"Known Player"}}], "safeSummary":"one sanitized login event parsed from declared source", "limits":{"maxEvents":256,"maxPayloadBytes":16384,"maxLineBytes":4096,"maxResultBytes":262144} } } }` var request RunJobResultRequest if err := json.Unmarshal([]byte(payload), &request); err != nil { t.Fatalf("unmarshal Run parsed log result: %v", err) } domainRequest := request.ToDomain() result := domainRequest.ExecutionResult.ParsedLogBatch if result == nil || result.ParserKey != "scum-login-log-login-parser" || result.ParserDigest != "sha256:"+strings.Repeat("e", 64) || result.EventCount != 1 || result.Events[0].LogicalEventDigest != "sha256:"+strings.Repeat("3", 64) { t.Fatalf("parsedLogBatch envelope did not parse: %+v", result) } } func TestAIProviderFromDomainCopiesModels(t *testing.T) { provider := 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"}, DefaultModel: "gpt-4.1", RelayMode: domain.AIRelayModeDirect, TimeoutMS: 30000, Status: domain.AIProviderStatusActive, RedactionPolicy: "default", } response := AIProviderFromDomain(provider) response.Models[0] = "mutated" if provider.Models[0] != "gpt-4.1" { t.Fatalf("expected response models to be copied, got source models %+v", provider.Models) } if !response.APIKeyConfigured { t.Fatal("expected configured API key presence") } if !response.BaseURLConfigured { t.Fatal("expected configured base URL presence") } } func TestGamePluginManifestRegistrationToDomainCopiesSlices(t *testing.T) { request := GamePluginManifestRegistrationRequest{ ManifestRef: "artifact://manifests/game.example/0.1.0", Manifest: GamePluginManifestBody{ ID: "game.example", Name: "Example Server", Version: "0.1.0", Kind: "game-plugin", Tags: []string{"example"}, Capabilities: []string{"process.start"}, Permissions: []string{"server.lifecycle"}, Server: GamePluginManifestServerBody{ Type: "example", DisplayName: "Example Server", SupportedOS: []string{"linux"}, CreateFormSchema: "schemas/create-form.schema.json", }, Actions: PluginLifecycleActionsBody{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json"}, AssetFiles: []PluginAssetFileBody{ {Path: "actions/install.json", Mode: 0o600}, }, Pages: []GamePluginPageBody{ {Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}}, }, AI: GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}}, }, AssetFiles: []PluginAssetFileBody{ {Path: "actions/install.json", Content: "{}", Mode: 0o600}, }, } domainRegistration := request.ToDomain() domainRegistration.Manifest.Tags[0] = "mutated" domainRegistration.Manifest.Server.SupportedOS[0] = "darwin" domainRegistration.Manifest.AssetFiles[0].Path = "actions/mutated.json" domainRegistration.AssetFiles[0].Content = "mutated" domainRegistration.Manifest.Pages[0].Permissions[0] = "ai.invoke" domainRegistration.Manifest.AI.Purposes[0] = "config.suggest" if request.Manifest.Tags[0] != "example" || request.Manifest.Server.SupportedOS[0] != "linux" || request.Manifest.AssetFiles[0].Path != "actions/install.json" || request.AssetFiles[0].Content != "{}" || request.Manifest.Pages[0].Permissions[0] != "server.logs.read" || request.Manifest.AI.Purposes[0] != "logs.diagnose" { t.Fatalf("expected manifest request slices to be copied, got %+v", request) } } func TestGamePluginFromDomainCopiesRegistryMetadata(t *testing.T) { plugin := domain.GamePlugin{ ID: "game.example", Name: "Example Server", Version: "0.1.0", ServerType: "example", RequiredRunCapabilities: []string{"process.start"}, DeclaredPermissions: []string{"server.lifecycle"}, SupportedOS: []string{"linux"}, Pages: []domain.GamePluginPage{ {Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}}, }, Tags: []string{"example"}, AIPurposes: []string{"logs.diagnose"}, } response := GamePluginFromDomain(plugin) response.RequiredRunCapabilities[0] = "files.read" response.DeclaredPermissions[0] = "ai.invoke" response.SupportedOS[0] = "darwin" response.Pages[0].Permissions[0] = "ai.invoke" response.Tags[0] = "mutated" response.AIPurposes[0] = "config.suggest" if plugin.RequiredRunCapabilities[0] != "process.start" || plugin.DeclaredPermissions[0] != "server.lifecycle" || plugin.SupportedOS[0] != "linux" || plugin.Pages[0].Permissions[0] != "server.logs.read" || plugin.Tags[0] != "example" || plugin.AIPurposes[0] != "logs.diagnose" { t.Fatalf("expected plugin response registry metadata to be copied, got %+v", plugin) } } func TestGameClientBridgeQueryTemplateDeclarationRoundTripIsSafe(t *testing.T) { body := GameClientBridgeManifestBody{ QueryTemplates: []GameClientBridgeQueryTemplateDeclarationBody{{ Key: "player.lookup", Title: "Player lookup", Permission: "server.game-client.read", Engine: "sqlite", TransportKey: "sqlite-db", TargetKey: "db/sqlite", ParameterSchemaRef: "schemas/bridge/query/player-lookup.parameters.schema.json", ResultSchemaRef: "schemas/bridge/query/player-lookup.result.schema.json", MaxRows: 50, TimeoutSeconds: 10, }}, CommandRetentionSeconds: 86400, MaxCommands: 1000, Pages: []GameClientBridgePageContractBody{{PageKey: "players", QueryTemplateKeys: []string{"player.lookup"}}}, } domainManifest := body.ToDomain() if len(domainManifest.QueryTemplates) != 1 || domainManifest.QueryTemplates[0].TransportKey != "sqlite-db" || domainManifest.QueryTemplates[0].MaxRows != 50 || domainManifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" { t.Fatalf("query template conversion lost declaration fields: %#v", domainManifest) } domainManifest.Pages[0].QueryTemplateKeys[0] = "mutated" if body.Pages[0].QueryTemplateKeys[0] != "player.lookup" { t.Fatal("query template page keys alias request DTO data") } domainManifest.Pages[0].QueryTemplateKeys[0] = "player.lookup" response := gameClientBridgeManifestFromDomain(domainManifest) response.Pages[0].QueryTemplateKeys[0] = "mutated" if domainManifest.Pages[0].QueryTemplateKeys[0] != "player.lookup" { t.Fatal("query template page keys alias domain data") } encoded, err := json.Marshal(response.QueryTemplates[0]) if err != nil { t.Fatalf("marshal safe query template projection: %v", err) } var projection map[string]any if err := json.Unmarshal(encoded, &projection); err != nil { t.Fatalf("decode safe query template projection: %v", err) } expectedFields := []string{"key", "title", "permission", "engine", "transportKey", "targetKey", "parameterSchemaRef", "resultSchemaRef", "maxRows", "timeoutSeconds"} if len(projection) != len(expectedFields) { t.Fatalf("query template projection contains unexpected fields: %s", encoded) } for _, field := range expectedFields { if _, exists := projection[field]; !exists { t.Fatalf("query template projection is missing %q: %s", field, encoded) } } } func TestGameClientBridgeOperationTemplateDeclarationRoundTripIsSafe(t *testing.T) { body := GameClientBridgeManifestBody{ OperationTemplates: []GameClientBridgeOperationTemplateDeclarationBody{{ Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: "platform-admin", Kind: "sqlite-mutation", TransportKey: "scum-mutation-db", TargetKey: "scum-mutation-db", PayloadSchemaRef: "schemas/bridge/operations/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/operations/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/operations/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Safety: GameClientBridgeOperationSafetyBody{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}, }}, CommandRetentionSeconds: 86400, MaxCommands: 1000, Pages: []GameClientBridgePageContractBody{{PageKey: "players", OperationKeys: []string{"player.attribute.855.set"}}}, } domainManifest := body.ToDomain() if len(domainManifest.OperationTemplates) != 1 || domainManifest.OperationTemplates[0].Kind != "sqlite-mutation" || domainManifest.OperationTemplates[0].MaxRowsAffected != 1 || !domainManifest.OperationTemplates[0].Safety.RequiresBeforeValue || domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" { t.Fatalf("operation template conversion lost declaration fields: %#v", domainManifest) } domainManifest.Pages[0].OperationKeys[0] = "mutated" if body.Pages[0].OperationKeys[0] != "player.attribute.855.set" { t.Fatal("operation template page keys alias request DTO data") } domainManifest.Pages[0].OperationKeys[0] = "player.attribute.855.set" response := gameClientBridgeManifestFromDomain(domainManifest) response.Pages[0].OperationKeys[0] = "mutated" if domainManifest.Pages[0].OperationKeys[0] != "player.attribute.855.set" { t.Fatal("operation template page keys alias domain data") } encoded, err := json.Marshal(response.OperationTemplates[0]) if err != nil { t.Fatalf("marshal safe operation template projection: %v", err) } if strings.Contains(strings.ToLower(string(encoded)), "sqltext") || strings.Contains(strings.ToLower(string(encoded)), "dsn") || strings.Contains(strings.ToLower(string(encoded)), "hostpath") || strings.Contains(strings.ToLower(string(encoded)), "socket") || strings.Contains(strings.ToLower(string(encoded)), "credential") { t.Fatalf("operation template projection leaked unsafe material: %s", encoded) } }