功能修改

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
+219 -3
View File
@@ -757,7 +757,7 @@ func TestCoreServiceManagesAIProviderMetadata(t *testing.T) {
if err != nil {
t.Fatalf("test enabled provider: %v", err)
}
if !testResult.Success || testResult.Mode != "metadata" {
if !testResult.Success || testResult.Mode != "provider" {
t.Fatalf("expected metadata test success, got %+v", testResult)
}
@@ -1022,6 +1022,147 @@ func TestCoreServiceRemoteAccessRequiresPluginDeclaration(t *testing.T) {
}
}
func TestCoreServiceDispatchesDeclaredSQLiteQueryTemplate(t *testing.T) {
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
queued, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
RequestID: "query-template-dispatch-1",
PluginID: plugin.ID,
RouteKey: "remote",
ServerInstanceID: instance.ID,
Action: domain.PluginBridgeActionRemoteAccessRequest,
Payload: map[string]string{
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
"declarationKey": "scum-db-read",
"targetKey": "scum-db.player-lookup",
"idempotencyKey": "query-template-dispatch-1",
"input.templateKey": "players.by-id",
"input.playerId": "steam-123",
"input.maxRows": "100",
},
})
if err != nil {
t.Fatalf("execute declared sqlite query template: %v", err)
}
if queued.Status != "queued" || queued.Result["jobId"] == "" {
t.Fatalf("expected queued query template job, got %+v", queued)
}
job, err := svc.store.Jobs().Get(queued.Result["jobId"])
if err != nil {
t.Fatalf("get query template job: %v", err)
}
if job.ExecutionInput.TimeoutSeconds != 20 {
t.Fatalf("expected template timeout 20, got %+v", job.ExecutionInput)
}
if job.ExecutionInput.Inputs["templateKey"] != "players.by-id" || job.ExecutionInput.Inputs["playerId"] != "steam-123" || job.ExecutionInput.Inputs["maxRows"] != "25" {
t.Fatalf("expected typed bounded query template inputs, got %#v", job.ExecutionInput.Inputs)
}
}
func TestCoreServiceDeniesUndeclaredOrMismatchedSQLiteQueryTemplateBeforeJob(t *testing.T) {
tests := []struct {
name string
templateKey string
declarationKey string
targetKey string
}{
{name: "undeclared template", templateKey: "players.unknown", declarationKey: "scum-db-read", targetKey: "scum-db.player-lookup"},
{name: "mismatched transport", templateKey: "players.by-id", declarationKey: "other-transport", targetKey: "scum-db.player-lookup"},
{name: "mismatched target", templateKey: "players.by-id", declarationKey: "scum-db-read", targetKey: "scum-db.other"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
RequestID: "query-template-denied-1",
PluginID: plugin.ID,
RouteKey: "remote",
ServerInstanceID: instance.ID,
Action: domain.PluginBridgeActionRemoteAccessRequest,
Payload: map[string]string{
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
"declarationKey": test.declarationKey,
"targetKey": test.targetKey,
"idempotencyKey": "query-template-denied-1",
"input.templateKey": test.templateKey,
"input.playerId": "steam-123",
},
})
if err != nil {
t.Fatalf("execute denied sqlite query template: %v", err)
}
if result.Status != "denied" || result.Error == nil || result.Error.Code != "query_template_denied" {
t.Fatalf("expected query template denial, got %+v", result)
}
jobs, err := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil {
t.Fatalf("list jobs after denial: %v", err)
}
if len(jobs) != 0 {
t.Fatalf("query template denial created jobs: %+v", jobs)
}
})
}
}
func TestFindBridgeQueryTemplateRequiresPagePermissionAndRemoteAction(t *testing.T) {
_, plugin, _, _, _ := createSQLiteQueryBridgeFixture(t)
plugin.GameClientBridge.QueryTemplates[0].Permission = "server.game-client.read"
for index := range plugin.Pages {
if plugin.Pages[index].Key == "remote" {
plugin.Pages[index].Permissions = []string{"server.remote.access"}
}
}
if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "permission") {
t.Fatalf("expected query template permission denial, got %q", reason)
}
for index := range plugin.Pages {
if plugin.Pages[index].Key == "remote" {
plugin.Pages[index].Permissions = []string{"server.remote.access"}
plugin.Pages[index].BridgeActions = nil
}
}
plugin.GameClientBridge.QueryTemplates[0].Permission = "server.remote.access"
if _, reason := findBridgeQueryTemplate(plugin, "remote", "players.by-id"); !strings.Contains(reason, "remote access") {
t.Fatalf("expected query template remote access denial, got %q", reason)
}
}
func TestCoreServiceRejectsArbitrarySQLBridgeInputBeforeJob(t *testing.T) {
svc, plugin, _, session, instance := createSQLiteQueryBridgeFixture(t)
result, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
RequestID: "query-template-sql-rejected-1",
PluginID: plugin.ID,
RouteKey: "remote",
ServerInstanceID: instance.ID,
Action: domain.PluginBridgeActionRemoteAccessRequest,
Payload: map[string]string{
"capability": domain.JobCapabilityRemoteRunDBSQLiteQuery,
"declarationKey": "scum-db-read",
"targetKey": "scum-db.player-lookup",
"idempotencyKey": "query-template-sql-rejected-1",
"input.templateKey": "players.by-id",
"input.sqlText": "SELECT * FROM users",
},
})
if err != nil {
t.Fatalf("execute arbitrary SQL bridge input: %v", err)
}
if result.Status != "error" || result.Error == nil || !strings.Contains(strings.ToLower(result.Error.Message), "unsafe") {
t.Fatalf("expected arbitrary SQL input rejection, got %+v", result)
}
jobs, listErr := svc.ListJobs(domain.JobFilter{ServerInstanceID: instance.ID})
if listErr != nil {
t.Fatalf("list jobs after arbitrary SQL rejection: %v", listErr)
}
if len(jobs) != 0 {
t.Fatalf("arbitrary SQL rejection created jobs: %+v", jobs)
}
}
func TestCoreServiceRejectsDuplicateGamePluginManifest(t *testing.T) {
svc := newTestCoreService()
registration := validPluginManifestRegistration()
@@ -1212,6 +1353,80 @@ func createPluginAndRunEndpoint(t *testing.T, svc *CoreService) (domain.GamePlug
return plugin, endpoint
}
func createSQLiteQueryBridgeFixture(t *testing.T) (*CoreService, domain.GamePlugin, domain.RunEndpoint, string, domain.ServerInstance) {
t.Helper()
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
capability := domain.JobCapabilityRemoteRunDBSQLiteQuery
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, capability)
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.remote.access")
plugin.Permissions.RemoteAccess = true
plugin.BridgeActions = append(plugin.BridgeActions, string(domain.PluginBridgeActionRemoteAccessRequest))
plugin.Pages = append(plugin.Pages, domain.GamePluginPage{
Key: "remote",
Title: "Remote",
Path: "/remote",
Permissions: []string{"server.remote.access"},
BridgeActions: []string{string(domain.PluginBridgeActionRemoteAccessRequest)},
})
plugin.RemoteAccess = domain.GamePluginRemoteAccess{
Methods: []string{"run"},
RunCapabilities: []string{capability},
DatabaseEngines: []string{"sqlite"},
}
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{
Key: "scum-db-read",
Kind: "sqlite",
TargetKey: "scum-db.player-lookup",
Capabilities: []string{capability},
})
plugin.GameClientBridge = domain.GameClientBridgeManifest{
QueryTemplates: []domain.GameClientBridgeQueryTemplateDeclaration{
{
Key: "players.by-id",
Title: "Player lookup",
Permission: "server.remote.access",
Engine: "sqlite",
TransportKey: "scum-db-read",
TargetKey: "scum-db.player-lookup",
ParameterSchemaRef: "schemas/queries/players.by-id.parameters.schema.json",
ResultSchemaRef: "schemas/queries/players.by-id.result.schema.json",
MaxRows: 25,
TimeoutSeconds: 20,
},
},
Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100},
Pages: []domain.GameClientBridgePageContract{
{PageKey: "remote", QueryTemplateKeys: []string{"players.by-id"}},
},
}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update sqlite query plugin fixture: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, capability)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update sqlite query endpoint fixture: %v", err)
}
session := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-query-owner",
DisplayName: "Query Owner",
Email: "query-owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
ID: "server-query-template",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Query Template Server",
State: domain.ServerInstanceStateRunning,
})
if err != nil {
t.Fatalf("create sqlite query server fixture: %v", err)
}
return svc, plugin, endpoint, session, instance
}
func createCompleteRuntimeBinding(t *testing.T, svc *CoreService, instance domain.ServerInstance, profileKey string) domain.RuntimeBinding {
t.Helper()
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
@@ -1269,8 +1484,9 @@ func validPluginManifestRegistration() domain.GamePluginManifestRegistration {
BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)},
},
},
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
ProductionLifecycle: domain.GamePluginProductionLifecycle{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
},
}
}