Remove pre-1.0 production governance surfaces

This commit is contained in:
npc0-hue
2026-08-21 00:06:00 +08:00
parent a7e2e4c6c0
commit da6c8d607e
30 changed files with 89 additions and 429 deletions
+149
View File
@@ -0,0 +1,149 @@
package service
import (
"errors"
"strings"
"testing"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
func TestPluginLifecycleDispatchIsIdempotentAndRejectsInputDrift(t *testing.T) {
svc, session, instance := newPluginOperationsFixture(t)
request := domain.PluginLifecycleRequest{PluginID: instance.PluginID, ServerInstanceID: instance.ID, Operation: domain.PluginLifecycleOperationInstall, TargetVersion: "1.0.0", IdempotencyKey: "plugin-install-v1"}
first, err := svc.RunPluginLifecycleForSession(session, request)
if err != nil {
t.Fatalf("dispatch plugin install: %v", err)
}
second, err := svc.RunPluginLifecycleForSession(session, request)
if err != nil {
t.Fatalf("repeat plugin install: %v", err)
}
if first.Job.ID == "" || second.Job.ID != first.Job.ID {
t.Fatalf("expected one idempotent job, got first=%+v second=%+v", first.Job, second.Job)
}
drift := request
drift.TargetVersion = "1.1.0"
if _, err := svc.RunPluginLifecycleForSession(session, drift); err == nil || !strings.Contains(err.Error(), "idempotencyKey") {
t.Fatalf("expected immutable input conflict, got %v", err)
}
jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if err != nil || len(jobs) != 1 {
t.Fatalf("expected exactly one lifecycle job, got %+v err=%v", jobs, err)
}
}
func TestPluginLifecycleBridgeDispatchesBoundedJob(t *testing.T) {
svc, session, instance := newPluginOperationsFixture(t)
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
t.Fatalf("get plugin: %v", err)
}
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.lifecycle")
plugin.BridgeActions = append(plugin.BridgeActions, string(domain.PluginBridgeActionPluginLifecycle))
plugin.Pages = append(plugin.Pages, domain.GamePluginPage{
Key: "operations", Title: "Operations", Path: "/operations",
Permissions: []string{"server.lifecycle"}, BridgeActions: []string{string(domain.PluginBridgeActionPluginLifecycle)},
})
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin bridge declaration: %v", err)
}
response, err := svc.ExecutePluginBridgeAction(session, domain.PluginBridgeExecuteRequest{
RequestID: "bridge-plugin-install", PluginID: plugin.ID, RouteKey: "operations",
ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionPluginLifecycle,
Payload: map[string]string{"operation": "install", "targetVersion": plugin.Version, "idempotencyKey": "bridge-plugin-install-v1", "confirmed": "false"},
})
if err != nil {
t.Fatalf("execute lifecycle bridge: %v", err)
}
if response.Status != "queued" || response.Result["jobId"] == "" || response.Result["installationId"] == "" {
t.Fatalf("expected plugin lifecycle job, got %+v", response)
}
serialized := strings.ToLower(strings.Join([]string{
response.Result["jobId"], response.Result["installationId"], response.Result["currentState"],
response.Result["desiredState"],
}, " "))
for _, forbidden := range []string{"password", "apikey", "token", "secret://", "baseurl", "hostpath", "socket", "pid", "dsn", "rcon", "runendpoint"} {
if strings.Contains(serialized, forbidden) {
t.Fatalf("bridge lifecycle result exposed forbidden fragment %q: %s", forbidden, serialized)
}
}
}
func TestAIConfigRecommendationRequiresApprovalAndRejectsStaleRevision(t *testing.T) {
svc, session, instance := newPluginOperationsFixture(t)
provider, err := svc.CreateAIProvider(domain.AIProvider{ID: "ai-local", Name: "Local AI", Kind: domain.AIProviderKindOllama, BaseURL: "http://127.0.0.1:11434/v1", Models: []string{"test-model"}, DefaultModel: "test-model", RelayMode: domain.AIRelayModeLocal, TimeoutMS: 1000, Status: domain.AIProviderStatusActive, RedactionPolicy: "strict"})
if err != nil {
t.Fatalf("create provider: %v", err)
}
response, err := svc.InvokeAIForSession(session, domain.AIInvocationRequest{RequestID: "ai-config-1", ServerInstanceID: instance.ID, ProviderID: provider.ID, Purpose: "config.suggest", Prompt: "disable pvp"})
if err != nil {
t.Fatalf("invoke AI: %v", err)
}
if response.ConfigRecommendation == nil || response.ConfigRecommendation.DiffID == "" {
t.Fatalf("expected persisted config recommendation, got %+v", response)
}
jobs, _ := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if len(jobs) != 0 {
t.Fatalf("AI recommendation must not dispatch before approval: %+v", jobs)
}
approved, err := svc.ApproveAIConfigDiffForSession(session, domain.AIConfigDiffApprovalRequest{DiffID: response.ConfigRecommendation.DiffID, IdempotencyKey: "approve-ai-config-1"})
if err != nil {
t.Fatalf("approve AI diff: %v", err)
}
if approved.Preview.State != domain.AIConfigDiffStateApproved || approved.Dispatch.Job.ID == "" {
t.Fatalf("expected approved diff and queued job, got %+v", approved)
}
repeated, err := svc.ApproveAIConfigDiffForSession(session, domain.AIConfigDiffApprovalRequest{DiffID: response.ConfigRecommendation.DiffID, IdempotencyKey: "approve-ai-config-1"})
if err != nil || repeated.Dispatch.Job.ID != approved.Dispatch.Job.ID {
t.Fatalf("repeat approval must return original job: %+v err=%v", repeated, err)
}
jobs, _ = svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID})
if len(jobs) != 1 {
t.Fatalf("approval must dispatch exactly one job, got %+v", jobs)
}
staleResponse, err := svc.InvokeAIForSession(session, domain.AIInvocationRequest{RequestID: "ai-config-stale", ServerInstanceID: instance.ID, ProviderID: provider.ID, Purpose: "config.suggest", Prompt: "disable pvp"})
if err != nil {
t.Fatalf("invoke stale AI candidate: %v", err)
}
stored, err := svc.store.ServerInstances().Get(instance.ID)
if err != nil {
t.Fatalf("get server: %v", err)
}
stored.ConfigVersion++
if err := svc.store.ServerInstances().Update(stored); err != nil {
t.Fatalf("advance config revision: %v", err)
}
if _, err := svc.ApproveAIConfigDiffForSession(session, domain.AIConfigDiffApprovalRequest{DiffID: staleResponse.ConfigRecommendation.DiffID, IdempotencyKey: "approve-stale"}); err == nil || !strings.Contains(err.Error(), "expectedConfigVersion") {
t.Fatalf("expected stale revision rejection, got %v", err)
}
}
func newPluginOperationsFixture(t *testing.T) (*CoreService, string, domain.ServerInstance) {
t.Helper()
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.SupportedOS = []string{"linux"}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin platform: %v", err)
}
endpoint.Platform = "linux"
endpoint.Architecture = "amd64"
endpoint.LastHeartbeatAt = fixedTime
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update endpoint metadata: %v", err)
}
session := createServiceUserAndLogin(t, svc, domain.User{ID: "production-owner", DisplayName: "Production Owner", Email: "production-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "production-server", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "Production Server", State: domain.ServerInstanceStateReady})
if err != nil {
t.Fatalf("create server: %v", err)
}
binding := domain.RuntimeBinding{ID: "runtime-binding-" + instance.ID, ServerInstanceID: instance.ID, PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: "local", Mode: "local-process", Status: domain.RuntimeBindingStatusComplete, CreatedAt: fixedTime, UpdatedAt: fixedTime}
if err := svc.store.RuntimeBindings().Create(binding); err != nil && !errors.Is(err, repo.ErrDuplicate) {
t.Fatalf("create runtime binding: %v", err)
}
return svc, session, instance
}