feat: 清理openspec

This commit is contained in:
npc0-hue
2026-08-20 17:58:36 +08:00
parent 49669a9d5d
commit 40b35b05c7
522 changed files with 168 additions and 19538 deletions
+22 -6
View File
@@ -30,7 +30,7 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
return domain.RunControlHelloResult{}, err
}
if hasComponentAuthIdentity(hello) {
if err := svc.validateDedicatedRunHello(hello); err != nil {
if err := svc.validateAuthenticatedRunHello(hello); err != nil {
return domain.RunControlHelloResult{}, err
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
@@ -76,6 +76,19 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
if err := svc.upsertRunEndpoint(endpoint); err != nil {
return domain.RunControlHelloResult{}, err
}
if hello.ServerInstanceID != "" && hello.ComponentKind == domain.DistributionComponentRun {
instance, instanceErr := svc.store.ServerInstances().Get(hello.ServerInstanceID)
if instanceErr != nil {
return domain.RunControlHelloResult{}, instanceErr
}
if instance.RunEndpointID != hello.RunEndpointID {
instance.RunEndpointID = hello.RunEndpointID
instance.UpdatedAt = stamp
if err := svc.store.ServerInstances().Update(instance); err != nil {
return domain.RunControlHelloResult{}, err
}
}
}
previous, previousErr := svc.store.RunControlSessions().Get(hello.RunEndpointID)
generation := 1
if previousErr == nil {
@@ -126,19 +139,22 @@ func (svc *CoreService) RegisterRunHello(hello domain.RunControlHello) (domain.R
}), nil
}
func (svc *CoreService) validateDedicatedRunHello(hello domain.RunControlHello) error {
func (svc *CoreService) validateAuthenticatedRunHello(hello domain.RunControlHello) error {
if hello.ComponentKind != domain.DistributionComponentRun {
return validationError("component-authenticated run hello must use the run component")
}
if hello.RunEndpointID == platformDistributionBuilderEndpointID {
return validationError("platform distribution builder cannot register as a server Run")
}
instance, err := svc.store.ServerInstances().Get(hello.ServerInstanceID)
if err != nil {
return err
}
if strings.TrimSpace(instance.DeploymentTargetID) == "" && instance.RunEndpointID != dedicatedRunEndpointID(instance.ID) {
return nil // legacy Run registrations keep their historical endpoint contract.
if hello.PluginID != instance.PluginID {
return validationError("Run plugin identity does not match the server instance")
}
if hello.PluginID != instance.PluginID || hello.RunEndpointID != instance.RunEndpointID {
return validationError("run endpoint identity does not match the server binding")
if instance.State == domain.ServerInstanceStateDeleted {
return validationError("deleted server cannot attach an active Run heartbeat")
}
instances, err := svc.store.ServerInstances().List(domain.ServerInstanceFilter{RunEndpointID: hello.RunEndpointID})
if err != nil {
+8 -9
View File
@@ -205,7 +205,6 @@ func TestCoreServiceRunHelloRejectsStalePackageKeyAfterReset(t *testing.T) {
func TestCoreServiceRunHelloRejectsGeneratedRunOnPromotedBuildEndpoint(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
builderID := instance.RunEndpointID
instance.State = domain.ServerInstanceStateFailed
if err := svc.store.ServerInstances().Update(instance); err != nil {
t.Fatalf("mark legacy server failed: %v", err)
@@ -213,28 +212,28 @@ func TestCoreServiceRunHelloRejectsGeneratedRunOnPromotedBuildEndpoint(t *testin
if _, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "promoted-hello-fence"}); err != nil {
t.Fatalf("generate promoted Run: %v", err)
}
migrated, err := svc.GetServerInstance(instance.ID)
if err != nil {
t.Fatalf("get migrated server: %v", err)
}
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("get component key: %v", err)
}
hello := validRunControlHello()
hello.RunEndpointID = builderID
hello.RunEndpointID = platformDistributionBuilderEndpointID
hello.RegistrationToken = plainKey
hello.ServerInstanceID = instance.ID
hello.PluginID = instance.PluginID
hello.ComponentKind = domain.DistributionComponentRun
hello.KeyGeneration = key.Generation
if _, err := svc.RegisterRunHello(hello); err == nil || !strings.Contains(err.Error(), "does not match") {
if _, err := svc.RegisterRunHello(hello); err == nil || !strings.Contains(err.Error(), "platform distribution builder") {
t.Fatalf("expected shared builder registration rejection, got %v", err)
}
hello.RunEndpointID = migrated.RunEndpointID
hello.RunEndpointID = generatedRunEndpointID(instance.ID)
if result, err := svc.RegisterRunHello(hello); err != nil || !result.Accepted {
t.Fatalf("expected dedicated Run registration acceptance, result=%+v err=%v", result, err)
t.Fatalf("expected automatically discovered Run registration acceptance, result=%+v err=%v", result, err)
}
attached, err := svc.GetServerInstance(instance.ID)
if err != nil || attached.RunEndpointID != hello.RunEndpointID {
t.Fatalf("expected heartbeat to attach active Run endpoint, instance=%+v err=%v", attached, err)
}
}
@@ -274,19 +274,11 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
packageConfig := readGeneratedPackageConfig(t, svc, session, first.ArtifactID)
instance, err = svc.GetServerInstance(instance.ID)
if err != nil {
t.Fatalf("get dedicated Run binding: %v", err)
}
bootstrap, err := svc.store.RunEndpoints().Get(instance.DeploymentTargetID)
if err != nil {
t.Fatalf("get former bootstrap endpoint: %v", err)
}
bootstrap.Status = domain.RunEndpointStatusOffline
if err := svc.store.RunEndpoints().Update(bootstrap); err != nil {
t.Fatalf("take former bootstrap endpoint offline: %v", err)
t.Fatalf("get server after Run build: %v", err)
}
helloRequest := validRunControlHello()
helloRequest.RunEndpointID = instance.RunEndpointID
helloRequest.RunEndpointID = first.RunEndpointID
helloRequest.RegistrationToken = packageConfig.AuthKey
helloRequest.ServerInstanceID = instance.ID
helloRequest.PluginID = instance.PluginID
@@ -304,7 +296,7 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
t.Fatalf("register generated Run: result=%+v err=%v", registered, err)
}
online, err := svc.store.RunEndpoints().List(domain.RunEndpointFilter{Status: domain.RunEndpointStatusOnline})
if err != nil || len(online) != 1 || online[0].ID != instance.RunEndpointID {
if err != nil || len(online) != 1 || online[0].ID != first.RunEndpointID {
t.Fatalf("expected generated Run to be the only online endpoint: endpoints=%+v err=%v", online, err)
}
for _, capability := range online[0].Capabilities {
@@ -313,7 +305,7 @@ func TestCoreServiceGeneratedRunOnlyEndpointCanGenerateAnotherRun(t *testing.T)
}
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{
RunEndpointID: instance.RunEndpointID,
RunEndpointID: first.RunEndpointID,
SessionToken: registered.SessionToken,
Capabilities: []string{domain.JobCapabilityDistributionBuild},
Capacity: domain.RunCapacity{MaxJobs: 1},
+6 -35
View File
@@ -46,9 +46,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
return domain.RunDistribution{}, err
}
}
if err := svc.promoteLegacyRunBinding(&instance); err != nil {
return domain.RunDistribution{}, err
}
if ready, reason := svc.distributionBuilderReadiness(); !ready {
_ = svc.recordAuditEvent(user.ID, "run.generate.denied", "server-instance", instance.ID, domain.AuditResultDenied, reason)
return domain.RunDistribution{}, validationError(reason)
@@ -72,7 +69,7 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
ID: distributionID,
ServerInstanceID: instance.ID,
PluginID: plugin.ID,
RunEndpointID: instance.RunEndpointID,
RunEndpointID: runEndpointIDForDistribution(instance),
TargetOS: request.TargetOS,
TargetArch: request.TargetArch,
PackageFormat: runPackageFormatForTarget(request.TargetOS),
@@ -123,29 +120,6 @@ func (svc *CoreService) GenerateRunDistributionForSession(sessionID string, requ
return domain.CopyRunDistribution(distribution), nil
}
// promoteLegacyRunBinding reserves the server-scoped endpoint used by a
// generated Run. A legacy shared endpoint remains an optional deployment target
// for non-build workflows; distribution builds are always platform-owned.
func (svc *CoreService) promoteLegacyRunBinding(instance *domain.ServerInstance) error {
if instance == nil || strings.TrimSpace(instance.DeploymentTargetID) != "" || (instance.State != domain.ServerInstanceStateDraft && instance.State != domain.ServerInstanceStateFailed) {
return nil
}
currentEndpointID := strings.TrimSpace(instance.RunEndpointID)
dedicatedEndpointID := dedicatedRunEndpointID(instance.ID)
if currentEndpointID == dedicatedEndpointID {
return nil
}
if currentEndpointID != "" {
instance.DeploymentTargetID = currentEndpointID
}
instance.RunEndpointID = dedicatedEndpointID
instance.UpdatedAt = svc.now()
if err := validator.ValidateServerInstance(*instance); err != nil {
return err
}
return svc.store.ServerInstances().Update(*instance)
}
func (svc *CoreService) GenerateClientManagerDistributionForSession(sessionID string, request domain.ClientManagerBuildRequest) (domain.ClientManagerDistribution, error) {
request = domain.CopyClientManagerBuildRequest(request)
if strings.TrimSpace(request.IdempotencyKey) == "" {
@@ -458,9 +432,6 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
}
endpoint, endpointErr := svc.store.RunEndpoints().Get(instance.RunEndpointID)
runRegistered := endpointErr == nil
if endpointErr != nil && strings.TrimSpace(instance.DeploymentTargetID) != "" {
endpoint, endpointErr = svc.store.RunEndpoints().Get(instance.DeploymentTargetID)
}
if endpointErr != nil && !errors.Is(endpointErr, repo.ErrNotFound) {
return domain.ServerRuntimeActions{}, endpointErr
}
@@ -506,15 +477,15 @@ func (svc *CoreService) GetServerRuntimeActionsForSession(sessionID string, serv
Actions: []domain.ServerRuntimeAction{
runtimeAction("generate-run", "Generate run", pluginDeclares(plugin, "server.run.distribution") && builderReady && runPackageInputsComplete, fallbackReason(!pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared", fallbackReason(!builderReady, builderReason, runPackageReason))),
runtimeAction("download-run", "Download run", hasAvailableRunPackage, "run package has not been generated"),
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && runPackageInputsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", runPackageReason))),
runtimeAction("push-run-update", "Push run update", runRegistered && pluginDeclares(plugin, "server.run.distribution") && svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate) && runPackageInputsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!pluginDeclares(plugin, "server.run.distribution") || !svc.endpointSupports(endpoint, domain.JobCapabilityRunSelfUpdate), "run endpoint cannot self-update", runPackageReason))),
runtimeAction("reset-run-key", "Reset run key", pluginDeclares(plugin, "server.run.distribution"), "plugin permission is not declared"),
runtimeAction("generate-client-manager", "Generate client manager", pluginDeclares(plugin, "server.client-manager.manage") && builderReady && bindingsComplete, fallbackReason(!pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared", fallbackReason(!builderReady, builderReason, bindingReason))),
runtimeAction("download-client-manager", "Download client manager", hasAvailableClientPackage, "client-manager package has not been generated"),
runtimeAction("reset-client-manager-key", "Reset client-manager key", pluginDeclares(plugin, "server.client-manager.manage"), "client-manager permission is not declared"),
runtimeAction("dependencies-check", "Check dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason)))),
runtimeAction("dependencies-install", "Install dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall), "run endpoint cannot install dependencies", bindingReason)))),
runtimeAction("live-logs", "Live logs", runRegistered && pluginSupports(plugin, "logs.read"), fallbackReason(!runRegistered, "dedicated Run has not registered", "plugin does not declare live logs")),
runtimeAction("historical-logs", "Historical logs", runRegistered && svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, fallbackReason(!runRegistered, "dedicated Run has not registered", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill), "run endpoint cannot backfill logs", bindingReason))),
runtimeAction("dependencies-check", "Check dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesCheck), "run endpoint cannot check dependencies", bindingReason)))),
runtimeAction("dependencies-install", "Install dependencies", runRegistered && dependencyPermissionDeclared && svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!dependencyPermissionDeclared, "plugin permission is not declared", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityDependenciesInstall), "run endpoint cannot install dependencies", bindingReason)))),
runtimeAction("live-logs", "Live logs", runRegistered && pluginSupports(plugin, "logs.read"), fallbackReason(!runRegistered, "Run heartbeat has not been observed", "plugin does not declare live logs")),
runtimeAction("historical-logs", "Historical logs", runRegistered && svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill) && bindingsComplete, fallbackReason(!runRegistered, "Run heartbeat has not been observed", fallbackReason(!svc.endpointSupports(endpoint, domain.JobCapabilityLogsBackfill), "run endpoint cannot backfill logs", bindingReason))),
},
}
if runRegistered {
+6 -6
View File
@@ -155,7 +155,7 @@ func TestCoreServiceBuildsSCUMGuidedRunWithoutCompleteRuntimeBinding(t *testing.
}
}
func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T) {
func TestCoreServiceDoesNotPrebindLegacyRunBeforeDistributionBuild(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
legacyEndpointID := instance.RunEndpointID
instance.State = domain.ServerInstanceStateFailed
@@ -165,18 +165,18 @@ func TestCoreServicePromotesLegacyRunBindingBeforeDistributionBuild(t *testing.T
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "legacy-promote-build"})
if err != nil {
t.Fatalf("generate promoted legacy Run: %v", err)
t.Fatalf("generate Run without prebinding: %v", err)
}
migrated, err := svc.GetServerInstance(instance.ID)
if err != nil {
t.Fatalf("get migrated server: %v", err)
}
if migrated.DeploymentTargetID != legacyEndpointID || migrated.RunEndpointID != "server-run-"+instance.ID {
t.Fatalf("expected legacy binding promotion, got %+v", migrated)
if migrated.DeploymentTargetID != "" || migrated.RunEndpointID != legacyEndpointID {
t.Fatalf("Run generation must not change the server's active endpoint, got %+v", migrated)
}
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != migrated.RunEndpointID {
t.Fatalf("expected platform build and dedicated package endpoint %q, job=%+v distribution=%+v err=%v", migrated.RunEndpointID, job, distribution, err)
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != legacyEndpointID {
t.Fatalf("expected platform build and unchanged package endpoint %q, job=%+v distribution=%+v err=%v", legacyEndpointID, job, distribution, err)
}
}
+3
View File
@@ -4,6 +4,9 @@ import "browser.local/platform/domain"
func applyPluginCreateDefaults(plugin domain.GamePlugin, definition domain.ServerDeploymentDefinition) domain.ServerDeploymentDefinition {
definition = domain.CopyServerDeploymentDefinition(definition)
if definition.ProfileKey == "" && len(plugin.RuntimeProfiles.LifecycleProfiles) > 0 {
definition.ProfileKey = plugin.RuntimeProfiles.LifecycleProfiles[0].Key
}
if definition.Mode != domain.ServerDeploymentModeGuided {
return definition
}
+2 -8
View File
@@ -60,10 +60,7 @@ func (svc *CoreService) UpdateServerDeploymentForSession(sessionID, serverInstan
return domain.ServerDeploymentView{}, err
}
if update.RunEndpointID != "" {
if _, err := svc.store.RunEndpoints().Get(update.RunEndpointID); err != nil {
return domain.ServerDeploymentView{}, err
}
instance.RunEndpointID = update.RunEndpointID
return domain.ServerDeploymentView{}, validationError("run endpoint identity is managed by Run heartbeat")
}
instance.Deployment = definition
instance.DeploymentProjection = domain.ServerDeploymentProjection{}
@@ -105,12 +102,9 @@ func (svc *CoreService) deployServerInstance(command domain.ServerLifecycleComma
return domain.ServerLifecycleResult{}, validationError("deployment definition is required")
}
if strings.TrimSpace(instance.RunEndpointID) == "" {
return domain.ServerLifecycleResult{}, validationError("run endpoint must be selected before deployment")
return domain.ServerLifecycleResult{}, validationError("an active Run heartbeat is required for legacy manual deployment dispatch")
}
if _, err := svc.store.RunEndpoints().Get(instance.RunEndpointID); err != nil {
if errors.Is(err, repo.ErrNotFound) && strings.TrimSpace(instance.DeploymentTargetID) != "" {
return domain.ServerLifecycleResult{}, validationError("dedicated Run must register before deployment")
}
return domain.ServerLifecycleResult{}, err
}
plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID)
+3 -26
View File
@@ -7,7 +7,7 @@ import (
"browser.local/platform/domain"
)
func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibleRun(t *testing.T) {
func TestCoreServiceSavesDraftDeploymentRedactsReadsAndKeepsRunIdentityHeartbeatManaged(t *testing.T) {
svc, _ := newLifecycleRunService(t)
createLifecyclePlugin(t, svc)
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "deployment-owner", DisplayName: "Deployment Owner", Email: "deployment-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
@@ -37,31 +37,8 @@ func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibl
t.Fatalf("expected explicit deployment reveal, reveal=%+v err=%v", revealed, err)
}
if _, err := svc.UpdateServerDeploymentForSession(ownerSession, draft.Instance.ID, domain.ServerDeploymentUpdate{RunEndpointID: "run-local", Mode: domain.ServerDeploymentModeCustom}); err != nil {
t.Fatalf("bind draft to run: %v", err)
}
if _, err := svc.DeployServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "deploy-incompatible"}); err == nil || !strings.Contains(err.Error(), "deployment.plan.v1") {
t.Fatalf("expected incompatible Run rejection, got %v", err)
}
endpoint, err := svc.store.RunEndpoints().Get("run-local")
if err != nil {
t.Fatalf("get endpoint: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityDeploymentPlan)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("enable deployment capability: %v", err)
}
deployed, err := svc.DeployServerInstanceForSession(ownerSession, domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "deploy-compatible"})
if err != nil {
t.Fatalf("deploy compatible draft: %v", err)
}
if deployed.Job.ExecutionInput.Deployment == nil || deployed.Job.ExecutionInput.Deployment.StartCommand != "/srv/venv-server/.venv/bin/python server.py" || deployed.Job.Progress.Phase != "queued" {
t.Fatalf("Run job must carry protected plan and queued phase: %+v", deployed.Job)
}
view, err = svc.GetServerDeploymentForSession(ownerSession, draft.Instance.ID)
if err != nil || view.LatestDispatch == nil || view.LatestDispatch.JobID != deployed.Job.ID || view.LatestDispatch.DeploymentRevision != deployed.Job.ExecutionInput.Deployment.Revision || !view.LatestDispatch.DeploymentDefinitionIncluded {
t.Fatalf("expected safe dispatch evidence, view=%+v err=%v", view, err)
if _, err := svc.UpdateServerDeploymentForSession(ownerSession, draft.Instance.ID, domain.ServerDeploymentUpdate{RunEndpointID: "run-local", Mode: domain.ServerDeploymentModeCustom}); err == nil || !strings.Contains(err.Error(), "managed by Run heartbeat") {
t.Fatalf("expected Run identity update to be rejected, got %v", err)
}
}
+15 -1
View File
@@ -152,10 +152,24 @@ func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecyc
}), nil
}
func dedicatedRunEndpointID(serverInstanceID string) string {
func generatedRunEndpointID(serverInstanceID string) string {
return "server-run-" + serverInstanceID
}
// dedicatedRunEndpointID remains a compatibility helper for legacy fixtures.
// Production flow uses generatedRunEndpointID only when building a package and
// attaches the active endpoint from the first authenticated Run heartbeat.
func dedicatedRunEndpointID(serverInstanceID string) string {
return generatedRunEndpointID(serverInstanceID)
}
func runEndpointIDForDistribution(instance domain.ServerInstance) string {
if endpointID := strings.TrimSpace(instance.RunEndpointID); endpointID != "" {
return endpointID
}
return generatedRunEndpointID(instance.ID)
}
func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) {
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
+17 -24
View File
@@ -159,43 +159,36 @@ func assertLogProcessStateEvent(t *testing.T, subscription LogEventSubscription,
}
}
func TestCoreServiceCreatesTargetBoundDraftAndRequiresDedicatedRunRegistration(t *testing.T) {
func TestCoreServiceCreatesUnboundDraftAndAttachesRunFromHeartbeat(t *testing.T) {
svc, _ := newLifecycleRunService(t)
plugin := createLifecyclePlugin(t, svc)
draft, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{
ID: "server-dedicated", PluginID: plugin.ID, DeploymentTargetID: "run-local", Name: "Dedicated SCUM", IdempotencyKey: "dedicated-draft", ProfileKey: "local",
})
draft, err := svc.CreateServerInstanceWorkflow(domain.ServerLifecycleCreate{ID: "server-dedicated", PluginID: plugin.ID, Name: "Dedicated SCUM", IdempotencyKey: "dedicated-draft"})
if err != nil {
t.Fatalf("create target-bound draft: %v", err)
}
if draft.Instance.State != domain.ServerInstanceStateDraft || draft.Job.ID != "" || draft.Instance.DeploymentTargetID != "run-local" || draft.Instance.RunEndpointID != "server-run-server-dedicated" {
t.Fatalf("expected draft with separate target and reserved Run identity, got %+v", draft)
}
if _, err := svc.DeployServerInstanceForSession("", domain.ServerLifecycleCommand{ServerInstanceID: draft.Instance.ID, ExpectedConfigVersion: draft.Instance.ConfigVersion, IdempotencyKey: "before-register"}); err == nil {
t.Fatal("expected deployment without a registered dedicated Run to fail")
if draft.Instance.State != domain.ServerInstanceStateDraft || draft.Job.ID != "" || draft.Instance.RunEndpointID != "" {
t.Fatalf("expected draft without a reserved Run identity, got %+v", draft)
}
key, plainKey, err := svc.ensureActiveComponentKey(draft.Instance.ID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("create Run key: %v", err)
}
wrong := validRunControlHello()
wrong.ServerInstanceID = draft.Instance.ID
wrong.PluginID = plugin.ID
wrong.ComponentKind = domain.DistributionComponentRun
wrong.KeyGeneration = key.Generation
wrong.RegistrationToken = plainKey
wrong.RunEndpointID = "run-local"
if _, err := svc.RegisterRunHello(wrong); err == nil || !strings.Contains(err.Error(), "does not match") {
t.Fatalf("expected mismatched endpoint registration rejection, got %v", err)
hello := validRunControlHello()
hello.ServerInstanceID = draft.Instance.ID
hello.PluginID = plugin.ID
hello.ComponentKind = domain.DistributionComponentRun
hello.KeyGeneration = key.Generation
hello.RegistrationToken = plainKey
hello.RunEndpointID = "run-local"
hello.DisplayName = "Automatic SCUM Run"
if registered, err := svc.RegisterRunHello(hello); err != nil || !registered.Accepted {
t.Fatalf("register automatic Run heartbeat: result=%+v err=%v", registered, err)
}
correct := wrong
correct.RunEndpointID = draft.Instance.RunEndpointID
correct.DisplayName = "Dedicated SCUM Run"
if registered, err := svc.RegisterRunHello(correct); err != nil || !registered.Accepted {
t.Fatalf("register dedicated Run: result=%+v err=%v", registered, err)
attached, err := svc.GetServerInstance(draft.Instance.ID)
if err != nil || attached.RunEndpointID != "run-local" {
t.Fatalf("expected heartbeat to attach Run endpoint, instance=%+v err=%v", attached, err)
}
}