功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
+4
View File
@@ -39,6 +39,8 @@ type AIConfigRecommendationResponse struct {
Key string `json:"key"`
SuggestedConfig string `json:"suggestedConfig,omitempty"`
DiffSummary string `json:"diffSummary"`
DiffID string `json:"diffId,omitempty"`
ExpiresAt string `json:"expiresAt,omitempty"`
}
type AIInvocationSafeErrorResponse struct {
@@ -82,6 +84,8 @@ func AIInvocationFromDomain(response domain.AIInvocationResponse) AIInvocationRe
Key: response.ConfigRecommendation.Key,
SuggestedConfig: response.ConfigRecommendation.SuggestedConfig,
DiffSummary: response.ConfigRecommendation.DiffSummary,
DiffID: response.ConfigRecommendation.DiffID,
ExpiresAt: response.ConfigRecommendation.ExpiresAt,
}
}
var safeError *AIInvocationSafeErrorResponse
+61 -20
View File
@@ -122,25 +122,56 @@ type ClientManagerLifecycleInputRequest struct {
}
type ClientManagerLifecycleInputResponse struct {
InstallationID string `json:"installationId"`
ServerInstanceID string `json:"serverInstanceId"`
ProfileKey string `json:"profileKey"`
Operation string `json:"operation"`
ArtifactID string `json:"artifactId,omitempty"`
Checksum string `json:"checksum,omitempty"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
Version string `json:"version,omitempty"`
SourceRevision string `json:"sourceRevision,omitempty"`
KeyGeneration int `json:"keyGeneration"`
DeploymentGeneration int `json:"deploymentGeneration"`
ExecutableRef string `json:"executableRef"`
Arguments []string `json:"arguments"`
AutoStart bool `json:"autoStart"`
StartupTimeoutSeconds int `json:"startupTimeoutSeconds"`
StopTimeoutSeconds int `json:"stopTimeoutSeconds"`
HealthConfirmationSeconds int `json:"healthConfirmationSeconds"`
IdempotencyKey string `json:"idempotencyKey"`
InstallationID string `json:"installationId"`
ServerInstanceID string `json:"serverInstanceId"`
ProfileKey string `json:"profileKey"`
Operation string `json:"operation"`
ArtifactID string `json:"artifactId,omitempty"`
Checksum string `json:"checksum,omitempty"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
Version string `json:"version,omitempty"`
SourceRevision string `json:"sourceRevision,omitempty"`
KeyGeneration int `json:"keyGeneration"`
DeploymentGeneration int `json:"deploymentGeneration"`
ExecutableRef string `json:"executableRef"`
Arguments []string `json:"arguments"`
AutoStart bool `json:"autoStart"`
StartupTimeoutSeconds int `json:"startupTimeoutSeconds"`
StopTimeoutSeconds int `json:"stopTimeoutSeconds"`
HealthConfirmationSeconds int `json:"healthConfirmationSeconds"`
IdempotencyKey string `json:"idempotencyKey"`
CompanionConfig *ClientManagerCompanionConfigInputResponse `json:"companionConfig,omitempty"`
}
type ClientManagerCompanionConfigInputResponse struct {
SchemaVersion int `json:"schemaVersion"`
ConfigTemplateKey string `json:"configTemplateKey"`
ConfigTemplateRef string `json:"configTemplateRef"`
ConfigOutputRef string `json:"configOutputRef"`
ConfigSchemaRef string `json:"configSchemaRef"`
ConfigFormat string `json:"configFormat"`
PlatformBaseURLSource string `json:"platformBaseUrlSource"`
InstallationID string `json:"installationId"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
ArtifactID string `json:"artifactId"`
Version string `json:"version"`
SourceRevision string `json:"sourceRevision"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
KeyGeneration int `json:"keyGeneration"`
DeploymentGeneration int `json:"deploymentGeneration"`
Capabilities []string `json:"capabilities"`
RegistrationProof string `json:"registrationProof"`
ProofMaterialSource string `json:"proofMaterialSource"`
ProofMaterialEnv string `json:"proofMaterialEnv"`
SessionMode string `json:"sessionMode"`
TLSPolicy string `json:"tlsPolicy"`
HeartbeatIntervalSeconds int `json:"heartbeatIntervalSeconds"`
CommandPollIntervalSeconds int `json:"commandPollIntervalSeconds"`
RequestTimeoutSeconds int `json:"requestTimeoutSeconds"`
}
type ClientManagerRegisterRequest struct {
@@ -219,7 +250,17 @@ func (request ClientManagerLifecycleInputRequest) ToDomain() domain.ClientManage
func ClientManagerLifecycleInputFromDomain(value domain.ClientManagerLifecycleInput) ClientManagerLifecycleInputResponse {
value = domain.CopyClientManagerLifecycleInput(value)
return ClientManagerLifecycleInputResponse{InstallationID: value.InstallationID, ServerInstanceID: value.ServerInstanceID, ProfileKey: value.ProfileKey, Operation: string(value.Operation), ArtifactID: value.ArtifactID, Checksum: value.Checksum, TargetOS: value.TargetOS, TargetArch: value.TargetArch, Version: value.Version, SourceRevision: value.SourceRevision, KeyGeneration: value.KeyGeneration, DeploymentGeneration: value.DeploymentGeneration, ExecutableRef: value.ExecutableRef, Arguments: value.Arguments, AutoStart: value.AutoStart, StartupTimeoutSeconds: value.StartupTimeoutSeconds, StopTimeoutSeconds: value.StopTimeoutSeconds, HealthConfirmationSeconds: value.HealthConfirmationSeconds, IdempotencyKey: value.IdempotencyKey}
response := ClientManagerLifecycleInputResponse{InstallationID: value.InstallationID, ServerInstanceID: value.ServerInstanceID, ProfileKey: value.ProfileKey, Operation: string(value.Operation), ArtifactID: value.ArtifactID, Checksum: value.Checksum, TargetOS: value.TargetOS, TargetArch: value.TargetArch, Version: value.Version, SourceRevision: value.SourceRevision, KeyGeneration: value.KeyGeneration, DeploymentGeneration: value.DeploymentGeneration, ExecutableRef: value.ExecutableRef, Arguments: value.Arguments, AutoStart: value.AutoStart, StartupTimeoutSeconds: value.StartupTimeoutSeconds, StopTimeoutSeconds: value.StopTimeoutSeconds, HealthConfirmationSeconds: value.HealthConfirmationSeconds, IdempotencyKey: value.IdempotencyKey}
if value.CompanionConfig != nil {
companion := ClientManagerCompanionConfigInputFromDomain(*value.CompanionConfig)
response.CompanionConfig = &companion
}
return response
}
func ClientManagerCompanionConfigInputFromDomain(value domain.ClientManagerCompanionConfigInput) ClientManagerCompanionConfigInputResponse {
value = domain.CopyClientManagerCompanionConfigInput(value)
return ClientManagerCompanionConfigInputResponse{SchemaVersion: value.SchemaVersion, ConfigTemplateKey: value.ConfigTemplateKey, ConfigTemplateRef: value.ConfigTemplateRef, ConfigOutputRef: value.ConfigOutputRef, ConfigSchemaRef: value.ConfigSchemaRef, ConfigFormat: value.ConfigFormat, PlatformBaseURLSource: value.PlatformBaseURLSource, InstallationID: value.InstallationID, ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, ProfileKey: value.ProfileKey, ArtifactID: value.ArtifactID, Version: value.Version, SourceRevision: value.SourceRevision, TargetOS: value.TargetOS, TargetArch: value.TargetArch, KeyGeneration: value.KeyGeneration, DeploymentGeneration: value.DeploymentGeneration, Capabilities: value.Capabilities, RegistrationProof: value.RegistrationProof, ProofMaterialSource: value.ProofMaterialSource, ProofMaterialEnv: value.ProofMaterialEnv, SessionMode: value.SessionMode, TLSPolicy: value.TLSPolicy, HeartbeatIntervalSeconds: value.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.RequestTimeoutSeconds}
}
func (request ClientManagerRegisterRequest) ToDomain() domain.ClientManagerRegisterRequest {
@@ -0,0 +1,69 @@
package dto
import (
"encoding/json"
"strings"
"testing"
"browser.local/platform/domain"
)
func TestClientManagerLifecycleInputProjectsRunOnlyCompanionConfigWithoutRawMaterial(t *testing.T) {
response := ClientManagerLifecycleInputFromDomain(domain.ClientManagerLifecycleInput{
InstallationID: "installation-1",
ServerInstanceID: "server-1",
ProfileKey: "scum-client-manager",
Operation: domain.ClientManagerOperationDeploy,
DeploymentGeneration: 2,
CompanionConfig: &domain.ClientManagerCompanionConfigInput{
SchemaVersion: 1,
ConfigTemplateKey: "client-config",
ConfigTemplateRef: "config.yaml.example",
ConfigOutputRef: "config.yaml",
ConfigSchemaRef: "schemas/companion/config.schema.json",
ConfigFormat: "yaml",
PlatformBaseURLSource: "run-control",
InstallationID: "installation-1",
ServerInstanceID: "server-1",
PluginID: "game.scum",
ProfileKey: "scum-client-manager",
ArtifactID: "artifact-1",
Version: "1.0.0",
SourceRevision: "revision-1",
TargetOS: "windows",
TargetArch: "amd64",
KeyGeneration: 3,
DeploymentGeneration: 2,
Capabilities: []string{"component.register", "component.heartbeat", "component.health", "game-client.bridge"},
RegistrationProof: "hmac-sha256",
ProofMaterialSource: "component-package",
ProofMaterialEnv: "SCUM_COMPONENT_PROOF",
SessionMode: "component-session",
TLSPolicy: "verify-system-roots",
HeartbeatIntervalSeconds: 30,
CommandPollIntervalSeconds: 5,
RequestTimeoutSeconds: 15,
},
})
if response.CompanionConfig == nil || response.CompanionConfig.ProofMaterialEnv != "SCUM_COMPONENT_PROOF" {
t.Fatalf("expected run-only companion projection, got %+v", response.CompanionConfig)
}
encoded, err := json.Marshal(response)
if err != nil {
t.Fatalf("marshal lifecycle input: %v", err)
}
serialized := string(encoded)
for _, forbidden := range []string{`"proofMaterial":`, `"authKey":`, `"componentKey":`, `"sessionToken":`, `"secretRef":`, "/Users/", "run.socket"} {
if strings.Contains(serialized, forbidden) {
t.Fatalf("run lifecycle input leaked %q: %s", forbidden, serialized)
}
}
browserProjection, err := json.Marshal(ClientManagerLifecycleViewFromDomain(domain.ClientManagerLifecycleView{Installation: domain.ClientManagerInstallation{ID: "installation-1"}}))
if err != nil {
t.Fatalf("marshal browser lifecycle projection: %v", err)
}
if strings.Contains(string(browserProjection), "companionConfig") || strings.Contains(string(browserProjection), "proofMaterialEnv") {
t.Fatalf("browser lifecycle projection exposed run-only config: %s", browserProjection)
}
}
+399
View File
@@ -0,0 +1,399 @@
package dto
import (
"time"
"browser.local/platform/domain"
)
type GameClientBridgeQueueRequest struct {
ProfileKey string `json:"profileKey"`
CommandType string `json:"commandType"`
Payload map[string]any `json:"payload"`
IdempotencyKey string `json:"idempotencyKey"`
Priority int `json:"priority,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
}
type GameClientBridgeClaimRequest struct {
SessionToken string `json:"sessionToken"`
Limit int `json:"limit,omitempty"`
}
type GameClientBridgeAckRequest struct {
SessionToken string `json:"sessionToken"`
FencingToken uint64 `json:"fencingToken"`
}
type GameClientBridgeResultRequest struct {
SessionToken string `json:"sessionToken"`
FencingToken uint64 `json:"fencingToken"`
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
}
type GameClientBridgeSnapshotIngestRequest struct {
SessionToken string `json:"sessionToken"`
Type string `json:"type"`
SchemaVersion string `json:"schemaVersion"`
StreamKey string `json:"streamKey"`
Sequence uint64 `json:"sequence"`
ObservedAt time.Time `json:"observedAt"`
Payload map[string]any `json:"payload"`
KeepForSeconds int `json:"keepForSeconds"`
MaxRecords int `json:"maxRecords,omitempty"`
}
type GameClientBridgeCancelRequest struct {
Reason string `json:"reason,omitempty"`
}
type GameClientBridgeSnapshotQuery struct {
ProfileKey string `json:"profileKey,omitempty"`
Type string `json:"type,omitempty"`
StreamKey string `json:"streamKey,omitempty"`
ObservedAfter time.Time `json:"observedAfter,omitempty"`
Limit int `json:"limit,omitempty"`
}
// GameClientBridgeCommandResultResponse is the browser-safe projection of a
// terminal command result. Result payloads pass the same bounded JSON
// validation as command inputs before they can be persisted.
type GameClientBridgeCommandResultResponse struct {
Status string `json:"status"`
Summary string `json:"summary,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
CompletedAt time.Time `json:"completedAt"`
}
type GameClientBridgeCommandCancellationResponse struct {
RequestedBy string `json:"requestedBy,omitempty"`
Reason string `json:"reason,omitempty"`
CancelledAt time.Time `json:"cancelledAt"`
}
type GameClientBridgeCommandResponse struct {
ID string `json:"id"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
CommandType string `json:"commandType"`
Priority int `json:"priority"`
State string `json:"state"`
ApprovalState string `json:"approvalState"`
RequesterID string `json:"requesterId,omitempty"`
ResultSummary string `json:"resultSummary,omitempty"`
Result *GameClientBridgeCommandResultResponse `json:"result,omitempty"`
Cancellation *GameClientBridgeCommandCancellationResponse `json:"cancellation,omitempty"`
AuditReferences []string `json:"auditReferences,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CompletedAt time.Time `json:"completedAt,omitempty"`
}
type GameClientBridgeRetentionResponse struct {
KeepForSeconds int `json:"keepForSeconds"`
MaxRecords int `json:"maxRecords,omitempty"`
}
type GameClientBridgeSnapshotResponse struct {
ID string `json:"id"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
Type string `json:"type"`
SchemaVersion string `json:"schemaVersion"`
StreamKey string `json:"streamKey"`
Sequence uint64 `json:"sequence"`
ObservedAt time.Time `json:"observedAt"`
Payload map[string]any `json:"payload"`
Retention GameClientBridgeRetentionResponse `json:"retention"`
AuditReferences []string `json:"auditReferences,omitempty"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
type GameClientBridgeCommandListResponse struct {
Items []GameClientBridgeCommandResponse `json:"items"`
Count int `json:"count"`
}
type GameClientBridgeSnapshotListResponse struct {
Items []GameClientBridgeSnapshotResponse `json:"items"`
Count int `json:"count"`
}
type GameClientBridgeClaimedCommandResponse struct {
ID string `json:"id"`
ProfileKey string `json:"profileKey"`
CommandType string `json:"commandType"`
Payload map[string]any `json:"payload"`
Priority int `json:"priority"`
FencingToken uint64 `json:"fencingToken"`
ClaimedAt time.Time `json:"claimedAt"`
LeaseExpiresAt time.Time `json:"leaseExpiresAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
type GameClientBridgeClaimResponse struct {
Items []GameClientBridgeClaimedCommandResponse `json:"items"`
Count int `json:"count"`
}
// GameClientBridgeClaimBatchResponse is kept as a descriptive alias for API
// handlers that name the operation after its bounded batch semantics.
type GameClientBridgeClaimBatchResponse = GameClientBridgeClaimResponse
type GameClientBridgeAckResponse struct {
CommandID string `json:"commandId"`
State string `json:"state"`
FencingToken uint64 `json:"fencingToken"`
AcknowledgedAt time.Time `json:"acknowledgedAt"`
}
type GameClientBridgeResultResponse struct {
CommandID string `json:"commandId"`
State string `json:"state"`
Result GameClientBridgeCommandResultResponse `json:"result"`
UpdatedAt time.Time `json:"updatedAt"`
CompletedAt time.Time `json:"completedAt"`
}
type GameClientBridgeCancelResponse struct {
CommandID string `json:"commandId"`
State string `json:"state"`
Cancellation GameClientBridgeCommandCancellationResponse `json:"cancellation"`
AuditReferences []string `json:"auditReferences,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
}
type GameClientBridgeSnapshotIngestResponse struct {
SnapshotID string `json:"snapshotId"`
ProfileKey string `json:"profileKey"`
Type string `json:"type"`
SchemaVersion string `json:"schemaVersion"`
StreamKey string `json:"streamKey"`
Sequence uint64 `json:"sequence"`
AcceptedAt time.Time `json:"acceptedAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
type GameClientBridgeAuditReferenceResponse struct {
ID string `json:"id"`
CommandID string `json:"commandId,omitempty"`
SnapshotID string `json:"snapshotId,omitempty"`
AuditEventID string `json:"auditEventId"`
CreatedAt time.Time `json:"createdAt"`
}
type GameClientBridgeAuditReferenceListResponse struct {
Items []GameClientBridgeAuditReferenceResponse `json:"items"`
Count int `json:"count"`
}
type GameClientBridgeProfileDeclarationResponse struct {
PluginID string `json:"pluginId"`
ProfileKey string `json:"profileKey"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
CommandTypes []string `json:"commandTypes"`
SnapshotTypes []string `json:"snapshotTypes"`
QueryTemplateKeys []string `json:"queryTemplateKeys"`
}
type GameClientBridgeStatusResponse struct {
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
Available bool `json:"available"`
Reason string `json:"reason,omitempty"`
Profiles []GameClientBridgeProfileDeclarationResponse `json:"profiles"`
}
func (request GameClientBridgeQueueRequest) ToDomain(serverID, pluginID string) domain.GameClientBridgeQueueRequest {
return domain.GameClientBridgeQueueRequest{ServerInstanceID: serverID, PluginID: pluginID, ProfileKey: request.ProfileKey, CommandType: request.CommandType, Payload: domain.CopyGameClientBridgePayload(request.Payload), IdempotencyKey: request.IdempotencyKey, Priority: request.Priority, ExpiresAt: request.ExpiresAt}
}
func (request GameClientBridgeClaimRequest) ToDomain() domain.GameClientBridgeClaimRequest {
return domain.GameClientBridgeClaimRequest{SessionToken: request.SessionToken, Limit: request.Limit}
}
func (request GameClientBridgeAckRequest) ToDomain(commandID string) domain.GameClientBridgeAckRequest {
return domain.GameClientBridgeAckRequest{SessionToken: request.SessionToken, CommandID: commandID, FencingToken: request.FencingToken}
}
func (request GameClientBridgeResultRequest) ToDomain(commandID string) domain.GameClientBridgeResultRequest {
return domain.GameClientBridgeResultRequest{SessionToken: request.SessionToken, CommandID: commandID, FencingToken: request.FencingToken, Status: domain.GameClientBridgeResultStatus(request.Status), Summary: request.Summary, Payload: domain.CopyGameClientBridgePayload(request.Payload)}
}
func (request GameClientBridgeCancelRequest) ToDomain(commandID string) domain.GameClientBridgeCancelRequest {
return domain.GameClientBridgeCancelRequest{CommandID: commandID, Reason: request.Reason}
}
func (request GameClientBridgeSnapshotIngestRequest) ToDomain() domain.GameClientBridgeSnapshotIngestRequest {
return domain.GameClientBridgeSnapshotIngestRequest{SessionToken: request.SessionToken, Type: request.Type, SchemaVersion: request.SchemaVersion, StreamKey: request.StreamKey, Sequence: request.Sequence, ObservedAt: request.ObservedAt, Payload: domain.CopyGameClientBridgePayload(request.Payload), Retention: domain.GameClientBridgeRetention{KeepForSeconds: request.KeepForSeconds, MaxRecords: request.MaxRecords}}
}
func (query GameClientBridgeSnapshotQuery) ToDomain(serverID, pluginID string) domain.GameClientBridgeSnapshotQuery {
return domain.GameClientBridgeSnapshotQuery{ServerInstanceID: serverID, PluginID: pluginID, ProfileKey: query.ProfileKey, Type: query.Type, StreamKey: query.StreamKey, ObservedAfter: query.ObservedAfter, Limit: query.Limit}
}
func GameClientBridgeCommandFromDomain(value domain.GameClientBridgeCommand) GameClientBridgeCommandResponse {
value = domain.CopyGameClientBridgeCommand(value)
response := GameClientBridgeCommandResponse{
ID: value.ID,
ServerInstanceID: value.ServerInstanceID,
PluginID: value.PluginID,
ProfileKey: value.ProfileKey,
CommandType: value.CommandType,
Priority: value.Priority,
State: string(value.State),
ApprovalState: string(value.ApprovalState),
RequesterID: value.RequesterID,
ResultSummary: value.Result.Summary,
AuditReferences: copyStrings(value.AuditReferences),
ExpiresAt: value.ExpiresAt,
CreatedAt: value.CreatedAt,
UpdatedAt: value.UpdatedAt,
CompletedAt: value.CompletedAt,
}
if hasGameClientBridgeResult(value.Result) {
result := gameClientBridgeCommandResultFromDomain(value.Result)
response.Result = &result
}
if hasGameClientBridgeCancellation(value.Cancellation) {
cancellation := gameClientBridgeCommandCancellationFromDomain(value.Cancellation)
response.Cancellation = &cancellation
}
return response
}
func GameClientBridgeSnapshotFromDomain(value domain.GameClientBridgeSnapshot) GameClientBridgeSnapshotResponse {
value = domain.CopyGameClientBridgeSnapshot(value)
return GameClientBridgeSnapshotResponse{
ID: value.ID,
ServerInstanceID: value.ServerInstanceID,
PluginID: value.PluginID,
ProfileKey: value.ProfileKey,
Type: value.Type,
SchemaVersion: value.SchemaVersion,
StreamKey: value.StreamKey,
Sequence: value.Sequence,
ObservedAt: value.ObservedAt,
Payload: value.Payload,
Retention: gameClientBridgeRetentionFromDomain(value.Retention),
AuditReferences: copyStrings(value.AuditReferences),
CreatedAt: value.CreatedAt,
ExpiresAt: value.ExpiresAt,
}
}
func GameClientBridgeClaimedCommandFromDomain(value domain.GameClientBridgeCommand) GameClientBridgeClaimedCommandResponse {
value = domain.CopyGameClientBridgeCommand(value)
return GameClientBridgeClaimedCommandResponse{ID: value.ID, ProfileKey: value.ProfileKey, CommandType: value.CommandType, Payload: value.Payload, Priority: value.Priority, FencingToken: value.Claim.FencingToken, ClaimedAt: value.Claim.ClaimedAt, LeaseExpiresAt: value.Claim.LeaseExpiresAt, ExpiresAt: value.ExpiresAt}
}
func GameClientBridgeCommandsFromDomain(values []domain.GameClientBridgeCommand) GameClientBridgeCommandListResponse {
items := make([]GameClientBridgeCommandResponse, len(values))
for index, value := range values {
items[index] = GameClientBridgeCommandFromDomain(value)
}
return GameClientBridgeCommandListResponse{Items: items, Count: len(items)}
}
func GameClientBridgeSnapshotsFromDomain(values []domain.GameClientBridgeSnapshot) GameClientBridgeSnapshotListResponse {
items := make([]GameClientBridgeSnapshotResponse, len(values))
for index, value := range values {
items[index] = GameClientBridgeSnapshotFromDomain(value)
}
return GameClientBridgeSnapshotListResponse{Items: items, Count: len(items)}
}
func GameClientBridgeClaimResponseFromDomain(values []domain.GameClientBridgeCommand) GameClientBridgeClaimResponse {
items := make([]GameClientBridgeClaimedCommandResponse, len(values))
for index, value := range values {
items[index] = GameClientBridgeClaimedCommandFromDomain(value)
}
return GameClientBridgeClaimResponse{Items: items, Count: len(items)}
}
func GameClientBridgeClaimBatchFromDomain(values []domain.GameClientBridgeCommand) GameClientBridgeClaimBatchResponse {
return GameClientBridgeClaimResponseFromDomain(values)
}
func GameClientBridgeAckFromDomain(value domain.GameClientBridgeCommand) GameClientBridgeAckResponse {
return GameClientBridgeAckResponse{CommandID: value.ID, State: string(value.State), FencingToken: value.Claim.FencingToken, AcknowledgedAt: value.Claim.AcknowledgedAt}
}
func GameClientBridgeResultFromDomain(value domain.GameClientBridgeCommand) GameClientBridgeResultResponse {
value = domain.CopyGameClientBridgeCommand(value)
return GameClientBridgeResultResponse{CommandID: value.ID, State: string(value.State), Result: gameClientBridgeCommandResultFromDomain(value.Result), UpdatedAt: value.UpdatedAt, CompletedAt: value.CompletedAt}
}
func GameClientBridgeCancelFromDomain(value domain.GameClientBridgeCommand) GameClientBridgeCancelResponse {
value = domain.CopyGameClientBridgeCommand(value)
return GameClientBridgeCancelResponse{CommandID: value.ID, State: string(value.State), Cancellation: gameClientBridgeCommandCancellationFromDomain(value.Cancellation), AuditReferences: copyStrings(value.AuditReferences), UpdatedAt: value.UpdatedAt}
}
func GameClientBridgeSnapshotIngestFromDomain(value domain.GameClientBridgeSnapshot) GameClientBridgeSnapshotIngestResponse {
return GameClientBridgeSnapshotIngestResponse{SnapshotID: value.ID, ProfileKey: value.ProfileKey, Type: value.Type, SchemaVersion: value.SchemaVersion, StreamKey: value.StreamKey, Sequence: value.Sequence, AcceptedAt: value.CreatedAt, ExpiresAt: value.ExpiresAt}
}
func GameClientBridgeAuditReferenceFromDomain(value domain.GameClientBridgeAuditReference) GameClientBridgeAuditReferenceResponse {
return GameClientBridgeAuditReferenceResponse{ID: value.ID, CommandID: value.CommandID, SnapshotID: value.SnapshotID, AuditEventID: value.AuditEventID, CreatedAt: value.CreatedAt}
}
func GameClientBridgeAuditReferencesFromDomain(values []domain.GameClientBridgeAuditReference) GameClientBridgeAuditReferenceListResponse {
items := make([]GameClientBridgeAuditReferenceResponse, len(values))
for index, value := range values {
items[index] = GameClientBridgeAuditReferenceFromDomain(value)
}
return GameClientBridgeAuditReferenceListResponse{Items: items, Count: len(items)}
}
func GameClientBridgeStatusFromDomain(value domain.GameClientBridgeStatus) GameClientBridgeStatusResponse {
value = domain.CopyGameClientBridgeStatus(value)
profiles := make([]GameClientBridgeProfileDeclarationResponse, len(value.Profiles))
for index, profile := range value.Profiles {
profiles[index] = GameClientBridgeProfileDeclarationResponse{PluginID: profile.PluginID, ProfileKey: profile.ProfileKey, Available: profile.Available, Reason: profile.Reason, CommandTypes: nonNilStrings(profile.CommandTypes), SnapshotTypes: nonNilStrings(profile.SnapshotTypes), QueryTemplateKeys: nonNilStrings(profile.QueryTemplateKeys)}
}
return GameClientBridgeStatusResponse{ServerInstanceID: value.ServerInstanceID, PluginID: value.PluginID, Available: value.Available, Reason: value.Reason, Profiles: profiles}
}
func gameClientBridgeCommandResultFromDomain(value domain.GameClientBridgeResult) GameClientBridgeCommandResultResponse {
value = domain.CopyGameClientBridgeResult(value)
return GameClientBridgeCommandResultResponse{Status: string(value.Status), Summary: value.Summary, Payload: value.Payload, CompletedAt: value.CompletedAt}
}
func gameClientBridgeCommandCancellationFromDomain(value domain.GameClientBridgeCancellation) GameClientBridgeCommandCancellationResponse {
return GameClientBridgeCommandCancellationResponse{RequestedBy: value.RequestedBy, Reason: value.Reason, CancelledAt: value.CancelledAt}
}
func gameClientBridgeRetentionFromDomain(value domain.GameClientBridgeRetention) GameClientBridgeRetentionResponse {
return GameClientBridgeRetentionResponse{KeepForSeconds: value.KeepForSeconds, MaxRecords: value.MaxRecords}
}
func hasGameClientBridgeResult(value domain.GameClientBridgeResult) bool {
return value.Status != "" || value.Summary != "" || value.Payload != nil || value.CompletedBy != "" || !value.CompletedAt.IsZero()
}
func hasGameClientBridgeCancellation(value domain.GameClientBridgeCancellation) bool {
return value.RequestedBy != "" || value.Reason != "" || !value.CancelledAt.IsZero()
}
func copyStrings(values []string) []string {
if values == nil {
return nil
}
result := make([]string, len(values))
copy(result, values)
return result
}
func nonNilStrings(values []string) []string {
if values == nil {
return []string{}
}
return copyStrings(values)
}
@@ -0,0 +1,44 @@
package dto
import (
"encoding/json"
"strings"
"testing"
)
func TestGameClientBridgeCompanionDeclarationRoundTripsWithoutMaterial(t *testing.T) {
body := GameClientBridgeManifestBody{
CommandRetentionSeconds: 86400,
MaxCommands: 1000,
Companion: &GameClientBridgeCompanionDeclarationBody{
ProfileKey: "scum-client-manager",
ConfigTemplateKey: "client-config",
ConfigSchemaRef: "schemas/companion/config.schema.json",
ConfigFormat: "yaml",
PlatformBaseURLSource: "run-control",
RegistrationProof: "hmac-sha256",
ProofMaterialSource: "component-package",
ProofMaterialEnv: "SCUM_COMPONENT_PROOF",
SessionMode: "component-session",
TLSPolicy: "verify-system-roots",
HeartbeatIntervalSeconds: 30,
CommandPollIntervalSeconds: 5,
RequestTimeoutSeconds: 15,
},
}
domainValue := body.ToDomain()
if domainValue.Companion.ProfileKey != "scum-client-manager" || domainValue.Companion.TLSPolicy != "verify-system-roots" {
t.Fatalf("companion declaration conversion lost fields: %+v", domainValue.Companion)
}
projection := gameClientBridgeManifestFromDomain(domainValue)
encoded, err := json.Marshal(projection)
if err != nil {
t.Fatalf("marshal companion declaration: %v", err)
}
serialized := string(encoded)
for _, forbidden := range []string{"authKey", "componentKey", "sessionToken", "credential", "secretRef", "hostPath", "runSocket"} {
if strings.Contains(serialized, forbidden) {
t.Fatalf("companion declaration exposed %q: %s", forbidden, serialized)
}
}
}
+116
View File
@@ -0,0 +1,116 @@
package dto
import (
"encoding/json"
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
func TestGameClientBridgeRequestConversionsInjectScopeAndCopyPayloads(t *testing.T) {
payload := map[string]any{"players": []any{map[string]any{"name": "Alice"}}}
queue := GameClientBridgeQueueRequest{ProfileKey: "scum-client", CommandType: "player.lookup", Payload: payload, IdempotencyKey: "lookup-1", Priority: 5, ExpiresAt: time.Now().UTC().Add(time.Minute)}.ToDomain("server-1", "game.scum")
claim := GameClientBridgeClaimRequest{SessionToken: "session", Limit: 5}.ToDomain()
ack := GameClientBridgeAckRequest{SessionToken: "session", FencingToken: 7}.ToDomain("command-1")
result := GameClientBridgeResultRequest{SessionToken: "session", FencingToken: 7, Status: "succeeded", Payload: payload}.ToDomain("command-1")
cancel := GameClientBridgeCancelRequest{Reason: "operator request"}.ToDomain("command-1")
snapshot := GameClientBridgeSnapshotIngestRequest{SessionToken: "session", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 2, Payload: payload, KeepForSeconds: 60, MaxRecords: 4}.ToDomain()
query := GameClientBridgeSnapshotQuery{ProfileKey: "scum-client", Type: "players", StreamKey: "current", Limit: 10}.ToDomain("server-1", "game.scum")
if queue.ServerInstanceID != "server-1" || queue.PluginID != "game.scum" || claim.Limit != 5 || ack.CommandID != "command-1" || result.CommandID != "command-1" || cancel.CommandID != "command-1" || snapshot.Retention.MaxRecords != 4 || query.ServerInstanceID != "server-1" || query.PluginID != "game.scum" {
t.Fatalf("request conversion lost route or body fields: queue=%#v claim=%#v ack=%#v result=%#v cancel=%#v snapshot=%#v query=%#v", queue, claim, ack, result, cancel, snapshot, query)
}
queue.Payload["players"].([]any)[0].(map[string]any)["name"] = "queue"
result.Payload["players"].([]any)[0].(map[string]any)["name"] = "result"
snapshot.Payload["players"].([]any)[0].(map[string]any)["name"] = "snapshot"
if got := payload["players"].([]any)[0].(map[string]any)["name"]; got != "Alice" {
t.Fatalf("request conversion aliases source payload: %v", got)
}
}
func TestGameClientBridgeBrowserProjectionsAreCompleteAndOmitInternalData(t *testing.T) {
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
command := domain.GameClientBridgeCommand{
ID: "command-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "announcement.send", Payload: map[string]any{"message": "internal command payload"}, IdempotencyKey: "internal-idempotency", Priority: 9,
State: domain.GameClientBridgeCommandCancelled, ApprovalState: domain.GameClientBridgeApprovalApproved, RequesterID: "operator-1",
Claim: domain.GameClientBridgeClaim{SessionID: "internal-session-secret", InstallationID: "internal-installation", DeploymentGeneration: 9, FencingToken: 42, LeaseExpiresAt: now.Add(time.Minute)},
Result: domain.GameClientBridgeResult{Status: domain.GameClientBridgeResultCancelled, Summary: "cancelled safely", Payload: map[string]any{"code": "cancelled"}, CompletedBy: "internal-completing-session", CompletedAt: now.Add(3 * time.Minute)},
Cancellation: domain.GameClientBridgeCancellation{RequestedBy: "operator-2", Reason: "operator request", CancelledAt: now.Add(2 * time.Minute)},
AuditReferences: []string{"audit-1"}, ExpiresAt: now.Add(10 * time.Minute), CreatedAt: now, UpdatedAt: now.Add(3 * time.Minute), CompletedAt: now.Add(3 * time.Minute),
}
snapshot := domain.GameClientBridgeSnapshot{ID: "snapshot-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 2, SourceSessionID: "internal-source-session", ObservedAt: now, Payload: map[string]any{"players": []any{map[string]any{"name": "Alice"}}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 10}, AuditReferences: []string{"audit-2"}, CreatedAt: now.Add(time.Second), ExpiresAt: now.Add(time.Hour)}
commandProjection := GameClientBridgeCommandFromDomain(command)
snapshotProjection := GameClientBridgeSnapshotFromDomain(snapshot)
if commandProjection.Result == nil || commandProjection.Result.Status != "cancelled" || commandProjection.Cancellation == nil || commandProjection.Cancellation.Reason != "operator request" || commandProjection.Priority != 9 || commandProjection.AuditReferences[0] != "audit-1" || !commandProjection.CompletedAt.Equal(command.CompletedAt) {
t.Fatalf("incomplete command projection: %#v", commandProjection)
}
if snapshotProjection.ProfileKey != "scum-client" || snapshotProjection.Retention.KeepForSeconds != 3600 || snapshotProjection.AuditReferences[0] != "audit-2" || !snapshotProjection.CreatedAt.Equal(snapshot.CreatedAt) {
t.Fatalf("incomplete snapshot projection: %#v", snapshotProjection)
}
commandProjection.Result.Payload["code"] = "changed"
commandProjection.AuditReferences[0] = "changed"
snapshotProjection.Payload["players"].([]any)[0].(map[string]any)["name"] = "changed"
snapshotProjection.AuditReferences[0] = "changed"
if command.Result.Payload["code"] != "cancelled" || command.AuditReferences[0] != "audit-1" || snapshot.Payload["players"].([]any)[0].(map[string]any)["name"] != "Alice" || snapshot.AuditReferences[0] != "audit-2" {
t.Fatal("browser projection aliases domain data")
}
encoded, err := json.Marshal(struct {
Command GameClientBridgeCommandResponse `json:"command"`
Snapshot GameClientBridgeSnapshotResponse `json:"snapshot"`
}{commandProjection, snapshotProjection})
if err != nil {
t.Fatalf("marshal projections: %v", err)
}
text := string(encoded)
for _, forbidden := range []string{"internal-session-secret", "internal-installation", "internal-completing-session", "internal-source-session", "internal-idempotency", "internal command payload", "fencingToken", "deploymentGeneration", "sourceSessionId", "completedBy", "idempotencyKey", "leaseExpiresAt"} {
if strings.Contains(text, forbidden) {
t.Fatalf("browser projection leaked %q: %s", forbidden, text)
}
}
}
func TestGameClientBridgeListAndCompanionResponseConversions(t *testing.T) {
now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
command := domain.GameClientBridgeCommand{
ID: "command-1", ProfileKey: "scum-client", CommandType: "diagnostic.safe", Payload: map[string]any{"scope": "health"}, Priority: 3, State: domain.GameClientBridgeCommandSucceeded,
Claim: domain.GameClientBridgeClaim{SessionID: "private-session", FencingToken: 7, ClaimedAt: now, AcknowledgedAt: now.Add(time.Second), LeaseExpiresAt: now.Add(time.Minute)},
Result: domain.GameClientBridgeResult{Status: domain.GameClientBridgeResultSucceeded, Summary: "ok", Payload: map[string]any{"healthy": true}, CompletedBy: "private-session", CompletedAt: now.Add(2 * time.Second)},
Cancellation: domain.GameClientBridgeCancellation{RequestedBy: "operator", Reason: "superseded", CancelledAt: now.Add(3 * time.Second)}, AuditReferences: []string{"audit-1"}, ExpiresAt: now.Add(5 * time.Minute), UpdatedAt: now.Add(2 * time.Second), CompletedAt: now.Add(2 * time.Second),
}
snapshot := domain.GameClientBridgeSnapshot{ID: "snapshot-1", ProfileKey: "scum-client", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 4, CreatedAt: now, ExpiresAt: now.Add(time.Hour)}
claim := GameClientBridgeClaimBatchFromDomain([]domain.GameClientBridgeCommand{command})
ack := GameClientBridgeAckFromDomain(command)
result := GameClientBridgeResultFromDomain(command)
cancel := GameClientBridgeCancelFromDomain(command)
ingest := GameClientBridgeSnapshotIngestFromDomain(snapshot)
commands := GameClientBridgeCommandsFromDomain([]domain.GameClientBridgeCommand{command})
snapshots := GameClientBridgeSnapshotsFromDomain([]domain.GameClientBridgeSnapshot{snapshot})
audits := GameClientBridgeAuditReferencesFromDomain([]domain.GameClientBridgeAuditReference{{ID: "reference-1", CommandID: "command-1", AuditEventID: "audit-1", CreatedAt: now}})
if claim.Count != 1 || claim.Items[0].FencingToken != 7 || claim.Items[0].ProfileKey != "scum-client" || ack.CommandID != "command-1" || !ack.AcknowledgedAt.Equal(now.Add(time.Second)) || result.Result.Summary != "ok" || cancel.Cancellation.Reason != "superseded" || ingest.SnapshotID != "snapshot-1" || commands.Count != 1 || snapshots.Count != 1 || audits.Count != 1 {
t.Fatalf("unexpected responses: claim=%#v ack=%#v result=%#v cancel=%#v ingest=%#v", claim, ack, result, cancel, ingest)
}
claim.Items[0].Payload["scope"] = "changed"
result.Result.Payload["healthy"] = false
if command.Payload["scope"] != "health" || command.Result.Payload["healthy"] != true {
t.Fatal("companion response aliases domain payload")
}
for name, value := range map[string]any{"claim": claim, "ack": ack, "result": result, "cancel": cancel, "ingest": ingest, "commands": commands, "snapshots": snapshots, "audits": audits} {
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal %s response: %v", name, err)
}
if strings.Contains(string(encoded), "private-session") || strings.Contains(string(encoded), "sessionId") || strings.Contains(string(encoded), "completedBy") {
t.Fatalf("%s response exposes a private session: %s", name, encoded)
}
}
if GameClientBridgeCommandsFromDomain(nil).Items == nil || GameClientBridgeSnapshotsFromDomain(nil).Items == nil || GameClientBridgeClaimBatchFromDomain(nil).Items == nil || GameClientBridgeAuditReferencesFromDomain(nil).Items == nil {
t.Fatal("list converters must serialize empty items as [] instead of null")
}
}
+13 -9
View File
@@ -92,14 +92,18 @@ type RunJobResultRequest struct {
}
type RunJobExecutionInputBody struct {
WorkspaceScope string `json:"workspaceScope,omitempty"`
Content string `json:"content,omitempty"`
ExpectedVersion int `json:"expectedVersion,omitempty"`
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
MaxReadBytes int `json:"maxReadBytes,omitempty"`
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
WorkspaceScope string `json:"workspaceScope,omitempty"`
Content string `json:"content,omitempty"`
ExpectedVersion int `json:"expectedVersion,omitempty"`
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
MaxReadBytes int `json:"maxReadBytes,omitempty"`
RemoteAdapterKey string `json:"remoteAdapterKey,omitempty"`
RemoteAdapterKind string `json:"remoteAdapterKind,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
PluginID string `json:"pluginId,omitempty"`
LifecycleOperation string `json:"lifecycleOperation,omitempty"`
TargetVersion string `json:"targetVersion,omitempty"`
Inputs map[string]string `json:"inputs,omitempty"`
}
type RunJobExecutionResultBody struct {
@@ -525,7 +529,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
State: assignment.State,
Progress: progressReportFromDomain(assignment.Progress),
ResultRef: assignment.ResultRef,
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds},
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs)},
LeaseToken: assignment.LeaseToken,
Attempt: assignment.Attempt,
MaxAttempts: assignment.MaxAttempts,
+9 -7
View File
@@ -82,12 +82,14 @@ type RemoteAdapterDeclarationListResponse struct {
}
type RemoteAdapterRequestBody struct {
DeclarationKey string `json:"declarationKey"`
TargetKey string `json:"targetKey"`
Capability string `json:"capability"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
MaxAttempts int `json:"maxAttempts,omitempty"`
IdempotencyKey string `json:"idempotencyKey"`
DeclarationKey string `json:"declarationKey"`
TargetKey string `json:"targetKey"`
Capability string `json:"capability"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
MaxAttempts int `json:"maxAttempts,omitempty"`
IdempotencyKey string `json:"idempotencyKey"`
InputRef string `json:"inputRef,omitempty"`
Inputs map[string]string `json:"inputs,omitempty"`
}
type RemoteAdapterResponse struct {
@@ -149,7 +151,7 @@ func RemoteAdapterDeclarationsFromDomain(declarations []domain.RemoteAdapterDecl
}
func (request RemoteAdapterRequestBody) ToDomain(serverInstanceID string) domain.RemoteAdapterRequest {
return domain.RemoteAdapterRequest{ServerInstanceID: serverInstanceID, DeclarationKey: request.DeclarationKey, TargetKey: request.TargetKey, Capability: request.Capability, TimeoutSeconds: request.TimeoutSeconds, MaxAttempts: request.MaxAttempts, IdempotencyKey: request.IdempotencyKey}
return domain.RemoteAdapterRequest{ServerInstanceID: serverInstanceID, DeclarationKey: request.DeclarationKey, TargetKey: request.TargetKey, Capability: request.Capability, TimeoutSeconds: request.TimeoutSeconds, MaxAttempts: request.MaxAttempts, IdempotencyKey: request.IdempotencyKey, InputRef: request.InputRef, Inputs: domain.CopyStringMap(request.Inputs)}
}
func RemoteAdapterFromDomain(result domain.RemoteAdapterResult) RemoteAdapterResponse {
+282
View File
@@ -0,0 +1,282 @@
package dto
import (
"time"
"browser.local/platform/domain"
)
type CapacityAdmissionRequest struct {
ServerInstanceID string `json:"serverInstanceId,omitempty"`
RunEndpointID string `json:"runEndpointId,omitempty"`
Capability string `json:"capability"`
TargetKey string `json:"targetKey,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
}
type CapacityAdmissionDecisionResponse struct {
Accepted bool `json:"accepted"`
State string `json:"state"`
Reason string `json:"reason"`
RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"`
ServerInstanceID string `json:"serverInstanceId,omitempty"`
RunEndpointID string `json:"runEndpointId,omitempty"`
Capability string `json:"capability"`
TargetKey string `json:"targetKey,omitempty"`
MaxJobs int `json:"maxJobs"`
RunningJobs int `json:"runningJobs"`
QueuedJobs int `json:"queuedJobs"`
PressureCodes []string `json:"pressureCodes,omitempty"`
CheckedAt time.Time `json:"checkedAt"`
AlertID string `json:"alertId,omitempty"`
AuditEventID string `json:"auditEventId,omitempty"`
}
type EndpointCapacityProjectionResponse struct {
RunEndpointID string `json:"runEndpointId"`
DisplayName string `json:"displayName"`
Status string `json:"status"`
Capabilities []string `json:"capabilities"`
MaxJobs int `json:"maxJobs"`
RunningJobs int `json:"runningJobs"`
QueuedJobs int `json:"queuedJobs"`
LogBacklogBatches int `json:"logBacklogBatches,omitempty"`
ArtifactBacklogChunks int `json:"artifactBacklogChunks,omitempty"`
PressureCodes []string `json:"pressureCodes,omitempty"`
Summary string `json:"summary,omitempty"`
LastHeartbeatAt time.Time `json:"lastHeartbeatAt"`
LastAdmissionDecision string `json:"lastAdmissionDecision,omitempty"`
LastAdmissionReason string `json:"lastAdmissionReason,omitempty"`
LastAdmissionCheckedAt time.Time `json:"lastAdmissionCheckedAt,omitempty"`
}
type ProductionCapacitySummaryResponse struct {
Endpoints []EndpointCapacityProjectionResponse `json:"endpoints"`
TotalMaxJobs int `json:"totalMaxJobs"`
TotalRunningJobs int `json:"totalRunningJobs"`
TotalQueuedJobs int `json:"totalQueuedJobs"`
ActiveAlerts int `json:"activeAlerts"`
GeneratedAt time.Time `json:"generatedAt"`
}
type AlertResponse struct {
ID string `json:"id"`
SourceKind string `json:"sourceKind"`
SourceID string `json:"sourceId"`
RuleKey string `json:"ruleKey"`
Severity string `json:"severity"`
State string `json:"state"`
Title string `json:"title"`
Message string `json:"message"`
OccurrenceCount int `json:"occurrenceCount"`
Retryable bool `json:"retryable"`
RetryAfterSeconds int `json:"retryAfterSeconds,omitempty"`
LastJobID string `json:"lastJobId,omitempty"`
LastAuditEventID string `json:"lastAuditEventId,omitempty"`
LastSeenAt time.Time `json:"lastSeenAt"`
AcknowledgedBy string `json:"acknowledgedBy,omitempty"`
AcknowledgedAt time.Time `json:"acknowledgedAt,omitempty"`
ResolvedBy string `json:"resolvedBy,omitempty"`
ResolvedAt time.Time `json:"resolvedAt,omitempty"`
ResolutionNote string `json:"resolutionNote,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type AlertListResponse struct {
Items []AlertResponse `json:"items"`
Count int `json:"count"`
}
type AlertAcknowledgeRequest struct {
Note string `json:"note,omitempty"`
}
type AlertResolveRequest struct {
Note string `json:"note,omitempty"`
}
type AlertRetryRequest struct {
IdempotencyKey string `json:"idempotencyKey"`
}
type AlertRetryResponse struct {
Status string `json:"status"`
Alert AlertResponse `json:"alert"`
Decision CapacityAdmissionDecisionResponse `json:"decision"`
}
type PluginLifecycleActionRequest struct {
ServerInstanceID string `json:"serverInstanceId"`
Operation string `json:"operation"`
TargetVersion string `json:"targetVersion,omitempty"`
IdempotencyKey string `json:"idempotencyKey"`
Confirmed bool `json:"confirmed"`
}
type PluginLifecycleInstallationResponse struct {
ID string `json:"id"`
PluginID string `json:"pluginId"`
ServerInstanceID string `json:"serverInstanceId"`
CurrentVersion string `json:"currentVersion,omitempty"`
TargetVersion string `json:"targetVersion,omitempty"`
PreviousVersion string `json:"previousVersion,omitempty"`
DesiredState string `json:"desiredState"`
CurrentState string `json:"currentState"`
LastOperation string `json:"lastOperation,omitempty"`
Compatibility string `json:"compatibility,omitempty"`
DependencyState string `json:"dependencyState,omitempty"`
JobID string `json:"jobId,omitempty"`
AlertID string `json:"alertId,omitempty"`
AuditEventID string `json:"auditEventId,omitempty"`
FailureReason string `json:"failureReason,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type PluginLifecycleListResponse struct {
Items []PluginLifecycleInstallationResponse `json:"items"`
Count int `json:"count"`
}
type PluginLifecycleActionResponse struct {
Status string `json:"status"`
Installation PluginLifecycleInstallationResponse `json:"installation"`
Job JobResponse `json:"job"`
Decision CapacityAdmissionDecisionResponse `json:"decision"`
Alert *AlertResponse `json:"alert,omitempty"`
}
type AIConfigDiffPreviewResponse struct {
ID string `json:"id"`
RequestID string `json:"requestId"`
CreatedBy string `json:"createdBy"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId,omitempty"`
ProviderID string `json:"providerId,omitempty"`
Model string `json:"model,omitempty"`
Key string `json:"key"`
ConfigVersion int `json:"configVersion"`
CurrentConfigChecksum string `json:"currentConfigChecksum,omitempty"`
ProposedConfig string `json:"proposedConfig,omitempty"`
DiffSummary string `json:"diffSummary"`
State string `json:"state"`
ExpiresAt time.Time `json:"expiresAt"`
ApprovedBy string `json:"approvedBy,omitempty"`
ApprovedAt time.Time `json:"approvedAt,omitempty"`
CancelledBy string `json:"cancelledBy,omitempty"`
CancelledAt time.Time `json:"cancelledAt,omitempty"`
JobID string `json:"jobId,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type AIConfigDiffListResponse struct {
Items []AIConfigDiffPreviewResponse `json:"items"`
Count int `json:"count"`
}
type AIConfigDiffApprovalRequest struct {
IdempotencyKey string `json:"idempotencyKey"`
}
type AIConfigDiffApprovalResponse struct {
Preview AIConfigDiffPreviewResponse `json:"preview"`
Dispatch ServerConfigWriteDispatchResponse `json:"dispatch"`
}
func (request CapacityAdmissionRequest) ToDomain() domain.CapacityAdmissionRequest {
return domain.CapacityAdmissionRequest{ServerInstanceID: request.ServerInstanceID, RunEndpointID: request.RunEndpointID, Capability: request.Capability, TargetKey: request.TargetKey, IdempotencyKey: request.IdempotencyKey}
}
func CapacityDecisionFromDomain(decision domain.CapacityAdmissionDecision) CapacityAdmissionDecisionResponse {
decision = domain.CopyCapacityAdmissionDecision(decision)
return CapacityAdmissionDecisionResponse{Accepted: decision.Accepted, State: string(decision.State), Reason: decision.Reason, RetryAfterSeconds: decision.RetryAfterSeconds, ServerInstanceID: decision.ServerInstanceID, RunEndpointID: decision.RunEndpointID, Capability: decision.Capability, TargetKey: decision.TargetKey, MaxJobs: decision.MaxJobs, RunningJobs: decision.RunningJobs, QueuedJobs: decision.QueuedJobs, PressureCodes: pressureCodesFromDomain(decision.PressureCodes), CheckedAt: decision.CheckedAt, AlertID: decision.AlertID, AuditEventID: decision.AuditEventID}
}
func ProductionCapacityFromDomain(summary domain.ProductionCapacitySummary) ProductionCapacitySummaryResponse {
summary = domain.CopyProductionCapacitySummary(summary)
items := make([]EndpointCapacityProjectionResponse, len(summary.Endpoints))
for i, endpoint := range summary.Endpoints {
items[i] = EndpointCapacityProjectionResponse{RunEndpointID: endpoint.RunEndpointID, DisplayName: endpoint.DisplayName, Status: string(endpoint.Status), Capabilities: endpoint.Capabilities, MaxJobs: endpoint.MaxJobs, RunningJobs: endpoint.RunningJobs, QueuedJobs: endpoint.QueuedJobs, LogBacklogBatches: endpoint.LogBacklogBatches, ArtifactBacklogChunks: endpoint.ArtifactBacklogChunks, PressureCodes: pressureCodesFromDomain(endpoint.PressureCodes), Summary: endpoint.Summary, LastHeartbeatAt: endpoint.LastHeartbeatAt, LastAdmissionDecision: string(endpoint.LastAdmissionDecision), LastAdmissionReason: endpoint.LastAdmissionReason, LastAdmissionCheckedAt: endpoint.LastAdmissionCheckedAt}
}
return ProductionCapacitySummaryResponse{Endpoints: items, TotalMaxJobs: summary.TotalMaxJobs, TotalRunningJobs: summary.TotalRunningJobs, TotalQueuedJobs: summary.TotalQueuedJobs, ActiveAlerts: summary.ActiveAlerts, GeneratedAt: summary.GeneratedAt}
}
func AlertFromDomain(alert domain.AlertRecord) AlertResponse {
alert = domain.CopyAlertRecord(alert)
return AlertResponse{ID: alert.ID, SourceKind: alert.SourceKind, SourceID: alert.SourceID, RuleKey: alert.RuleKey, Severity: string(alert.Severity), State: string(alert.State), Title: alert.Title, Message: alert.Message, OccurrenceCount: alert.OccurrenceCount, Retryable: alert.Retryable, RetryAfterSeconds: alert.RetryAfterSeconds, LastJobID: alert.LastJobID, LastAuditEventID: alert.LastAuditEventID, LastSeenAt: alert.LastSeenAt, AcknowledgedBy: alert.AcknowledgedBy, AcknowledgedAt: alert.AcknowledgedAt, ResolvedBy: alert.ResolvedBy, ResolvedAt: alert.ResolvedAt, ResolutionNote: alert.ResolutionNote, CreatedAt: alert.CreatedAt, UpdatedAt: alert.UpdatedAt}
}
func AlertListFromDomain(alerts []domain.AlertRecord) AlertListResponse {
items := make([]AlertResponse, len(alerts))
for i, alert := range alerts {
items[i] = AlertFromDomain(alert)
}
return AlertListResponse{Items: items, Count: len(items)}
}
func AlertRetryFromDomain(result domain.AlertRetryResult) AlertRetryResponse {
result = domain.CopyAlertRetryResult(result)
return AlertRetryResponse{Status: result.Status, Alert: AlertFromDomain(result.Alert), Decision: CapacityDecisionFromDomain(result.Decision)}
}
func (request PluginLifecycleActionRequest) ToDomain(pluginID string) domain.PluginLifecycleRequest {
return domain.PluginLifecycleRequest{PluginID: pluginID, ServerInstanceID: request.ServerInstanceID, Operation: domain.PluginLifecycleOperation(request.Operation), TargetVersion: request.TargetVersion, IdempotencyKey: request.IdempotencyKey, Confirmed: request.Confirmed}
}
func PluginLifecycleFromDomain(installation domain.PluginLifecycleInstallation) PluginLifecycleInstallationResponse {
installation = domain.CopyPluginLifecycleInstallation(installation)
return PluginLifecycleInstallationResponse{ID: installation.ID, PluginID: installation.PluginID, ServerInstanceID: installation.ServerInstanceID, CurrentVersion: installation.CurrentVersion, TargetVersion: installation.TargetVersion, PreviousVersion: installation.PreviousVersion, DesiredState: string(installation.DesiredState), CurrentState: string(installation.CurrentState), LastOperation: string(installation.LastOperation), Compatibility: installation.Compatibility, DependencyState: string(installation.DependencyState), JobID: installation.JobID, AlertID: installation.AlertID, AuditEventID: installation.AuditEventID, FailureReason: installation.FailureReason, CreatedAt: installation.CreatedAt, UpdatedAt: installation.UpdatedAt}
}
func PluginLifecycleListFromDomain(installations []domain.PluginLifecycleInstallation) PluginLifecycleListResponse {
items := make([]PluginLifecycleInstallationResponse, len(installations))
for i, installation := range installations {
items[i] = PluginLifecycleFromDomain(installation)
}
return PluginLifecycleListResponse{Items: items, Count: len(items)}
}
func PluginLifecycleResultFromDomain(result domain.PluginLifecycleResult) PluginLifecycleActionResponse {
result = domain.CopyPluginLifecycleResult(result)
var alert *AlertResponse
if result.Alert != nil {
item := AlertFromDomain(*result.Alert)
alert = &item
}
return PluginLifecycleActionResponse{Status: result.Status, Installation: PluginLifecycleFromDomain(result.Installation), Job: JobFromDomain(result.Job), Decision: CapacityDecisionFromDomain(result.Decision), Alert: alert}
}
func AIConfigDiffFromDomain(preview domain.AIConfigDiffPreview) AIConfigDiffPreviewResponse {
preview = domain.CopyAIConfigDiffPreview(preview)
return AIConfigDiffPreviewResponse{ID: preview.ID, RequestID: preview.RequestID, CreatedBy: preview.CreatedBy, ServerInstanceID: preview.ServerInstanceID, PluginID: preview.PluginID, ProviderID: preview.ProviderID, Model: preview.Model, Key: preview.Key, ConfigVersion: preview.ConfigVersion, CurrentConfigChecksum: preview.CurrentConfigChecksum, ProposedConfig: preview.ProposedConfig, DiffSummary: preview.DiffSummary, State: string(preview.State), ExpiresAt: preview.ExpiresAt, ApprovedBy: preview.ApprovedBy, ApprovedAt: preview.ApprovedAt, CancelledBy: preview.CancelledBy, CancelledAt: preview.CancelledAt, JobID: preview.JobID, CreatedAt: preview.CreatedAt, UpdatedAt: preview.UpdatedAt}
}
func AIConfigDiffListFromDomain(previews []domain.AIConfigDiffPreview) AIConfigDiffListResponse {
items := make([]AIConfigDiffPreviewResponse, len(previews))
for i, preview := range previews {
items[i] = AIConfigDiffFromDomain(preview)
}
return AIConfigDiffListResponse{Items: items, Count: len(items)}
}
func (request AIConfigDiffApprovalRequest) ToDomain(diffID string) domain.AIConfigDiffApprovalRequest {
return domain.AIConfigDiffApprovalRequest{DiffID: diffID, IdempotencyKey: request.IdempotencyKey}
}
func AIConfigDiffApprovalFromDomain(result domain.AIConfigDiffApprovalResult) AIConfigDiffApprovalResponse {
result = domain.CopyAIConfigDiffApprovalResult(result)
return AIConfigDiffApprovalResponse{Preview: AIConfigDiffFromDomain(result.Preview), Dispatch: ServerConfigWriteDispatchFromDomain(result.Dispatch)}
}
func pressureCodesFromDomain(codes []domain.CapacityPressureCode) []string {
if codes == nil {
return nil
}
out := make([]string, len(codes))
for i, code := range codes {
out[i] = string(code)
}
return out
}
+285 -129
View File
@@ -126,17 +126,17 @@ type AIProviderStatusRequest struct {
}
type AIProviderResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Kind domain.AIProviderKind `json:"kind"`
BaseURL string `json:"baseUrl"`
APIKeyConfigured bool `json:"apiKeyConfigured"`
Models []string `json:"models"`
DefaultModel string `json:"defaultModel,omitempty"`
RelayMode domain.AIRelayMode `json:"relayMode"`
TimeoutMS int `json:"timeoutMs"`
Status domain.AIProviderStatus `json:"status"`
RedactionPolicy string `json:"redactionPolicy"`
ID string `json:"id"`
Name string `json:"name"`
Kind domain.AIProviderKind `json:"kind"`
BaseURLConfigured bool `json:"baseUrlConfigured"`
APIKeyConfigured bool `json:"apiKeyConfigured"`
Models []string `json:"models"`
DefaultModel string `json:"defaultModel,omitempty"`
RelayMode domain.AIRelayMode `json:"relayMode"`
TimeoutMS int `json:"timeoutMs"`
Status domain.AIProviderStatus `json:"status"`
RedactionPolicy string `json:"redactionPolicy"`
}
type AIProviderListResponse struct {
@@ -195,7 +195,15 @@ type GamePluginManifestServerBody struct {
}
type GamePluginManifestAIBody struct {
Purposes []string `json:"purposes,omitempty"`
Purposes []string `json:"purposes,omitempty"`
Mediation string `json:"mediation,omitempty"`
ConfigWritePolicy string `json:"configWritePolicy,omitempty"`
}
type GamePluginProductionLifecycleBody struct {
Operations []string `json:"operations"`
DependencyPolicy string `json:"dependencyPolicy"`
ApprovalRequired []string `json:"approvalRequired"`
}
type GamePluginRemoteAccessBody struct {
@@ -206,22 +214,89 @@ type GamePluginRemoteAccessBody struct {
LogTransfer bool `json:"logTransfer,omitempty"`
}
type GameClientBridgeCommandDeclarationBody struct {
Type string `json:"type"`
Title string `json:"title"`
Permission string `json:"permission"`
ApprovalLevel string `json:"approvalLevel"`
PayloadSchemaRef string `json:"payloadSchemaRef"`
ResultSchemaRef string `json:"resultSchemaRef,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds"`
MaxPayloadBytes int `json:"maxPayloadBytes"`
}
type GameClientBridgeSnapshotDeclarationBody struct {
Type string `json:"type"`
SchemaVersion string `json:"schemaVersion"`
SchemaRef string `json:"schemaRef"`
KeepForSeconds int `json:"keepForSeconds"`
MaxRecords int `json:"maxRecords"`
}
type GameClientBridgeQueryTemplateDeclarationBody struct {
Key string `json:"key"`
Title string `json:"title"`
Permission string `json:"permission"`
Engine string `json:"engine"`
TransportKey string `json:"transportKey"`
TargetKey string `json:"targetKey"`
ParameterSchemaRef string `json:"parameterSchemaRef"`
ResultSchemaRef string `json:"resultSchemaRef"`
MaxRows int `json:"maxRows"`
TimeoutSeconds int `json:"timeoutSeconds"`
}
type GameClientBridgePageContractBody struct {
PageKey string `json:"pageKey"`
CommandTypes []string `json:"commandTypes,omitempty"`
SnapshotTypes []string `json:"snapshotTypes,omitempty"`
QueryTemplateKeys []string `json:"queryTemplateKeys,omitempty"`
}
type GameClientBridgeCompanionDeclarationBody struct {
ProfileKey string `json:"profileKey"`
ConfigTemplateKey string `json:"configTemplateKey"`
ConfigSchemaRef string `json:"configSchemaRef"`
ConfigFormat string `json:"configFormat"`
PlatformBaseURLSource string `json:"platformBaseUrlSource"`
RegistrationProof string `json:"registrationProof"`
ProofMaterialSource string `json:"proofMaterialSource"`
ProofMaterialEnv string `json:"proofMaterialEnv"`
SessionMode string `json:"sessionMode"`
TLSPolicy string `json:"tlsPolicy"`
HeartbeatIntervalSeconds int `json:"heartbeatIntervalSeconds"`
CommandPollIntervalSeconds int `json:"commandPollIntervalSeconds"`
RequestTimeoutSeconds int `json:"requestTimeoutSeconds"`
}
type GameClientBridgeManifestBody struct {
Commands []GameClientBridgeCommandDeclarationBody `json:"commands"`
Snapshots []GameClientBridgeSnapshotDeclarationBody `json:"snapshots"`
QueryTemplates []GameClientBridgeQueryTemplateDeclarationBody `json:"queryTemplates,omitempty"`
CommandRetentionSeconds int `json:"commandRetentionSeconds"`
MaxCommands int `json:"maxCommands"`
Pages []GameClientBridgePageContractBody `json:"pages,omitempty"`
Companion *GameClientBridgeCompanionDeclarationBody `json:"companion,omitempty"`
}
type GamePluginManifestBody struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
Kind string `json:"kind"`
Tags []string `json:"tags,omitempty"`
Server GamePluginManifestServerBody `json:"server"`
Bridge GamePluginBridgeBody `json:"bridge,omitempty"`
Capabilities []string `json:"capabilities"`
Permissions []string `json:"permissions"`
Actions PluginLifecycleActionsBody `json:"actions"`
Pages []GamePluginPageBody `json:"pages,omitempty"`
AI GamePluginManifestAIBody `json:"ai,omitempty"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
Kind string `json:"kind"`
Tags []string `json:"tags,omitempty"`
Server GamePluginManifestServerBody `json:"server"`
Bridge GamePluginBridgeBody `json:"bridge,omitempty"`
Capabilities []string `json:"capabilities"`
Permissions []string `json:"permissions"`
Actions PluginLifecycleActionsBody `json:"actions"`
Pages []GamePluginPageBody `json:"pages,omitempty"`
AI GamePluginManifestAIBody `json:"ai,omitempty"`
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
}
type GamePluginManifestRegistrationRequest struct {
@@ -230,50 +305,54 @@ type GamePluginManifestRegistrationRequest struct {
}
type GamePluginCreateRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
DeclaredPermissions []string `json:"declaredPermissions,omitempty"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions,omitempty"`
BridgeActions []string `json:"bridgeActions,omitempty"`
Pages []GamePluginPageBody `json:"pages,omitempty"`
Tags []string `json:"tags,omitempty"`
AIPurposes []string `json:"aiPurposes,omitempty"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
DeclaredPermissions []string `json:"declaredPermissions,omitempty"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions,omitempty"`
BridgeActions []string `json:"bridgeActions,omitempty"`
Pages []GamePluginPageBody `json:"pages,omitempty"`
Tags []string `json:"tags,omitempty"`
AIPurposes []string `json:"aiPurposes,omitempty"`
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle,omitempty"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
}
type GamePluginResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
DeclaredPermissions []string `json:"declaredPermissions"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
BridgeActions []string `json:"bridgeActions"`
Pages []GamePluginPageBody `json:"pages"`
Tags []string `json:"tags"`
AIPurposes []string `json:"aiPurposes"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
RequiredRunCapabilities []string `json:"requiredRunCapabilities"`
DeclaredPermissions []string `json:"declaredPermissions"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
BridgeActions []string `json:"bridgeActions"`
Pages []GamePluginPageBody `json:"pages"`
Tags []string `json:"tags"`
AIPurposes []string `json:"aiPurposes"`
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
}
type GamePluginListResponse struct {
@@ -282,28 +361,30 @@ type GamePluginListResponse struct {
}
type MarketplacePluginResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
Capabilities []string `json:"capabilities"`
DeclaredPermissions []string `json:"declaredPermissions"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
BridgeActions []string `json:"bridgeActions"`
Pages []GamePluginPageBody `json:"pages"`
Tags []string `json:"tags"`
AIPurposes []string `json:"aiPurposes"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
Source string `json:"source"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
ServerType string `json:"serverType"`
ServerDisplayName string `json:"serverDisplayName,omitempty"`
SupportedOS []string `json:"supportedOs,omitempty"`
ManifestRef string `json:"manifestRef"`
CreateFormSchemaRef string `json:"createFormSchemaRef"`
Capabilities []string `json:"capabilities"`
DeclaredPermissions []string `json:"declaredPermissions"`
Permissions PluginPermissionsResponse `json:"permissions"`
LifecycleActions PluginLifecycleActionsBody `json:"lifecycleActions"`
BridgeActions []string `json:"bridgeActions"`
Pages []GamePluginPageBody `json:"pages"`
Tags []string `json:"tags"`
AIPurposes []string `json:"aiPurposes"`
ProductionLifecycle GamePluginProductionLifecycleBody `json:"productionLifecycle"`
RemoteAccess GamePluginRemoteAccessBody `json:"remoteAccess,omitempty"`
RuntimeProfiles GamePluginRuntimeProfilesBody `json:"runtimeProfiles,omitempty"`
GameClientBridge GameClientBridgeManifestBody `json:"gameClientBridge,omitempty"`
ValidationViolations []string `json:"validationViolations,omitempty"`
Status domain.GamePluginStatus `json:"status"`
Source string `json:"source"`
}
type MarketplacePluginListResponse struct {
@@ -520,10 +601,13 @@ type FileOperationDispatchResponse struct {
}
type RunCapacityResponse struct {
MaxJobs int `json:"maxJobs"`
RunningJobs int `json:"runningJobs"`
QueuedJobs int `json:"queuedJobs"`
Summary string `json:"summary,omitempty"`
MaxJobs int `json:"maxJobs"`
RunningJobs int `json:"runningJobs"`
QueuedJobs int `json:"queuedJobs"`
LogBacklogBatches int `json:"logBacklogBatches,omitempty"`
ArtifactBacklogChunks int `json:"artifactBacklogChunks,omitempty"`
PressureCodes []string `json:"pressureCodes,omitempty"`
Summary string `json:"summary,omitempty"`
}
type RunEndpointCreateRequest struct {
@@ -816,21 +900,23 @@ func (request GamePluginManifestRegistrationRequest) ToDomain() domain.GamePlugi
return domain.GamePluginManifestRegistration{
ManifestRef: request.ManifestRef,
Manifest: domain.GamePluginManifest{
ID: request.Manifest.ID,
Name: request.Manifest.Name,
Description: request.Manifest.Description,
Version: request.Manifest.Version,
Kind: request.Manifest.Kind,
Tags: domain.CopyStringSlice(request.Manifest.Tags),
Server: request.Manifest.Server.ToDomain(),
Bridge: request.Manifest.Bridge.ToDomain(),
Capabilities: domain.CopyStringSlice(request.Manifest.Capabilities),
Permissions: domain.CopyStringSlice(request.Manifest.Permissions),
Actions: request.Manifest.Actions.ToDomain(),
Pages: pagesToDomain(request.Manifest.Pages),
AI: request.Manifest.AI.ToDomain(),
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
ID: request.Manifest.ID,
Name: request.Manifest.Name,
Description: request.Manifest.Description,
Version: request.Manifest.Version,
Kind: request.Manifest.Kind,
Tags: domain.CopyStringSlice(request.Manifest.Tags),
Server: request.Manifest.Server.ToDomain(),
Bridge: request.Manifest.Bridge.ToDomain(),
Capabilities: domain.CopyStringSlice(request.Manifest.Capabilities),
Permissions: domain.CopyStringSlice(request.Manifest.Permissions),
Actions: request.Manifest.Actions.ToDomain(),
Pages: pagesToDomain(request.Manifest.Pages),
AI: request.Manifest.AI.ToDomain(),
ProductionLifecycle: request.Manifest.ProductionLifecycle.ToDomain(),
RemoteAccess: request.Manifest.RemoteAccess.ToDomain(),
RuntimeProfiles: request.Manifest.RuntimeProfiles.ToDomain(),
GameClientBridge: request.Manifest.GameClientBridge.ToDomain(),
},
}
}
@@ -849,7 +935,11 @@ func (server GamePluginManifestServerBody) ToDomain() domain.GamePluginManifestS
}
func (ai GamePluginManifestAIBody) ToDomain() domain.GamePluginManifestAI {
return domain.GamePluginManifestAI{Purposes: domain.CopyStringSlice(ai.Purposes)}
return domain.GamePluginManifestAI{Purposes: domain.CopyStringSlice(ai.Purposes), Mediation: ai.Mediation, ConfigWritePolicy: ai.ConfigWritePolicy}
}
func (lifecycle GamePluginProductionLifecycleBody) ToDomain() domain.GamePluginProductionLifecycle {
return domain.GamePluginProductionLifecycle{Operations: domain.CopyStringSlice(lifecycle.Operations), DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: domain.CopyStringSlice(lifecycle.ApprovalRequired)}
}
func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAccess {
@@ -862,6 +952,30 @@ func (remote GamePluginRemoteAccessBody) ToDomain() domain.GamePluginRemoteAcces
}
}
func (body GameClientBridgeManifestBody) ToDomain() domain.GameClientBridgeManifest {
commands := make([]domain.GameClientBridgeCommandDeclaration, len(body.Commands))
for index, command := range body.Commands {
commands[index] = domain.GameClientBridgeCommandDeclaration{Type: command.Type, Title: command.Title, Permission: command.Permission, ApprovalLevel: domain.GameClientBridgeApprovalLevel(command.ApprovalLevel), PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
}
snapshots := make([]domain.GameClientBridgeSnapshotDeclaration, len(body.Snapshots))
for index, snapshot := range body.Snapshots {
snapshots[index] = domain.GameClientBridgeSnapshotDeclaration{Type: snapshot.Type, SchemaVersion: snapshot.SchemaVersion, SchemaRef: snapshot.SchemaRef, Retention: domain.GameClientBridgeRetention{KeepForSeconds: snapshot.KeepForSeconds, MaxRecords: snapshot.MaxRecords}}
}
queryTemplates := make([]domain.GameClientBridgeQueryTemplateDeclaration, len(body.QueryTemplates))
for index, template := range body.QueryTemplates {
queryTemplates[index] = domain.GameClientBridgeQueryTemplateDeclaration{Key: template.Key, Title: template.Title, Permission: template.Permission, Engine: template.Engine, TransportKey: template.TransportKey, TargetKey: template.TargetKey, ParameterSchemaRef: template.ParameterSchemaRef, ResultSchemaRef: template.ResultSchemaRef, MaxRows: template.MaxRows, TimeoutSeconds: template.TimeoutSeconds}
}
pages := make([]domain.GameClientBridgePageContract, len(body.Pages))
for index, page := range body.Pages {
pages[index] = domain.GameClientBridgePageContract{PageKey: page.PageKey, CommandTypes: domain.CopyStringSlice(page.CommandTypes), SnapshotTypes: domain.CopyStringSlice(page.SnapshotTypes), QueryTemplateKeys: domain.CopyStringSlice(page.QueryTemplateKeys)}
}
companion := domain.GameClientBridgeCompanionDeclaration{}
if body.Companion != nil {
companion = domain.GameClientBridgeCompanionDeclaration{ProfileKey: body.Companion.ProfileKey, ConfigTemplateKey: body.Companion.ConfigTemplateKey, ConfigSchemaRef: body.Companion.ConfigSchemaRef, ConfigFormat: body.Companion.ConfigFormat, PlatformBaseURLSource: body.Companion.PlatformBaseURLSource, RegistrationProof: body.Companion.RegistrationProof, ProofMaterialSource: body.Companion.ProofMaterialSource, ProofMaterialEnv: body.Companion.ProofMaterialEnv, SessionMode: body.Companion.SessionMode, TLSPolicy: body.Companion.TLSPolicy, HeartbeatIntervalSeconds: body.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: body.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: body.Companion.RequestTimeoutSeconds}
}
return domain.GameClientBridgeManifest{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, Retention: domain.GameClientBridgeRetention{KeepForSeconds: body.CommandRetentionSeconds, MaxRecords: body.MaxCommands}, Pages: pages, Companion: companion}
}
func (actions PluginLifecycleActionsBody) ToDomain() domain.PluginLifecycleActions {
return domain.PluginLifecycleActions{
Install: actions.Install,
@@ -891,8 +1005,10 @@ func (request GamePluginCreateRequest) ToDomain() domain.GamePlugin {
Pages: pagesToDomain(request.Pages),
Tags: domain.CopyStringSlice(request.Tags),
AIPurposes: domain.CopyStringSlice(request.AIPurposes),
ProductionLifecycle: request.ProductionLifecycle.ToDomain(),
RemoteAccess: request.RemoteAccess.ToDomain(),
RuntimeProfiles: request.RuntimeProfiles.ToDomain(),
GameClientBridge: request.GameClientBridge.ToDomain(),
ValidationViolations: domain.CopyStringSlice(request.ValidationViolations),
}
}
@@ -1073,17 +1189,17 @@ func UserListFromDomain(users []domain.User) UserListResponse {
func AIProviderFromDomain(provider domain.AIProvider) AIProviderResponse {
provider = domain.CopyAIProvider(provider)
return AIProviderResponse{
ID: provider.ID,
Name: provider.Name,
Kind: provider.Kind,
BaseURL: provider.BaseURL,
APIKeyConfigured: provider.APIKeyRef != "",
Models: provider.Models,
DefaultModel: provider.DefaultModel,
RelayMode: provider.RelayMode,
TimeoutMS: provider.TimeoutMS,
Status: provider.Status,
RedactionPolicy: provider.RedactionPolicy,
ID: provider.ID,
Name: provider.Name,
Kind: provider.Kind,
BaseURLConfigured: provider.BaseURL != "",
APIKeyConfigured: provider.APIKeyRef != "",
Models: provider.Models,
DefaultModel: provider.DefaultModel,
RelayMode: provider.RelayMode,
TimeoutMS: provider.TimeoutMS,
Status: provider.Status,
RedactionPolicy: provider.RedactionPolicy,
}
}
@@ -1135,8 +1251,10 @@ func GamePluginFromDomain(plugin domain.GamePlugin) GamePluginResponse {
Pages: pagesFromDomain(plugin.Pages),
Tags: plugin.Tags,
AIPurposes: plugin.AIPurposes,
ProductionLifecycle: productionLifecycleFromDomain(plugin.ProductionLifecycle),
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
}
@@ -1224,14 +1342,46 @@ func MarketplacePluginFromDomain(plugin domain.PluginMarketplacePlugin) Marketpl
Pages: pagesFromDomain(plugin.Pages),
Tags: plugin.Tags,
AIPurposes: plugin.AIPurposes,
ProductionLifecycle: productionLifecycleFromDomain(plugin.ProductionLifecycle),
RemoteAccess: remoteAccessFromDomain(plugin.RemoteAccess),
RuntimeProfiles: runtimeProfilesFromDomain(plugin.RuntimeProfiles),
GameClientBridge: gameClientBridgeManifestFromDomain(plugin.GameClientBridge),
ValidationViolations: plugin.ValidationViolations,
Status: plugin.Status,
Source: plugin.Source,
}
}
func productionLifecycleFromDomain(lifecycle domain.GamePluginProductionLifecycle) GamePluginProductionLifecycleBody {
lifecycle = domain.CopyGamePluginProductionLifecycle(lifecycle)
return GamePluginProductionLifecycleBody{Operations: lifecycle.Operations, DependencyPolicy: lifecycle.DependencyPolicy, ApprovalRequired: lifecycle.ApprovalRequired}
}
func gameClientBridgeManifestFromDomain(value domain.GameClientBridgeManifest) GameClientBridgeManifestBody {
value = domain.CopyGameClientBridgeManifest(value)
commands := make([]GameClientBridgeCommandDeclarationBody, len(value.Commands))
for index, command := range value.Commands {
commands[index] = GameClientBridgeCommandDeclarationBody{Type: command.Type, Title: command.Title, Permission: command.Permission, ApprovalLevel: string(command.ApprovalLevel), PayloadSchemaRef: command.PayloadSchemaRef, ResultSchemaRef: command.ResultSchemaRef, TimeoutSeconds: command.TimeoutSeconds, MaxPayloadBytes: command.MaxPayloadBytes}
}
snapshots := make([]GameClientBridgeSnapshotDeclarationBody, len(value.Snapshots))
for index, snapshot := range value.Snapshots {
snapshots[index] = GameClientBridgeSnapshotDeclarationBody{Type: snapshot.Type, SchemaVersion: snapshot.SchemaVersion, SchemaRef: snapshot.SchemaRef, KeepForSeconds: snapshot.Retention.KeepForSeconds, MaxRecords: snapshot.Retention.MaxRecords}
}
queryTemplates := make([]GameClientBridgeQueryTemplateDeclarationBody, len(value.QueryTemplates))
for index, template := range value.QueryTemplates {
queryTemplates[index] = GameClientBridgeQueryTemplateDeclarationBody{Key: template.Key, Title: template.Title, Permission: template.Permission, Engine: template.Engine, TransportKey: template.TransportKey, TargetKey: template.TargetKey, ParameterSchemaRef: template.ParameterSchemaRef, ResultSchemaRef: template.ResultSchemaRef, MaxRows: template.MaxRows, TimeoutSeconds: template.TimeoutSeconds}
}
pages := make([]GameClientBridgePageContractBody, len(value.Pages))
for index, page := range value.Pages {
pages[index] = GameClientBridgePageContractBody{PageKey: page.PageKey, CommandTypes: page.CommandTypes, SnapshotTypes: page.SnapshotTypes, QueryTemplateKeys: page.QueryTemplateKeys}
}
var companion *GameClientBridgeCompanionDeclarationBody
if value.Companion.ProfileKey != "" {
companion = &GameClientBridgeCompanionDeclarationBody{ProfileKey: value.Companion.ProfileKey, ConfigTemplateKey: value.Companion.ConfigTemplateKey, ConfigSchemaRef: value.Companion.ConfigSchemaRef, ConfigFormat: value.Companion.ConfigFormat, PlatformBaseURLSource: value.Companion.PlatformBaseURLSource, RegistrationProof: value.Companion.RegistrationProof, ProofMaterialSource: value.Companion.ProofMaterialSource, ProofMaterialEnv: value.Companion.ProofMaterialEnv, SessionMode: value.Companion.SessionMode, TLSPolicy: value.Companion.TLSPolicy, HeartbeatIntervalSeconds: value.Companion.HeartbeatIntervalSeconds, CommandPollIntervalSeconds: value.Companion.CommandPollIntervalSeconds, RequestTimeoutSeconds: value.Companion.RequestTimeoutSeconds}
}
return GameClientBridgeManifestBody{Commands: commands, Snapshots: snapshots, QueryTemplates: queryTemplates, CommandRetentionSeconds: value.Retention.KeepForSeconds, MaxCommands: value.Retention.MaxRecords, Pages: pages, Companion: companion}
}
func MarketplacePluginListFromDomain(plugins []domain.PluginMarketplacePlugin) MarketplacePluginListResponse {
items := make([]MarketplacePluginResponse, len(plugins))
for i, plugin := range plugins {
@@ -1606,19 +1756,25 @@ func pagesFromDomain(pages []domain.GamePluginPage) []GamePluginPageBody {
func capacityFromDomain(capacity domain.RunCapacity) RunCapacityResponse {
return RunCapacityResponse{
MaxJobs: capacity.MaxJobs,
RunningJobs: capacity.RunningJobs,
QueuedJobs: capacity.QueuedJobs,
Summary: capacity.Summary,
MaxJobs: capacity.MaxJobs,
RunningJobs: capacity.RunningJobs,
QueuedJobs: capacity.QueuedJobs,
LogBacklogBatches: capacity.LogBacklogBatches,
ArtifactBacklogChunks: capacity.ArtifactBacklogChunks,
PressureCodes: domain.CopyStringSlice(capacity.PressureCodes),
Summary: capacity.Summary,
}
}
func capacityToDomain(capacity RunCapacityResponse) domain.RunCapacity {
return domain.RunCapacity{
MaxJobs: capacity.MaxJobs,
RunningJobs: capacity.RunningJobs,
QueuedJobs: capacity.QueuedJobs,
Summary: capacity.Summary,
MaxJobs: capacity.MaxJobs,
RunningJobs: capacity.RunningJobs,
QueuedJobs: capacity.QueuedJobs,
LogBacklogBatches: capacity.LogBacklogBatches,
ArtifactBacklogChunks: capacity.ArtifactBacklogChunks,
PressureCodes: domain.CopyStringSlice(capacity.PressureCodes),
Summary: capacity.Summary,
}
}
+57 -1
View File
@@ -1,14 +1,18 @@
package dto
import (
"encoding/json"
"reflect"
"testing"
"browser.local/platform/domain"
)
func TestAIProviderResponseExposesOnlyKeyPresence(t *testing.T) {
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")
}
@@ -21,6 +25,9 @@ func TestAIProviderResponseExposesOnlyKeyPresence(t *testing.T) {
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 TestAIProviderFromDomainCopiesModels(t *testing.T) {
@@ -47,6 +54,9 @@ func TestAIProviderFromDomainCopiesModels(t *testing.T) {
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) {
@@ -113,3 +123,49 @@ func TestGamePluginFromDomainCopiesRegistryMetadata(t *testing.T) {
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)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
package dto
import (
"encoding/json"
"testing"
"browser.local/platform/domain"
)
func TestRuntimeLogEventDeclarationRoundTripUsesSafeProjection(t *testing.T) {
body := GamePluginRuntimeProfilesBody{
LogEvents: []RuntimeLogEventBody{{
Key: "chat-message", Title: "Chat message", SourceKey: "chat-log", EventType: "chat.message",
Permission: "server.logs.read", SchemaRef: "schemas/log-events/chat-message.schema.json", RetentionDays: 30, Severity: "info",
}},
}
profiles := body.ToDomain()
if len(profiles.LogEvents) != 1 || profiles.LogEvents[0].EventType != "chat.message" || profiles.LogEvents[0].Severity != "info" {
t.Fatalf("log event conversion lost declaration fields: %+v", profiles.LogEvents)
}
profiles.LogEvents[0].Title = "Mutated"
if body.LogEvents[0].Title != "Chat message" {
t.Fatal("log event domain conversion aliases request DTO data")
}
projection := runtimeProfilesFromDomain(domain.GamePluginRuntimeProfiles{LogEvents: profiles.LogEvents})
encoded, err := json.Marshal(projection.LogEvents[0])
if err != nil {
t.Fatalf("marshal log event projection: %v", err)
}
var fields map[string]any
if err := json.Unmarshal(encoded, &fields); err != nil {
t.Fatalf("decode log event projection: %v", err)
}
expected := []string{"key", "title", "sourceKey", "eventType", "permission", "schemaRef", "retentionDays", "severity"}
if len(fields) != len(expected) {
t.Fatalf("log event projection contains unexpected fields: %s", encoded)
}
for _, field := range expected {
if _, exists := fields[field]; !exists {
t.Fatalf("log event projection is missing %q: %s", field, encoded)
}
}
}
+18
View File
@@ -61,6 +61,17 @@ type RuntimeLogSourceBody struct {
RetentionDays int `json:"retentionDays,omitempty"`
}
type RuntimeLogEventBody struct {
Key string `json:"key"`
Title string `json:"title"`
SourceKey string `json:"sourceKey"`
EventType string `json:"eventType"`
Permission string `json:"permission"`
SchemaRef string `json:"schemaRef"`
RetentionDays int `json:"retentionDays"`
Severity string `json:"severity"`
}
type RuntimeTransportProfileBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
@@ -145,6 +156,7 @@ type GamePluginRuntimeProfilesBody struct {
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
}
@@ -170,6 +182,9 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
for _, item := range body.LogSources {
profiles.LogSources = append(profiles.LogSources, domain.RuntimeLogSource{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
}
for _, item := range body.LogEvents {
profiles.LogEvents = append(profiles.LogEvents, domain.RuntimeLogEvent{Key: item.Key, Title: item.Title, SourceKey: item.SourceKey, EventType: item.EventType, Permission: item.Permission, SchemaRef: item.SchemaRef, RetentionDays: item.RetentionDays, Severity: domain.RuntimeLogEventSeverity(item.Severity)})
}
for _, item := range body.TransportProfiles {
profiles.TransportProfiles = append(profiles.TransportProfiles, domain.RuntimeTransportProfile{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: domain.CopyStringSlice(item.Capabilities)})
}
@@ -217,6 +232,9 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
for _, item := range profiles.LogSources {
body.LogSources = append(body.LogSources, RuntimeLogSourceBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
}
for _, item := range profiles.LogEvents {
body.LogEvents = append(body.LogEvents, RuntimeLogEventBody{Key: item.Key, Title: item.Title, SourceKey: item.SourceKey, EventType: item.EventType, Permission: item.Permission, SchemaRef: item.SchemaRef, RetentionDays: item.RetentionDays, Severity: string(item.Severity)})
}
for _, item := range profiles.TransportProfiles {
body.TransportProfiles = append(body.TransportProfiles, RuntimeTransportProfileBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: item.Capabilities})
}