Complete platform management workflows

This commit is contained in:
npc0-hue
2026-07-14 16:39:37 +08:00
parent 7e05d0a4e7
commit 4f33f761a3
106 changed files with 11313 additions and 460 deletions
+207 -1
View File
@@ -211,7 +211,9 @@ func TestConfigWriteAndFileDispatchAPIAreScopedAndSafe(t *testing.T) {
ownerSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "owner-config-api@example.test", Password: "secret-password"}).SessionID
otherSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "other-config-api@example.test", Password: "secret-password"}).SessionID
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
pluginRequest := validGamePluginRequest()
pluginRequest.RequiredRunCapabilities = append(pluginRequest.RequiredRunCapabilities, domain.JobCapabilityConfigWrite, domain.JobCapabilityFilesRead, domain.JobCapabilityFilesWrite)
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
instance := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-config-api",
@@ -286,6 +288,101 @@ func TestConfigWriteAndFileDispatchAPIAreScopedAndSafe(t *testing.T) {
}
}
func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
serverID := createRuntimeAPIFixtures(t, router, adminSession)
actions := getJSONWithAuth[dto.ServerRuntimeActionsResponse](t, router, "/api/v1/server-instances/"+serverID+"/runtime/actions", adminSession)
availability := map[string]bool{}
for _, action := range actions.Actions {
availability[action.Key] = action.Available
}
for _, key := range []string{"generate-run", "push-run-update", "generate-client-manager", "dependencies-check", "dependencies-install", "historical-logs"} {
if !availability[key] {
t.Fatalf("expected action %q available in %+v", key, actions.Actions)
}
}
runDistribution := postJSONWithAuth[dto.RunDistributionResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/generate", dto.RunDistributionGenerateRequest{TargetOS: "linux", TargetArch: "amd64", IdempotencyKey: "api-run-generate"}, adminSession)
if runDistribution.ArtifactID == "" || runDistribution.KeyGeneration != 1 || runDistribution.SecretRef == "" {
t.Fatalf("unexpected run distribution: %+v", runDistribution)
}
runDownload := postOKJSONWithAuth[dto.ArtifactDownloadReferenceResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/download", map[string]string{}, adminSession)
if runDownload.ArtifactID != runDistribution.ArtifactID || runDownload.DownloadURL == "" {
t.Fatalf("unexpected run download: %+v", runDownload)
}
updateRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/run/update", dto.RunUpdateRequest{ArtifactID: runDistribution.ArtifactID, Checksum: runDistribution.Checksum, IdempotencyKey: "api-run-update"}, adminSession)
assertStatus(t, updateRecorder, http.StatusAccepted)
update := decodeBody[dto.RunUpdateJobResponse](t, updateRecorder)
if update.JobID == "" || update.ArtifactID != runDistribution.ArtifactID || update.Status != string(domain.DistributionJobStatusQueued) {
t.Fatalf("unexpected run update job: %+v", update)
}
clientDistribution := postJSONWithAuth[dto.ClientManagerDistributionResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/generate", dto.ClientManagerBuildRequest{ProfileKey: "scum-client-manager", TargetOS: "windows", TargetArch: "amd64", RepositoryURL: "https://github.com/F88888/scum_client.git", SourceRevision: "main", IdempotencyKey: "api-client-manager"}, adminSession)
if clientDistribution.ArtifactID == "" || clientDistribution.BuildJobID == "" || clientDistribution.SecretRef == runDistribution.SecretRef {
t.Fatalf("unexpected client distribution: %+v", clientDistribution)
}
clientDownload := postOKJSONWithAuth[dto.ArtifactDownloadReferenceResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/download", dto.ClientManagerDownloadRequest{ProfileKey: "scum-client-manager"}, adminSession)
if clientDownload.ArtifactID != clientDistribution.ArtifactID {
t.Fatalf("unexpected client download: %+v", clientDownload)
}
dependencyCheckRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/check", dto.DependencyJobRequest{ProbeKey: "java-runtime", IdempotencyKey: "api-dependency-check"}, adminSession)
assertStatus(t, dependencyCheckRecorder, http.StatusAccepted)
dependencyCheck := decodeBody[dto.JobResponse](t, dependencyCheckRecorder)
if dependencyCheck.Capability != domain.JobCapabilityDependenciesCheck || dependencyCheck.TargetKey != "dependencies/java-runtime" {
t.Fatalf("unexpected dependency check job: %+v", dependencyCheck)
}
dependencyInstallRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/install", dto.DependencyJobRequest{ProbeKey: "java-runtime", InstallPlanKey: "java-install", IdempotencyKey: "api-dependency-install"}, adminSession)
assertStatus(t, dependencyInstallRecorder, http.StatusAccepted)
dependencyInstall := decodeBody[dto.JobResponse](t, dependencyInstallRecorder)
if dependencyInstall.Capability != domain.JobCapabilityDependenciesInstall || dependencyInstall.TargetKey != "dependencies/install/java-install" {
t.Fatalf("unexpected dependency install job: %+v", dependencyInstall)
}
unsafeDependency := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/dependencies/install", dto.DependencyJobRequest{ProbeKey: "java-runtime", InstallPlanKey: "bash -c whoami", IdempotencyKey: "api-dependency-unsafe"}, adminSession)
assertErrorResponse(t, unsafeDependency, http.StatusBadRequest, errorCodeValidation)
backfillRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+serverID+"/logs/backfill", dto.LogBackfillRequest{SourceKey: "latest", CheckpointRef: "input://logs/" + serverID + "/latest/v1", Limit: 500, IdempotencyKey: "api-logs-backfill"}, adminSession)
assertStatus(t, backfillRecorder, http.StatusAccepted)
backfill := decodeBody[dto.JobResponse](t, backfillRecorder)
if backfill.Capability != domain.JobCapabilityLogsBackfill || backfill.ResultRef != "" || backfill.InputRef == "" {
t.Fatalf("unexpected log backfill job: %+v", backfill)
}
liveLogs := getJSONWithAuth[dto.LogStreamListResponse](t, router, "/api/v1/server-instances/"+serverID+"/logs/live", adminSession)
if liveLogs.Count != 1 || liveLogs.Items[0].StreamKey != "stdout" {
t.Fatalf("unexpected live logs: %+v", liveLogs)
}
runReset := postOKJSONWithAuth[dto.ComponentKeyResponse](t, router, "/api/v1/server-instances/"+serverID+"/run/key/reset", map[string]string{}, adminSession)
if runReset.Generation != 2 || runReset.SecretRef == "" {
t.Fatalf("unexpected run key reset: %+v", runReset)
}
clientReset := postOKJSONWithAuth[dto.ComponentKeyResponse](t, router, "/api/v1/server-instances/"+serverID+"/client-managers/key/reset", dto.ComponentKeyResetRequest{ComponentKey: "scum-client-manager"}, adminSession)
if clientReset.Generation != 2 || clientReset.SecretRef == runReset.SecretRef {
t.Fatalf("unexpected client key reset: %+v", clientReset)
}
for _, body := range []string{mustJSON(t, runDistribution), mustJSON(t, clientDistribution), mustJSON(t, runDownload), mustJSON(t, clientDownload), mustJSON(t, runReset), mustJSON(t, clientReset), mustJSON(t, dependencyInstall), mustJSON(t, backfill)} {
for _, forbidden := range []string{"authKey", "enc:v1", "password=", "unix://", "tcp://", "/Users/", "mysql://", "sqlite://"} {
if strings.Contains(body, forbidden) {
t.Fatalf("runtime API response exposed forbidden fragment %q: %s", forbidden, body)
}
}
}
audits := getJSONWithAuth[dto.AuditEventListResponse](t, router, "/api/v1/audit-events?resourceId="+serverID, adminSession)
auditActions := map[string]bool{}
for _, audit := range audits.Items {
auditActions[audit.Action] = true
}
for _, action := range []string{"run.generate", "run.download", "run.update", "client-manager.build", "client-manager.download", "dependency.install", "logs.backfill", "runtime-key.reset"} {
if !auditActions[action] {
t.Fatalf("expected audit action %q in %+v", action, audits.Items)
}
}
}
func TestCoreAPIErrorResponses(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
@@ -562,6 +659,54 @@ func TestServerLifecycleWorkflowAPI(t *testing.T) {
assertErrorResponse(t, invalidStop, http.StatusBadRequest, errorCodeValidation)
}
func TestServerInstanceManagementAPI(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest())
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
ready := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-management",
PluginID: "server.scum",
RunEndpointID: "run-local",
Name: "SCUM Ops",
State: domain.ServerInstanceStateReady,
}, adminSession)
newName := "SCUM Ops Renamed"
updated := putJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances/server-management", dto.ServerInstanceUpdateRequest{Name: &newName}, adminSession)
if updated.Name != newName || updated.PluginID != ready.PluginID || updated.RunEndpointID != ready.RunEndpointID {
t.Fatalf("unexpected server update: %+v", updated)
}
running := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-running-archive",
PluginID: "server.scum",
RunEndpointID: "run-local",
Name: "SCUM Running Archive",
State: domain.ServerInstanceStateRunning,
}, adminSession)
unsafeArchive := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/"+running.ID, "", adminSession)
assertErrorResponse(t, unsafeArchive, http.StatusBadRequest, errorCodeValidation)
archived := requestWithAuth(t, router, http.MethodDelete, "/api/v1/server-instances/server-management", "", adminSession)
assertStatus(t, archived, http.StatusNoContent)
activeList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances", adminSession)
for _, item := range activeList.Items {
if item.ID == "server-management" {
t.Fatalf("archived server should be hidden from normal list: %+v", activeList)
}
}
deletedList := getJSONWithAuth[dto.ServerInstanceListResponse](t, router, "/api/v1/server-instances?state=deleted", adminSession)
if deletedList.Count != 1 || deletedList.Items[0].ID != "server-management" || deletedList.Items[0].State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected explicit deleted filter to return archived server, got %+v", deletedList)
}
blank := ""
invalidUpdate := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/server-instances/server-running-archive", dto.ServerInstanceUpdateRequest{Name: &blank}, adminSession)
assertErrorResponse(t, invalidUpdate, http.StatusBadRequest, errorCodeValidation)
}
func TestServerAccessAPIScopesOwnersAndAdministrators(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
@@ -1313,6 +1458,67 @@ func anyJSON(t *testing.T, value any) map[string]any {
return body
}
func createRuntimeAPIFixtures(t *testing.T, router http.Handler, adminSession string) string {
t.Helper()
pluginRequest := validGamePluginRequest()
pluginRequest.ID = "server.runtime"
pluginRequest.Name = "Runtime Test Plugin"
pluginRequest.ServerType = "runtime-test"
pluginRequest.SupportedOS = []string{"linux", "windows"}
pluginRequest.RequiredRunCapabilities = []string{
"process.install",
"process.start",
"process.stop",
"logs.read",
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
}
pluginRequest.DeclaredPermissions = []string{
"server.read",
"server.logs.read",
"server.run.distribution",
"server.client-manager.manage",
"server.dependencies.manage",
"server.artifacts.read",
}
pluginRequest.BridgeActions = []string{
string(domain.PluginBridgeActionRunDistribution),
string(domain.PluginBridgeActionClientManager),
string(domain.PluginBridgeActionDependenciesRequest),
string(domain.PluginBridgeActionLogsBackfillRequest),
}
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
endpoint := validRunEndpointRequest()
endpoint.ID = "run-runtime"
endpoint.Capabilities = append(endpoint.Capabilities,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
)
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", endpoint)
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-runtime-api",
PluginID: "server.runtime",
RunEndpointID: "run-runtime",
Name: "Runtime API Server",
State: domain.ServerInstanceStateReady,
}, adminSession)
postJSON[dto.LogStreamResponse](t, router, "/api/v1/log-streams", dto.LogStreamCreateRequest{
ID: "log-runtime-api",
ServerInstanceID: server.ID,
Source: domain.LogStreamSourceProcess,
StreamKey: "stdout",
StorageBackend: domain.LogStorageBackendLocalSegments,
RetentionPolicy: "default",
})
return server.ID
}
func validAIProviderRequest() dto.AIProviderCreateRequest {
return dto.AIProviderCreateRequest{
ID: "ai.openai",