fix: 调试发布run

This commit is contained in:
npc0-hue
2026-07-22 11:44:48 +08:00
parent b06623d0ce
commit 6c3eb6e45f
41 changed files with 1289 additions and 206 deletions
+1 -1
View File
@@ -600,7 +600,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
State: job.State,
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Message: job.Progress.Message},
ResultRef: job.ResultRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs)},
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs)},
LeaseToken: leaseToken,
Attempt: job.Attempt,
MaxAttempts: job.RetryPolicy.MaxAttempts,
+1 -1
View File
@@ -510,7 +510,7 @@ func (svc *CoreService) ApproveAIConfigDiffForSession(sessionID string, request
}
func (svc *CoreService) projectProductionOpsJobResult(job domain.Job, stamp time.Time) error {
if job.ExecutionInput.LifecycleOperation == "" || job.ExecutionInput.PluginID == "" {
if !strings.HasPrefix(job.ID, "job-plugin-lifecycle-") || job.ExecutionInput.LifecycleOperation == "" || job.ExecutionInput.PluginID == "" {
return nil
}
svc.productionMu.Lock()
+9 -3
View File
@@ -98,7 +98,7 @@ type Core interface {
ListServerAdministratorCandidates(string, string) ([]domain.User, error)
AddServerAdministrator(string, string, string) (domain.ServerInstance, error)
RemoveServerAdministrator(string, string, string) (domain.ServerInstance, error)
ArchiveServerInstanceForSession(string, string) (domain.ServerInstance, error)
DeleteServerInstanceForSession(string, string, string) (domain.ServerInstance, error)
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
GetProductionCapacityForSession(string) (domain.ProductionCapacitySummary, error)
@@ -2091,7 +2091,7 @@ func (svc *CoreService) RemoveServerAdministrator(sessionID string, serverInstan
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) ArchiveServerInstanceForSession(sessionID string, serverInstanceID string) (domain.ServerInstance, error) {
func (svc *CoreService) DeleteServerInstanceForSession(sessionID string, serverInstanceID string, password string) (domain.ServerInstance, error) {
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerInstance{}, err
@@ -2103,8 +2103,14 @@ func (svc *CoreService) ArchiveServerInstanceForSession(sessionID string, server
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
return domain.ServerInstance{}, ErrForbidden
}
if strings.TrimSpace(password) == "" {
return domain.ServerInstance{}, validationError("password is required")
}
if !verifyPassword(user.PasswordHash, password) {
return domain.ServerInstance{}, forbiddenError("password confirmation failed")
}
if instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateInstalling {
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before archive")
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before delete")
}
if instance.State == domain.ServerInstanceStateDeleted {
return domain.CopyServerInstance(instance), nil
+99
View File
@@ -414,6 +414,105 @@ func TestCoreServiceScopesServerAccessAndMembership(t *testing.T) {
}
}
func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-owner",
DisplayName: "Delete Owner",
Email: "delete-owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
adminSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-admin",
DisplayName: "Delete Admin",
Email: "delete-admin@example.test",
Roles: []string{"platform-admin"},
PasswordHash: "secret-password",
})
otherSession := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-delete-other",
DisplayName: "Delete Other",
Email: "delete-other@example.test",
Roles: []string{"server-admin"},
PasswordHash: "secret-password",
})
ownerInstance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-owner",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Owner Server",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create owner instance: %v", err)
}
adminTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-admin",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Admin Target",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create admin target: %v", err)
}
runningTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-running",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Running Target",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create running target: %v", err)
}
runningTarget.State = domain.ServerInstanceStateRunning
if err := svc.store.ServerInstances().Update(runningTarget); err != nil {
t.Fatalf("set running target state: %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(otherSession, ownerInstance.ID, "secret-password"); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected non-owner delete to be forbidden, got %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, ""); err == nil {
t.Fatalf("expected missing password to fail")
} else {
var validationErr validator.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected missing password to be validation error, got %v", err)
}
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, "wrong-password"); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected wrong password to be forbidden, got %v", err)
}
deletedOwner, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, "secret-password")
if err != nil {
t.Fatalf("delete owner instance: %v", err)
}
if deletedOwner.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted owner state, got %+v", deletedOwner)
}
deletedAdmin, err := svc.DeleteServerInstanceForSession(adminSession, adminTarget.ID, "secret-password")
if err != nil {
t.Fatalf("delete admin target: %v", err)
}
if deletedAdmin.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted admin state, got %+v", deletedAdmin)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, "secret-password"); err == nil {
t.Fatalf("expected running instance delete to fail")
} else {
var validationErr validator.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected running delete to be validation error, got %v", err)
}
}
}
func TestCoreServiceMetricsAndConfigReadAreRoleScoped(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
+20 -1
View File
@@ -220,7 +220,11 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
Capability: capability,
TargetKey: actionRef,
IdempotencyKey: idempotencyKey,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: binding.ProfileKey},
ExecutionInput: domain.JobExecutionInput{
WorkspaceScope: binding.ProfileKey,
PluginID: plugin.ID,
LifecycleOperation: lifecycleExecutionOperation(action),
},
})
if err != nil {
return domain.Job{}, err
@@ -231,6 +235,21 @@ func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, act
return job, nil
}
func lifecycleExecutionOperation(action domain.ServerLifecycleAction) string {
switch action {
case domain.ServerLifecycleActionCreate:
return "install"
case domain.ServerLifecycleActionStart:
return "start"
case domain.ServerLifecycleActionStop:
return "stop"
case domain.ServerLifecycleActionStatus:
return "status"
default:
return string(action)
}
}
func runtimeProfileActionRef(actions domain.PluginLifecycleActions, action domain.ServerLifecycleAction) string {
switch action {
case domain.ServerLifecycleActionCreate:
+12
View File
@@ -25,6 +25,9 @@ func TestCoreServiceServerLifecycleWorkflows(t *testing.T) {
if created.Action != domain.ServerLifecycleActionCreate || created.Instance.State != domain.ServerInstanceStateInstalling || created.Job.Capability != domain.LifecycleCapabilityInstall {
t.Fatalf("expected install workflow result, got %+v", created)
}
if created.Job.ExecutionInput.PluginID != "server.scum" || created.Job.ExecutionInput.WorkspaceScope != "local" || created.Job.ExecutionInput.LifecycleOperation != "install" {
t.Fatalf("expected install job to carry plugin/profile metadata, got %+v", created.Job.ExecutionInput)
}
claimAndCompleteLifecycleJob(t, svc, sessionToken, domain.LifecycleCapabilityInstall, domain.JobStateSucceeded)
ready, err := svc.GetServerInstance("server-1")
@@ -46,6 +49,9 @@ func TestCoreServiceServerLifecycleWorkflows(t *testing.T) {
if started.Action != domain.ServerLifecycleActionStart || started.Job.Capability != domain.LifecycleCapabilityStart {
t.Fatalf("expected start workflow result, got %+v", started)
}
if started.Job.ExecutionInput.PluginID != "server.scum" || started.Job.ExecutionInput.WorkspaceScope != "local" || started.Job.ExecutionInput.LifecycleOperation != "start" {
t.Fatalf("expected start job to carry plugin/profile metadata, got %+v", started.Job.ExecutionInput)
}
claimAndCompleteLifecycleJob(t, svc, sessionToken, domain.LifecycleCapabilityStart, domain.JobStateSucceeded)
running, err := svc.GetServerInstance("server-1")
if err != nil {
@@ -66,6 +72,9 @@ func TestCoreServiceServerLifecycleWorkflows(t *testing.T) {
if stopped.Action != domain.ServerLifecycleActionStop || stopped.Job.Capability != domain.LifecycleCapabilityStop {
t.Fatalf("expected stop workflow result, got %+v", stopped)
}
if stopped.Job.ExecutionInput.PluginID != "server.scum" || stopped.Job.ExecutionInput.WorkspaceScope != "local" || stopped.Job.ExecutionInput.LifecycleOperation != "stop" {
t.Fatalf("expected stop job to carry plugin/profile metadata, got %+v", stopped.Job.ExecutionInput)
}
claimAndCompleteLifecycleJob(t, svc, sessionToken, domain.LifecycleCapabilityStop, domain.JobStateSucceeded)
final, err := svc.GetServerInstance("server-1")
if err != nil {
@@ -282,6 +291,9 @@ func claimAndCompleteLifecycleJobForServer(t *testing.T, svc *CoreService, sessi
if !claim.HasJob || claim.Job.Capability != capability {
t.Fatalf("expected claimed lifecycle job %s, got %+v", capability, claim)
}
if claim.Job.ExecutionInput.PluginID == "" || claim.Job.ExecutionInput.WorkspaceScope == "" {
t.Fatalf("expected lifecycle claim to carry plugin/profile metadata, got %+v", claim.Job.ExecutionInput)
}
if serverInstanceID != "" && claim.Job.ServerInstanceID != serverInstanceID {
t.Fatalf("expected claimed lifecycle job for %s, got %+v", serverInstanceID, claim.Job)
}