package service import ( "strings" "testing" "browser.local/platform/domain" ) func TestCoreServiceSavesDraftDeploymentRedactsReadsAndDispatchesOnlyToCompatibleRun(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"}) draft, err := svc.CreateServerInstanceWorkflowForSession(ownerSession, domain.ServerLifecycleCreate{ ID: "venv-draft", PluginID: "server.scum", Name: "Venv server", IdempotencyKey: "draft-venv", Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeCustom, ServerRoot: "/srv/venv-server", WorkingDirectory: "/srv/venv-server", StartCommand: "/srv/venv-server/.venv/bin/python server.py"}, }) if err != nil { t.Fatalf("create unbound deployment draft: %v", err) } if draft.Instance.State != domain.ServerInstanceStateDraft || draft.Job.ID != "" { t.Fatalf("draft must have no lifecycle job, got %+v", draft) } view, err := svc.GetServerDeploymentForSession(ownerSession, draft.Instance.ID) if err != nil { t.Fatalf("read deployment view: %v", err) } if !view.ServerRootConfigured || !view.WorkingDirectoryConfigured || !view.StartCommandConfigured { t.Fatalf("expected protected values to be marked configured: %+v", view) } if strings.Contains(strings.Join([]string{view.ServerInstanceID, string(view.Mode), view.ProfileKey}, " "), "/srv/") { t.Fatalf("redacted deployment view leaked host path: %+v", view) } revealed, err := svc.RevealServerDeploymentForSession(ownerSession, draft.Instance.ID) if err != nil || revealed.ServerRoot != "/srv/venv-server" || revealed.WorkingDirectory != "/srv/venv-server" || revealed.StartCommand != "/srv/venv-server/.venv/bin/python server.py" { 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) } }