Declare SCUM Run data targets
This commit is contained in:
@@ -20,6 +20,8 @@
|
|||||||
- Platform durable probe job `job-remote-adapter-server-scum-1785923898033-3442596095552254276` was queued through `POST /api/v1/server-instances/server-scum-1785923898033/scum/schema-probe` with idempotency key `zao-zhuang-schema-probe-20260812-1632`, claimed by the authenticated active Run binding, acknowledged, and executed with target `databases/scum-database`, `MaxAttempts=1`, and a nonzero fencing token.
|
- Platform durable probe job `job-remote-adapter-server-scum-1785923898033-3442596095552254276` was queued through `POST /api/v1/server-instances/server-scum-1785923898033/scum/schema-probe` with idempotency key `zao-zhuang-schema-probe-20260812-1632`, claimed by the authenticated active Run binding, acknowledged, and executed with target `databases/scum-database`, `MaxAttempts=1`, and a nonzero fencing token.
|
||||||
- The probe terminal result was accepted by Platform as a typed `sqlite.schema-probe` result with safe status `failed`, safe error code `target_unavailable`, result digest `sha256:41624741855866ce10b3143edba66c3a6b771029256b9489a30f395885526b61`, and observed time `2026-08-12T08:31:45Z`. This proves the Platform durable job path and active Run probe executor are wired, but it does not prove current SCUM schema compatibility.
|
- The probe terminal result was accepted by Platform as a typed `sqlite.schema-probe` result with safe status `failed`, safe error code `target_unavailable`, result digest `sha256:41624741855866ce10b3143edba66c3a6b771029256b9489a30f395885526b61`, and observed time `2026-08-12T08:31:45Z`. This proves the Platform durable job path and active Run probe executor are wired, but it does not prove current SCUM schema compatibility.
|
||||||
- The generated Run workspace currently contains lifecycle package assets but no `databases/scum-database` logical database target, so current-service schema capture remains blocked at the package/database-target mapping layer. Keep every database-backed SCUM read/write gate disabled until a subsequent packaged target/snapshot fix produces successful schema metadata.
|
- The generated Run workspace currently contains lifecycle package assets but no `databases/scum-database` logical database target, so current-service schema capture remains blocked at the package/database-target mapping layer. Keep every database-backed SCUM read/write gate disabled until a subsequent packaged target/snapshot fix produces successful schema metadata.
|
||||||
|
- Follow-up bounded SSH diagnostics on `2026-08-12` located exactly one active `SCUM.db` candidate by process-relative metadata only, with no SQL execution or row reads; the live file was locked for direct hashing/copying. This supports the package-target diagnosis but is not current-service schema evidence for tasks 2.5-2.9.
|
||||||
|
- Platform/plugin contracts now declare a plugin-owned `runtimeProfiles.dataTargets` sqlite snapshot target for `scum-database` that materializes to `databases/scum-database` inside the generated Run workspace; SCUM schema-probe dispatch fails closed when that data target is absent. The active external Run still needs the matching generic materializer before a successful current-service probe can complete.
|
||||||
|
|
||||||
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
|
- [x] 2.1 Add a release gate that keeps every database-backed SCUM read and write capability disabled until capability-specific current-service evidence matches a versioned plugin adapter; do not add production SQL or mutation assets before this group is complete.
|
||||||
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
|
- [x] 2.2 Define the minimal generic schema-probe request/result contract, safe error model, binding identity, bounds, and redacted evidence DTO needed by Platform and the plugin without embedding SCUM table names or host paths in Run-facing generic code.
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
|
|
||||||
- Platform now has an internal-only `RequestSCUMSchemaProbeForSession` path that builds a durable `remote.run.db.sqlite.probe` job from the SCUM plugin's manifest declaration, active runtime binding, logical target key, adapter version, and bounded probe limits; public remote-adapter and plugin-page requests for the probe capability are denied.
|
- Platform now has an internal-only `RequestSCUMSchemaProbeForSession` path that builds a durable `remote.run.db.sqlite.probe` job from the SCUM plugin's manifest declaration, active runtime binding, logical target key, adapter version, and bounded probe limits; public remote-adapter and plugin-page requests for the probe capability are denied.
|
||||||
- Run terminal results may carry `executionResult.sqliteSchemaProbe`; Platform DTO/domain/validator/job-channel code validates the typed redacted envelope, job/request identity, and binding fence before persisting it on the durable job.
|
- Run terminal results may carry `executionResult.sqliteSchemaProbe`; Platform DTO/domain/validator/job-channel code validates the typed redacted envelope, job/request identity, and binding fence before persisting it on the durable job.
|
||||||
|
- Generated Run packages now carry redacted autonomous lifecycle `dataTargets` entries for plugin-owned sqlite snapshots, and browser-facing runtime-profile responses continue to omit those source declarations.
|
||||||
- Focused evidence: `go test ./dto ./service -run 'Test(RunJobResultRequestParsesSQLiteSchemaProbeEnvelope|SCUMSchemaProbeDispatchIsPlatformScheduledAndFenced|RemoteAdapterRequestPropagatesTypedInputsToRunJob)'` and `(cd platform && go test ./...)` passed locally. These tests do not prove the active Windows Run deployment or current SCUM schema, so tasks 2.3-2.9 and 4.2-4.9 remain unchecked.
|
- Focused evidence: `go test ./dto ./service -run 'Test(RunJobResultRequestParsesSQLiteSchemaProbeEnvelope|SCUMSchemaProbeDispatchIsPlatformScheduledAndFenced|RemoteAdapterRequestPropagatesTypedInputsToRunJob)'` and `(cd platform && go test ./...)` passed locally. These tests do not prove the active Windows Run deployment or current SCUM schema, so tasks 2.3-2.9 and 4.2-4.9 remain unchecked.
|
||||||
|
|
||||||
- [x] 4.1 Add Platform protocol contracts under `platform/protocol`, API DTOs under `platform/dto`, validation under `platform/validator`, and plugin contracts/assets under `plugins/sdk` and `plugins/schemas`, plus contract documentation/mocks for probes, read-only template execution, typed RCON, guarded SQLite/XML mutation, parsed log events, and terminal result envelopes.
|
- [x] 4.1 Add Platform protocol contracts under `platform/protocol`, API DTOs under `platform/dto`, validation under `platform/validator`, and plugin contracts/assets under `plugins/sdk` and `plugins/schemas`, plus contract documentation/mocks for probes, read-only template execution, typed RCON, guarded SQLite/XML mutation, parsed log events, and terminal result envelopes.
|
||||||
|
|||||||
@@ -20,21 +20,25 @@ func TestSCUMSchemaProbeEndpointQueuesPlatformScheduledDurableJob(t *testing.T)
|
|||||||
ServerType: "scum",
|
ServerType: "scum",
|
||||||
ManifestRef: "artifact://manifests/server.scum/1.0.0",
|
ManifestRef: "artifact://manifests/server.scum/1.0.0",
|
||||||
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
|
CreateFormSchemaRef: "artifact://schemas/server.scum/create-form/1.0.0",
|
||||||
RequiredRunCapabilities: []string{"process.start", domain.JobCapabilityRemoteRunDBSQLiteProbe},
|
RequiredRunCapabilities: []string{"process.start", domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunDBSQLiteProbe},
|
||||||
DeclaredPermissions: []string{"server.remote.access"},
|
DeclaredPermissions: []string{"server.remote.access"},
|
||||||
Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true},
|
Permissions: domain.PluginPermissions{Jobs: true, RemoteAccess: true},
|
||||||
LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"},
|
LifecycleActions: domain.PluginLifecycleActions{Start: "actions/start.json"},
|
||||||
RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
RemoteAccess: domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
RuntimeProfiles: domain.GamePluginRuntimeProfiles{
|
RuntimeProfiles: domain.GamePluginRuntimeProfiles{
|
||||||
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}},
|
LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}},
|
||||||
TransportProfiles: []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}},
|
TransportProfiles: []domain.RuntimeTransportProfile{
|
||||||
|
{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||||
|
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
|
},
|
||||||
|
DataTargets: []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}},
|
||||||
},
|
},
|
||||||
SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}},
|
SCUMLiveData: domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create plugin: %v", err)
|
t.Fatalf("create plugin: %v", err)
|
||||||
}
|
}
|
||||||
endpoint, err := core.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Capabilities: []string{"process.start", domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
endpoint, err := core.CreateRunEndpoint(domain.RunEndpoint{ID: "run-local", DisplayName: "Local Run", Version: "0.1.0", Capabilities: []string{"process.start", domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunDBSQLiteProbe}, Capacity: domain.RunCapacity{MaxJobs: 4}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create run endpoint: %v", err)
|
t.Fatalf("create run endpoint: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ type RunAutonomousLifecyclePlan struct {
|
|||||||
InstallPlans []RunAutonomousInstallPlan `json:"installPlans,omitempty"`
|
InstallPlans []RunAutonomousInstallPlan `json:"installPlans,omitempty"`
|
||||||
LogSources []RunAutonomousLogSource `json:"logSources,omitempty"`
|
LogSources []RunAutonomousLogSource `json:"logSources,omitempty"`
|
||||||
DLLExtensions []RunAutonomousDLLExtension `json:"dllExtensions,omitempty"`
|
DLLExtensions []RunAutonomousDLLExtension `json:"dllExtensions,omitempty"`
|
||||||
|
DataTargets []RunAutonomousDataTarget `json:"dataTargets,omitempty"`
|
||||||
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
|
||||||
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
|
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -208,6 +209,18 @@ type RunAutonomousDLLExtension struct {
|
|||||||
RCONPort int `json:"rconPort,omitempty"`
|
RCONPort int `json:"rconPort,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RunAutonomousDataTarget struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
TransportKey string `json:"transportKey"`
|
||||||
|
SourceRootKey string `json:"sourceRootKey"`
|
||||||
|
SourcePath string `json:"sourcePath"`
|
||||||
|
WorkspaceKey string `json:"workspaceKey"`
|
||||||
|
RefreshPolicy string `json:"refreshPolicy"`
|
||||||
|
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||||
|
Platforms []string `json:"platforms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RunAutonomousDeployment struct {
|
type RunAutonomousDeployment struct {
|
||||||
SchemaVersion string `json:"schemaVersion"`
|
SchemaVersion string `json:"schemaVersion"`
|
||||||
Mode ServerDeploymentMode `json:"mode"`
|
Mode ServerDeploymentMode `json:"mode"`
|
||||||
@@ -497,6 +510,10 @@ func CopyRunAutonomousLifecyclePlanPtr(plan *RunAutonomousLifecyclePlan) *RunAut
|
|||||||
}
|
}
|
||||||
copy.LogSources = append([]RunAutonomousLogSource(nil), plan.LogSources...)
|
copy.LogSources = append([]RunAutonomousLogSource(nil), plan.LogSources...)
|
||||||
copy.DLLExtensions = append([]RunAutonomousDLLExtension(nil), plan.DLLExtensions...)
|
copy.DLLExtensions = append([]RunAutonomousDLLExtension(nil), plan.DLLExtensions...)
|
||||||
|
copy.DataTargets = append([]RunAutonomousDataTarget(nil), plan.DataTargets...)
|
||||||
|
for i := range copy.DataTargets {
|
||||||
|
copy.DataTargets[i].Platforms = CopyStringSlice(plan.DataTargets[i].Platforms)
|
||||||
|
}
|
||||||
copy.RuntimeBindings = CopyStringMap(plan.RuntimeBindings)
|
copy.RuntimeBindings = CopyStringMap(plan.RuntimeBindings)
|
||||||
if plan.Deployment != nil {
|
if plan.Deployment != nil {
|
||||||
deployment := *plan.Deployment
|
deployment := *plan.Deployment
|
||||||
|
|||||||
@@ -479,6 +479,18 @@ type RuntimeTransportProfile struct {
|
|||||||
Capabilities []string
|
Capabilities []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeDataTarget struct {
|
||||||
|
Key string
|
||||||
|
Kind string
|
||||||
|
TransportKey string
|
||||||
|
SourceRootKey string
|
||||||
|
SourcePath string
|
||||||
|
WorkspaceKey string
|
||||||
|
RefreshPolicy string
|
||||||
|
MaxBytes int64
|
||||||
|
Platforms []string
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeClientManagerProfile struct {
|
type RuntimeClientManagerProfile struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
@@ -609,6 +621,7 @@ type GamePluginRuntimeProfiles struct {
|
|||||||
LogSources []RuntimeLogSource
|
LogSources []RuntimeLogSource
|
||||||
LogEvents []RuntimeLogEvent
|
LogEvents []RuntimeLogEvent
|
||||||
TransportProfiles []RuntimeTransportProfile
|
TransportProfiles []RuntimeTransportProfile
|
||||||
|
DataTargets []RuntimeDataTarget
|
||||||
ClientManagers []RuntimeClientManagerProfile
|
ClientManagers []RuntimeClientManagerProfile
|
||||||
DLLExtensions []RuntimeDLLExtensionProfile
|
DLLExtensions []RuntimeDLLExtensionProfile
|
||||||
}
|
}
|
||||||
@@ -1833,6 +1846,10 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
|||||||
for i := range profiles.TransportProfiles {
|
for i := range profiles.TransportProfiles {
|
||||||
profiles.TransportProfiles[i].Capabilities = CopyStringSlice(profiles.TransportProfiles[i].Capabilities)
|
profiles.TransportProfiles[i].Capabilities = CopyStringSlice(profiles.TransportProfiles[i].Capabilities)
|
||||||
}
|
}
|
||||||
|
profiles.DataTargets = append([]RuntimeDataTarget(nil), profiles.DataTargets...)
|
||||||
|
for i := range profiles.DataTargets {
|
||||||
|
profiles.DataTargets[i].Platforms = CopyStringSlice(profiles.DataTargets[i].Platforms)
|
||||||
|
}
|
||||||
profiles.ClientManagers = append([]RuntimeClientManagerProfile(nil), profiles.ClientManagers...)
|
profiles.ClientManagers = append([]RuntimeClientManagerProfile(nil), profiles.ClientManagers...)
|
||||||
for i := range profiles.ClientManagers {
|
for i := range profiles.ClientManagers {
|
||||||
profiles.ClientManagers[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.ClientManagers[i].SupportedTargets...)
|
profiles.ClientManagers[i].SupportedTargets = append([]RuntimeTarget(nil), profiles.ClientManagers[i].SupportedTargets...)
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ type RuntimeTransportProfileBody struct {
|
|||||||
Capabilities []string `json:"capabilities"`
|
Capabilities []string `json:"capabilities"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeDataTargetBody struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
TransportKey string `json:"transportKey"`
|
||||||
|
SourceRootKey string `json:"sourceRootKey"`
|
||||||
|
SourcePath string `json:"sourcePath"`
|
||||||
|
WorkspaceKey string `json:"workspaceKey"`
|
||||||
|
RefreshPolicy string `json:"refreshPolicy"`
|
||||||
|
MaxBytes int64 `json:"maxBytes,omitempty"`
|
||||||
|
Platforms []string `json:"platforms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type RuntimeRepositoryBody struct {
|
type RuntimeRepositoryBody struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
RevisionPolicy string `json:"revisionPolicy"`
|
RevisionPolicy string `json:"revisionPolicy"`
|
||||||
@@ -263,6 +275,7 @@ type GamePluginRuntimeProfilesBody struct {
|
|||||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||||
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
LogEvents []RuntimeLogEventBody `json:"logEvents,omitempty"`
|
||||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||||
|
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||||
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
ClientManagers []RuntimeClientManagerProfileBody `json:"clientManagers,omitempty"`
|
||||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -329,6 +342,9 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
|||||||
for _, item := range body.TransportProfiles {
|
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)})
|
profiles.TransportProfiles = append(profiles.TransportProfiles, domain.RuntimeTransportProfile{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: domain.CopyStringSlice(item.Capabilities)})
|
||||||
}
|
}
|
||||||
|
for _, item := range body.DataTargets {
|
||||||
|
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||||
|
}
|
||||||
for _, item := range body.ClientManagers {
|
for _, item := range body.ClientManagers {
|
||||||
manager := domain.RuntimeClientManagerProfile{
|
manager := domain.RuntimeClientManagerProfile{
|
||||||
Key: item.Key, DisplayName: item.DisplayName, Version: item.Version,
|
Key: item.Key, DisplayName: item.DisplayName, Version: item.Version,
|
||||||
|
|||||||
@@ -268,6 +268,11 @@ func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance do
|
|||||||
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, target := range plugin.RuntimeProfiles.DataTargets {
|
||||||
|
if runtimePlatformsContain(target.Platforms, distribution.TargetOS) {
|
||||||
|
plan.DataTargets = append(plan.DataTargets, autonomousDataTarget(target))
|
||||||
|
}
|
||||||
|
}
|
||||||
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
if hasProfile && len(profile.DLLExtensionRefs) > 0 {
|
||||||
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
endpoint := domain.RunEndpoint{ID: distribution.RunEndpointID, Platform: distribution.TargetOS, Architecture: distribution.TargetArch}
|
||||||
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
extensions, err := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint)
|
||||||
@@ -326,6 +331,10 @@ func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.Run
|
|||||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
||||||
|
return domain.RunAutonomousDataTarget{Key: target.Key, Kind: target.Kind, TransportKey: target.TransportKey, SourceRootKey: target.SourceRootKey, SourcePath: target.SourcePath, WorkspaceKey: target.WorkspaceKey, RefreshPolicy: target.RefreshPolicy, MaxBytes: target.MaxBytes, Platforms: domain.CopyStringSlice(target.Platforms)}
|
||||||
|
}
|
||||||
|
|
||||||
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
func autonomousDeploymentFromDefinition(definition domain.ServerDeploymentDefinition, profileKey string, bindings map[string]string) *domain.RunAutonomousDeployment {
|
||||||
if definition.Mode == "" {
|
if definition.Mode == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
|||||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||||
)
|
)
|
||||||
|
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||||
|
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||||
|
domain.RuntimeTransportProfile{Key: "world-db", Kind: "sqlite", TargetKey: "world-db", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
|
)
|
||||||
|
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets,
|
||||||
|
domain.RuntimeDataTarget{Key: "world-db", Kind: "sqlite.snapshot", TransportKey: "world-db", SourceRootKey: "server-root", SourcePath: "world/current.db", WorkspaceKey: "databases/world-db", RefreshPolicy: "on-demand-snapshot", MaxBytes: 128 * 1024 * 1024, Platforms: []string{"linux"}},
|
||||||
|
)
|
||||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||||
}
|
}
|
||||||
@@ -90,14 +97,14 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
|||||||
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
||||||
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
||||||
}
|
}
|
||||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || len(plan.DataTargets) != 1 || plan.DataTargets[0].WorkspaceKey != "databases/world-db" || plan.DataTargets[0].SourcePath != "world/current.db" || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||||
}
|
}
|
||||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||||
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
if err := json.Unmarshal([]byte(seedFiles[2].Content), &seededPlan); err != nil {
|
||||||
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
t.Fatalf("unmarshal seeded autonomous lifecycle plan: %v", err)
|
||||||
}
|
}
|
||||||
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey {
|
if seededPlan.ServerInstanceID != plan.ServerInstanceID || seededPlan.Bootstrap == nil || seededPlan.Bootstrap.TargetKey != plan.Bootstrap.TargetKey || len(seededPlan.DataTargets) != 1 || seededPlan.DataTargets[0].WorkspaceKey != "databases/world-db" {
|
||||||
t.Fatalf("seeded lifecycle plan differs from build input: seed=%+v input=%+v", seededPlan, plan)
|
t.Fatalf("seeded lifecycle plan differs from build input: seed=%+v input=%+v", seededPlan, plan)
|
||||||
}
|
}
|
||||||
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ func TestSCUMSchemaProbeDispatchIsPlatformScheduledAndFenced(t *testing.T) {
|
|||||||
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
|
||||||
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||||
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
|
plugin.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}, DatabaseEngines: []string{"sqlite"}}
|
||||||
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}})
|
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles,
|
||||||
|
domain.RuntimeTransportProfile{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||||
|
domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
|
)
|
||||||
|
plugin.RuntimeProfiles.DataTargets = append(plugin.RuntimeProfiles.DataTargets, domain.RuntimeDataTarget{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}})
|
||||||
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
|
plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys = append(plugin.RuntimeProfiles.LifecycleProfiles[0].TransportKeys, "scum-database")
|
||||||
plugin.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}}
|
plugin.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilitySchemaProbe, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "waiting for current service evidence"}}}
|
||||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ func (svc *CoreService) RequestSCUMSchemaProbeForSession(sessionID, serverInstan
|
|||||||
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
|
if plugin.SCUMLiveData.SchemaVersion == "" || probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe || strings.TrimSpace(probe.TargetKey) == "" {
|
||||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe is not declared by the plugin")
|
||||||
}
|
}
|
||||||
|
if !scumSchemaProbeHasDataTarget(plugin.RuntimeProfiles, probe.TargetKey) {
|
||||||
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, validationError("SCUM schema probe target is not declared as a generated Run data target")
|
||||||
|
}
|
||||||
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
return domain.SCUMSchemaProbeRequest{}, domain.RemoteAdapterResult{}, err
|
||||||
@@ -94,6 +97,16 @@ func scumSchemaProbeDatabaseIdentity(targetKey string) string {
|
|||||||
return strings.TrimPrefix(trimmed, "databases/")
|
return strings.TrimPrefix(trimmed, "databases/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scumSchemaProbeHasDataTarget(profiles domain.GamePluginRuntimeProfiles, targetKey string) bool {
|
||||||
|
expectedWorkspaceKey := "databases/" + scumSchemaProbeDatabaseIdentity(targetKey)
|
||||||
|
for _, target := range profiles.DataTargets {
|
||||||
|
if target.Key == targetKey && target.Kind == "sqlite.snapshot" && target.WorkspaceKey == expectedWorkspaceKey && target.RefreshPolicy == "on-demand-snapshot" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
func scumSchemaProbeAdapterVersion(manifest domain.SCUMLiveDataManifest) string {
|
||||||
for _, gate := range manifest.CapabilityGates {
|
for _, gate := range manifest.CapabilityGates {
|
||||||
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
if gate.Capability == domain.SCUMDataCapabilitySchemaProbe {
|
||||||
|
|||||||
@@ -298,9 +298,13 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermi
|
|||||||
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
||||||
registration := validGamePluginManifestRegistration()
|
registration := validGamePluginManifestRegistration()
|
||||||
registration.Manifest.ID = "game.scum"
|
registration.Manifest.ID = "game.scum"
|
||||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunFilesRead)
|
||||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
||||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}}
|
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{
|
||||||
|
{Key: "server-files", Kind: "file", TargetKey: "server-root", Capabilities: []string{domain.JobCapabilityRemoteRunFilesRead}},
|
||||||
|
{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}},
|
||||||
|
}
|
||||||
|
registration.Manifest.RuntimeProfiles.DataTargets = []domain.RuntimeDataTarget{{Key: "scum-database", Kind: "sqlite.snapshot", TransportKey: "scum-database", SourceRootKey: "server-root", SourcePath: "SCUM/Saved/SaveFiles/SCUM.db", WorkspaceKey: "databases/scum-database", RefreshPolicy: "on-demand-snapshot", MaxBytes: 1024 * 1024 * 1024, Platforms: []string{"windows"}}}
|
||||||
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
||||||
|
|
||||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
logSourceRetentions := map[string]int{}
|
logSourceRetentions := map[string]int{}
|
||||||
logEventKeys := map[string]struct{}{}
|
logEventKeys := map[string]struct{}{}
|
||||||
logEventTypes := map[string]struct{}{}
|
logEventTypes := map[string]struct{}{}
|
||||||
|
dataTargetKeys := map[string]struct{}{}
|
||||||
|
transportTargetKeys := map[string]struct{}{}
|
||||||
|
|
||||||
for i, probe := range profiles.Discovery {
|
for i, probe := range profiles.Discovery {
|
||||||
prefix := fmt.Sprintf("runtimeProfiles.discovery[%d]", i)
|
prefix := fmt.Sprintf("runtimeProfiles.discovery[%d]", i)
|
||||||
@@ -301,6 +303,7 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
}
|
}
|
||||||
if transport.TargetKey != "" {
|
if transport.TargetKey != "" {
|
||||||
violations = append(violations, validateProfileKey(prefix+".targetKey", transport.TargetKey)...)
|
violations = append(violations, validateProfileKey(prefix+".targetKey", transport.TargetKey)...)
|
||||||
|
transportTargetKeys[transport.TargetKey] = struct{}{}
|
||||||
}
|
}
|
||||||
if len(transport.Capabilities) == 0 {
|
if len(transport.Capabilities) == 0 {
|
||||||
violations = append(violations, prefix+".capabilities must not be empty")
|
violations = append(violations, prefix+".capabilities must not be empty")
|
||||||
@@ -312,6 +315,35 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
}
|
}
|
||||||
violations = append(violations, duplicateViolations(prefix+".capabilities", transport.Capabilities)...)
|
violations = append(violations, duplicateViolations(prefix+".capabilities", transport.Capabilities)...)
|
||||||
}
|
}
|
||||||
|
for i, target := range profiles.DataTargets {
|
||||||
|
prefix := fmt.Sprintf("runtimeProfiles.dataTargets[%d]", i)
|
||||||
|
violations = append(violations, validateProfileKey(prefix+".key", target.Key)...)
|
||||||
|
violations = append(violations, recordRuntimeProfileKey(dataTargetKeys, prefix+".key", target.Key)...)
|
||||||
|
if target.Kind != "sqlite.snapshot" {
|
||||||
|
violations = append(violations, prefix+".kind is invalid")
|
||||||
|
}
|
||||||
|
violations = append(violations, validateProfileKey(prefix+".transportKey", target.TransportKey)...)
|
||||||
|
transport, transportExists := transportByKey(profiles.TransportProfiles, target.TransportKey)
|
||||||
|
if !transportExists || transport.Kind != "sqlite" {
|
||||||
|
violations = append(violations, prefix+".transportKey must reference a declared sqlite transport")
|
||||||
|
}
|
||||||
|
violations = append(violations, validateProfileKey(prefix+".sourceRootKey", target.SourceRootKey)...)
|
||||||
|
if _, exists := transportTargetKeys[target.SourceRootKey]; !exists {
|
||||||
|
violations = append(violations, prefix+".sourceRootKey must reference a declared runtime transport target")
|
||||||
|
}
|
||||||
|
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".sourcePath", target.SourcePath)...)
|
||||||
|
violations = append(violations, validateSafeRelativeRuntimePath(prefix+".workspaceKey", target.WorkspaceKey)...)
|
||||||
|
if !strings.HasPrefix(target.WorkspaceKey, "databases/") {
|
||||||
|
violations = append(violations, prefix+".workspaceKey must live under databases/")
|
||||||
|
}
|
||||||
|
if target.RefreshPolicy != "on-demand-snapshot" {
|
||||||
|
violations = append(violations, prefix+".refreshPolicy is invalid")
|
||||||
|
}
|
||||||
|
if target.MaxBytes < 1 || target.MaxBytes > 1024*1024*1024 {
|
||||||
|
violations = append(violations, prefix+".maxBytes is invalid")
|
||||||
|
}
|
||||||
|
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", target.Platforms)...)
|
||||||
|
}
|
||||||
for i, manager := range profiles.ClientManagers {
|
for i, manager := range profiles.ClientManagers {
|
||||||
prefix := fmt.Sprintf("runtimeProfiles.clientManagers[%d]", i)
|
prefix := fmt.Sprintf("runtimeProfiles.clientManagers[%d]", i)
|
||||||
violations = append(violations, validateProfileKey(prefix+".key", manager.Key)...)
|
violations = append(violations, validateProfileKey(prefix+".key", manager.Key)...)
|
||||||
@@ -656,6 +688,15 @@ func recordRuntimeProfileKey(seen map[string]struct{}, field, key string) []stri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transportByKey(transports []domain.RuntimeTransportProfile, key string) (domain.RuntimeTransportProfile, bool) {
|
||||||
|
for _, transport := range transports {
|
||||||
|
if transport.Key == key {
|
||||||
|
return transport, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domain.RuntimeTransportProfile{}, false
|
||||||
|
}
|
||||||
|
|
||||||
func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRuntimeProfiles, declared []string) []string {
|
func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRuntimeProfiles, declared []string) []string {
|
||||||
declaredSet := map[string]struct{}{}
|
declaredSet := map[string]struct{}{}
|
||||||
for _, capability := range declared {
|
for _, capability := range declared {
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManife
|
|||||||
violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability")
|
violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability")
|
||||||
}
|
}
|
||||||
probeTransportFound := false
|
probeTransportFound := false
|
||||||
|
probeDataTargetFound := false
|
||||||
|
expectedWorkspaceKey := "databases/" + strings.TrimPrefix(value.Probe.TargetKey, "databases/")
|
||||||
for _, transport := range runtimeProfiles.TransportProfiles {
|
for _, transport := range runtimeProfiles.TransportProfiles {
|
||||||
if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey {
|
if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey {
|
||||||
continue
|
continue
|
||||||
@@ -107,6 +109,19 @@ func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManife
|
|||||||
if !probeTransportFound {
|
if !probeTransportFound {
|
||||||
violations = append(violations, prefix+".probe.targetKey must reference a declared transport")
|
violations = append(violations, prefix+".probe.targetKey must reference a declared transport")
|
||||||
}
|
}
|
||||||
|
for _, target := range runtimeProfiles.DataTargets {
|
||||||
|
if target.Key != value.Probe.TargetKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
probeDataTargetFound = true
|
||||||
|
transport, ok := transportByKey(runtimeProfiles.TransportProfiles, target.TransportKey)
|
||||||
|
if target.Kind != "sqlite.snapshot" || target.WorkspaceKey != expectedWorkspaceKey || !ok || transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) {
|
||||||
|
violations = append(violations, prefix+".probe.targetKey must reference a sqlite snapshot data target for the generated Run workspace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !probeDataTargetFound {
|
||||||
|
violations = append(violations, prefix+".probe.targetKey must reference a declared runtime data target")
|
||||||
|
}
|
||||||
violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...)
|
violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...)
|
||||||
seen := map[domain.SCUMDataCapability]struct{}{}
|
seen := map[domain.SCUMDataCapability]struct{}{}
|
||||||
for i, gate := range value.CapabilityGates {
|
for i, gate := range value.CapabilityGates {
|
||||||
|
|||||||
@@ -925,6 +925,21 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"dataTargets": [
|
||||||
|
{
|
||||||
|
"key": "scum-database",
|
||||||
|
"kind": "sqlite.snapshot",
|
||||||
|
"transportKey": "scum-database",
|
||||||
|
"sourceRootKey": "server-root",
|
||||||
|
"sourcePath": "SCUM/Saved/SaveFiles/SCUM.db",
|
||||||
|
"workspaceKey": "databases/scum-database",
|
||||||
|
"refreshPolicy": "on-demand-snapshot",
|
||||||
|
"maxBytes": 1073741824,
|
||||||
|
"platforms": [
|
||||||
|
"windows"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"clientManagers": [
|
"clientManagers": [
|
||||||
{
|
{
|
||||||
"key": "scum-client-manager",
|
"key": "scum-client-manager",
|
||||||
|
|||||||
@@ -133,6 +133,12 @@
|
|||||||
"items": { "$ref": "#/$defs/runtimeTransportProfile" },
|
"items": { "$ref": "#/$defs/runtimeTransportProfile" },
|
||||||
"uniqueItems": true
|
"uniqueItems": true
|
||||||
},
|
},
|
||||||
|
"dataTargets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/$defs/runtimeDataTarget" },
|
||||||
|
"uniqueItems": true,
|
||||||
|
"maxItems": 64
|
||||||
|
},
|
||||||
"clientManagers": {
|
"clientManagers": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "$ref": "#/$defs/runtimeClientManagerProfile" },
|
"items": { "$ref": "#/$defs/runtimeClientManagerProfile" },
|
||||||
@@ -838,6 +844,22 @@
|
|||||||
"capabilities": { "type": "array", "items": { "$ref": "#/$defs/runCapability" }, "uniqueItems": true, "minItems": 1 }
|
"capabilities": { "type": "array", "items": { "$ref": "#/$defs/runCapability" }, "uniqueItems": true, "minItems": 1 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"runtimeDataTarget": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "kind", "transportKey", "sourceRootKey", "sourcePath", "workspaceKey", "refreshPolicy", "maxBytes"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"key": { "$ref": "#/$defs/logicalKey" },
|
||||||
|
"kind": { "const": "sqlite.snapshot" },
|
||||||
|
"transportKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
|
"sourceRootKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
|
"sourcePath": { "$ref": "#/$defs/relativePathRef" },
|
||||||
|
"workspaceKey": { "type": "string", "pattern": "^databases/[a-z0-9][a-z0-9._/-]{0,119}$" },
|
||||||
|
"refreshPolicy": { "const": "on-demand-snapshot" },
|
||||||
|
"maxBytes": { "type": "integer", "minimum": 1, "maximum": 1073741824 },
|
||||||
|
"platforms": { "type": "array", "items": { "enum": ["windows", "linux", "darwin"] }, "uniqueItems": true }
|
||||||
|
}
|
||||||
|
},
|
||||||
"relativePathRef": {
|
"relativePathRef": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*://)(?!.*\\.\\.)[a-zA-Z0-9_./-]+$",
|
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*://)(?!.*\\.\\.)[a-zA-Z0-9_./-]+$",
|
||||||
|
|||||||
@@ -1064,6 +1064,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
|||||||
type Gate = { capability?: string; gate?: string; adapterVersion?: string; requiredSchemaFingerprint?: string; requiredAssetDigests?: string[]; evidenceStatus?: string; safeReason?: string };
|
type Gate = { capability?: string; gate?: string; adapterVersion?: string; requiredSchemaFingerprint?: string; requiredAssetDigests?: string[]; evidenceStatus?: string; safeReason?: string };
|
||||||
type Probe = { capability?: string; targetKey?: string; bounds?: { maxSampleRows?: number; timeoutMs?: number; maxResultBytes?: number } };
|
type Probe = { capability?: string; targetKey?: string; bounds?: { maxSampleRows?: number; timeoutMs?: number; maxResultBytes?: number } };
|
||||||
type Transport = { key?: string; kind?: string; targetKey?: string; capabilities?: string[] };
|
type Transport = { key?: string; kind?: string; targetKey?: string; capabilities?: string[] };
|
||||||
|
type DataTarget = { key?: string; kind?: string; transportKey?: string; sourceRootKey?: string; sourcePath?: string; workspaceKey?: string; refreshPolicy?: string; maxBytes?: number; platforms?: string[] };
|
||||||
type RuntimeLogSource = { key?: string };
|
type RuntimeLogSource = { key?: string };
|
||||||
type AssetFile = { path?: string };
|
type AssetFile = { path?: string };
|
||||||
type Asset = { key?: string; adapterVersion?: string; assetPath?: string; digest?: string };
|
type Asset = { key?: string; adapterVersion?: string; assetPath?: string; digest?: string };
|
||||||
@@ -1075,7 +1076,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
|||||||
type MapAsset = Asset & { requiredSchemaFingerprint?: string; metadataSchemaRef?: string; transformAssetPath?: string; transformDigest?: string; worldBounds?: { minX?: number; minY?: number; maxX?: number; maxY?: number }; image?: { width?: number; height?: number } };
|
type MapAsset = Asset & { requiredSchemaFingerprint?: string; metadataSchemaRef?: string; transformAssetPath?: string; transformDigest?: string; worldBounds?: { minX?: number; minY?: number; maxX?: number; maxY?: number }; image?: { width?: number; height?: number } };
|
||||||
type GiftCatalog = Asset & { catalogVersion?: string; itemSchemaRef?: string; transportTemplateKeys?: string[] };
|
type GiftCatalog = Asset & { catalogVersion?: string; itemSchemaRef?: string; transportTemplateKeys?: string[] };
|
||||||
type LiveData = { schemaVersion?: string; probe?: Probe; capabilityGates?: Gate[]; logParsers?: LogParser[]; sqliteQueries?: SQLiteQuery[]; syncCadences?: SyncCadence[]; typedRconTemplates?: TypedRCON[]; guardedMutations?: GuardedMutation[]; mapAssets?: MapAsset[]; giftCatalogs?: GiftCatalog[] };
|
type LiveData = { schemaVersion?: string; probe?: Probe; capabilityGates?: Gate[]; logParsers?: LogParser[]; sqliteQueries?: SQLiteQuery[]; syncCadences?: SyncCadence[]; typedRconTemplates?: TypedRCON[]; guardedMutations?: GuardedMutation[]; mapAssets?: MapAsset[]; giftCatalogs?: GiftCatalog[] };
|
||||||
const declaration = manifest as { id?: string; capabilities?: string[]; permissions?: string[]; assetFiles?: AssetFile[]; remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[]; logSources?: RuntimeLogSource[] }; scumLiveData?: LiveData };
|
const declaration = manifest as { id?: string; capabilities?: string[]; permissions?: string[]; assetFiles?: AssetFile[]; remoteAccess?: { runCapabilities?: string[]; databaseEngines?: string[] }; runtimeProfiles?: { transportProfiles?: Transport[]; dataTargets?: DataTarget[]; logSources?: RuntimeLogSource[] }; scumLiveData?: LiveData };
|
||||||
const liveData = declaration.scumLiveData;
|
const liveData = declaration.scumLiveData;
|
||||||
if (!liveData) return [];
|
if (!liveData) return [];
|
||||||
|
|
||||||
@@ -1085,6 +1086,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
|||||||
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
const remoteCapabilities = new Set(declaration.remoteAccess?.runCapabilities ?? []);
|
||||||
const remoteDatabaseEngines = new Set(declaration.remoteAccess?.databaseEngines ?? []);
|
const remoteDatabaseEngines = new Set(declaration.remoteAccess?.databaseEngines ?? []);
|
||||||
const transportProfiles = declaration.runtimeProfiles?.transportProfiles ?? [];
|
const transportProfiles = declaration.runtimeProfiles?.transportProfiles ?? [];
|
||||||
|
const dataTargets = declaration.runtimeProfiles?.dataTargets ?? [];
|
||||||
const logSources = new Set((declaration.runtimeProfiles?.logSources ?? []).map((source) => source.key ?? ""));
|
const logSources = new Set((declaration.runtimeProfiles?.logSources ?? []).map((source) => source.key ?? ""));
|
||||||
const assetFiles = new Set((declaration.assetFiles ?? []).map((asset) => asset.path ?? ""));
|
const assetFiles = new Set((declaration.assetFiles ?? []).map((asset) => asset.path ?? ""));
|
||||||
const probe = liveData.probe;
|
const probe = liveData.probe;
|
||||||
@@ -1157,6 +1159,17 @@ export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: st
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const transportByKey = (key?: string): Transport | undefined => transportProfiles.find((candidate) => candidate.key === key);
|
const transportByKey = (key?: string): Transport | undefined => transportProfiles.find((candidate) => candidate.key === key);
|
||||||
|
const probeDataTarget = dataTargets.find((candidate) => candidate.key === probe?.targetKey);
|
||||||
|
const expectedProbeWorkspaceKey = `databases/${String(probe?.targetKey ?? "").replace(/^databases\//, "")}`;
|
||||||
|
if (!probeDataTarget) {
|
||||||
|
errors.push(`${location}.probe.targetKey: must reference a declared runtime data target`);
|
||||||
|
} else {
|
||||||
|
const probeDataTargetTransport = transportByKey(probeDataTarget.transportKey);
|
||||||
|
if (probeDataTarget.kind !== "sqlite.snapshot" || probeDataTarget.workspaceKey !== expectedProbeWorkspaceKey || probeDataTarget.refreshPolicy !== "on-demand-snapshot" || !probeDataTargetTransport || probeDataTargetTransport.kind !== "sqlite" || !probeDataTargetTransport.capabilities?.includes("remote.run.db.sqlite.probe")) {
|
||||||
|
errors.push(`${location}.probe.targetKey: must reference a sqlite snapshot data target for the generated Run workspace`);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(probeDataTarget.maxBytes) || (probeDataTarget.maxBytes ?? 0) < 1 || (probeDataTarget.maxBytes ?? 0) > 1073741824) errors.push(`${location}.probe.targetKey.maxBytes: must be between 1 and 1073741824`);
|
||||||
|
}
|
||||||
const requireRefs = (entry: Record<string, unknown>, entryLocation: string, refs: string[]): void => {
|
const requireRefs = (entry: Record<string, unknown>, entryLocation: string, refs: string[]): void => {
|
||||||
for (const field of refs) {
|
for (const field of refs) {
|
||||||
const ref = entry[field];
|
const ref = entry[field];
|
||||||
|
|||||||
+14
-1
@@ -655,11 +655,23 @@ export interface RuntimeLogEventDeclaration {
|
|||||||
|
|
||||||
export interface RuntimeTransportProfile {
|
export interface RuntimeTransportProfile {
|
||||||
key: string;
|
key: string;
|
||||||
kind: "file" | "ftp" | "rsync" | "mysql" | "sqlite" | "rcon";
|
kind: "file" | "ftp" | "rsync" | "mysql" | "sqlite" | "rcon" | "program";
|
||||||
targetKey?: string;
|
targetKey?: string;
|
||||||
capabilities: RunCapability[];
|
capabilities: RunCapability[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuntimeDataTargetDeclaration {
|
||||||
|
key: string;
|
||||||
|
kind: "sqlite.snapshot";
|
||||||
|
transportKey: string;
|
||||||
|
sourceRootKey: string;
|
||||||
|
sourcePath: string;
|
||||||
|
workspaceKey: string;
|
||||||
|
refreshPolicy: "on-demand-snapshot";
|
||||||
|
maxBytes: number;
|
||||||
|
platforms?: RuntimePlatform[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RuntimeClientManagerProfile {
|
export interface RuntimeClientManagerProfile {
|
||||||
key: string;
|
key: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
@@ -739,6 +751,7 @@ export interface GamePluginRuntimeProfiles {
|
|||||||
logSources?: RuntimeLogSource[];
|
logSources?: RuntimeLogSource[];
|
||||||
logEvents?: RuntimeLogEventDeclaration[];
|
logEvents?: RuntimeLogEventDeclaration[];
|
||||||
transportProfiles?: RuntimeTransportProfile[];
|
transportProfiles?: RuntimeTransportProfile[];
|
||||||
|
dataTargets?: RuntimeDataTargetDeclaration[];
|
||||||
clientManagers?: RuntimeClientManagerProfile[];
|
clientManagers?: RuntimeClientManagerProfile[];
|
||||||
dllExtensions?: RuntimeDLLExtensionProfile[];
|
dllExtensions?: RuntimeDLLExtensionProfile[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,11 +212,19 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe");
|
expect(manifest.capabilities).toContain("remote.run.db.sqlite.probe");
|
||||||
expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe");
|
expect(manifest.remoteAccess.runCapabilities).toContain("remote.run.db.sqlite.probe");
|
||||||
expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" });
|
expect(manifest.scumLiveData.probe).toMatchObject({ capability: "remote.run.db.sqlite.probe", targetKey: "scum-database" });
|
||||||
|
expect(manifest.runtimeProfiles?.dataTargets?.find((target) => target.key === "scum-database")).toMatchObject({ kind: "sqlite.snapshot", transportKey: "scum-database", sourceRootKey: "server-root", sourcePath: "SCUM/Saved/SaveFiles/SCUM.db", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" });
|
||||||
expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
|
expect(manifest.scumLiveData.capabilityGates.map((gate) => gate.capability)).toEqual(expect.arrayContaining(["players.read", "squads.read", "vehicles.read", "flags.read", "positions.read", "profile-xml.write", "economy-command.write", "gift-command.write"]));
|
||||||
expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true);
|
expect(manifest.scumLiveData.capabilityGates.every((gate) => gate.gate === "disabled" && gate.evidenceStatus === "missing")).toBe(true);
|
||||||
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
|
expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires SCUM schema probe targets to have generated Run workspace data targets", () => {
|
||||||
|
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||||
|
manifest.runtimeProfiles.dataTargets = [];
|
||||||
|
});
|
||||||
|
expect(errors.some((error) => error.includes("probe.targetKey") && error.includes("runtime data target"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => {
|
it("rejects enabling SCUM live-data gates without compatible evidence and immutable digests", () => {
|
||||||
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||||
manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" };
|
manifest.scumLiveData.capabilityGates[1] = { ...manifest.scumLiveData.capabilityGates[1], gate: "enabled", evidenceStatus: "missing" };
|
||||||
@@ -338,6 +346,7 @@ describe("plugin manifest validation", () => {
|
|||||||
runtimeProfiles?: {
|
runtimeProfiles?: {
|
||||||
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
|
lifecycleProfiles?: Array<{ key: string; capabilities?: string[]; transportKeys?: string[] }>;
|
||||||
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
|
transportProfiles?: Array<{ key?: string; kind?: string; capabilities?: string[] }>;
|
||||||
|
dataTargets?: Array<Record<string, unknown>>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
|
const local = manifest.runtimeProfiles?.lifecycleProfiles?.find((profile) => profile.key === "run-local");
|
||||||
@@ -348,6 +357,9 @@ describe("plugin manifest validation", () => {
|
|||||||
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.protected.rcon"] }),
|
expect.objectContaining({ key: "scum-management", kind: "rcon", capabilities: ["remote.run.protected.rcon"] }),
|
||||||
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
expect.objectContaining({ key: "scum-program", kind: "program", capabilities: ["remote.run.program.command"] })
|
||||||
]));
|
]));
|
||||||
|
expect(manifest.runtimeProfiles?.dataTargets).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ key: "scum-database", kind: "sqlite.snapshot", workspaceKey: "databases/scum-database", refreshPolicy: "on-demand-snapshot" })
|
||||||
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defines a generated SCUM companion config without inline proof or session material", () => {
|
it("defines a generated SCUM companion config without inline proof or session material", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user