Feat v0.9.6 stage mode collapse (#80)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m49s
CI/CD / test-sqlite (push) Successful in 3m4s
CI/CD / build-and-deploy (push) Successful in 1m15s
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m49s
CI/CD / test-sqlite (push) Successful in 3m4s
CI/CD / build-and-deploy (push) Successful in 1m15s
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #80.
This commit is contained in:
@@ -18,7 +18,7 @@ func testEngine(t *testing.T) *workflow.Engine {
|
||||
return workflow.NewEngine(s, nil, nil)
|
||||
}
|
||||
|
||||
// seedEngineFixture creates a 3-stage workflow (form → review → form),
|
||||
// seedEngineFixture creates a 3-stage workflow (form → form → form),
|
||||
// publishes it, and returns (workflowID, userID, teamID).
|
||||
func seedEngineFixture(t *testing.T, slug string) (string, string, string) {
|
||||
t.Helper()
|
||||
@@ -42,7 +42,7 @@ func seedEngineFixture(t *testing.T, slug string) (string, string, string) {
|
||||
|
||||
stages := []models.WorkflowStage{
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "intake", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "review", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, AssignmentTeamID: &teamID},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "review", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, AssignmentTeamID: &teamID},
|
||||
{WorkflowID: wf.ID, Ordinal: 2, Name: "final", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
@@ -134,8 +134,8 @@ func TestEngine_BranchRouting(t *testing.T) {
|
||||
|
||||
stages := []models.WorkflowStage{
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "intake", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, BranchRules: branchRules},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "normal-review", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 2, Name: "escalation", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "normal-review", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 2, Name: "escalation", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
s.Workflows.CreateStage(ctx, &stages[i])
|
||||
@@ -184,7 +184,7 @@ func TestEngine_PublicEntry(t *testing.T) {
|
||||
|
||||
stages := []models.WorkflowStage{
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "public-form", StageMode: models.StageModeForm, Audience: models.AudiencePublic, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "team-review", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "team-review", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
s.Workflows.CreateStage(ctx, &stages[i])
|
||||
@@ -253,7 +253,7 @@ func TestEngine_SignoffGate(t *testing.T) {
|
||||
"validation": map[string]any{"required_approvals": 2},
|
||||
})
|
||||
stages := []models.WorkflowStage{
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "gated-stage", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, StageConfig: validationCfg},
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "gated-stage", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, StageConfig: validationCfg},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "done", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
@@ -307,7 +307,7 @@ func TestEngine_SignoffRejection(t *testing.T) {
|
||||
"validation": map[string]any{"required_approvals": 1, "reject_action": "cancel"},
|
||||
})
|
||||
stages := []models.WorkflowStage{
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "review", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, StageConfig: validationCfg},
|
||||
{WorkflowID: wf.ID, Ordinal: 0, Name: "review", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, StageConfig: validationCfg},
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "approved", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
@@ -590,7 +590,7 @@ func TestPackageInstall_ManifestRoundtrip(t *testing.T) {
|
||||
{WorkflowID: wf.ID, Ordinal: 1, Name: "classify", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, BranchRules: branchRules},
|
||||
{WorkflowID: wf.ID, Ordinal: 2, Name: "fix-critical", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple, SLASeconds: &sla},
|
||||
{WorkflowID: wf.ID, Ordinal: 3, Name: "fix-normal", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 4, Name: "verify", StageMode: models.StageModeReview, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
{WorkflowID: wf.ID, Ordinal: 4, Name: "verify", StageMode: models.StageModeForm, Audience: models.AudienceTeam, StageType: models.StageTypeSimple},
|
||||
}
|
||||
for i := range stages {
|
||||
if err := s.Workflows.CreateStage(ctx, &stages[i]); err != nil {
|
||||
|
||||
@@ -55,7 +55,6 @@ func (h *WorkflowPackageHandler) ExportWorkflowPackage(c *gin.Context) {
|
||||
"ordinal": s.Ordinal,
|
||||
"stage_mode": s.StageMode,
|
||||
"audience": s.Audience,
|
||||
"stage_type": s.StageType,
|
||||
"auto_transition": s.AutoTransition,
|
||||
}
|
||||
if s.AssignmentTeamID != nil {
|
||||
@@ -230,6 +229,7 @@ func InstallWorkflowFromManifest(ctx *gin.Context, stores store.Stores, pkgID st
|
||||
StarlarkHook: s.StarlarkHook,
|
||||
SLASeconds: s.SLASeconds,
|
||||
}
|
||||
st.StageMode = models.NormalizeStageModeInput(st.StageMode)
|
||||
if st.StageMode == "" {
|
||||
st.StageMode = models.StageModeDelegated
|
||||
}
|
||||
@@ -284,7 +284,7 @@ type workflowPkgStage struct {
|
||||
Ordinal int `json:"ordinal"`
|
||||
StageMode string `json:"stage_mode"`
|
||||
Audience string `json:"audience"`
|
||||
StageType string `json:"stage_type"`
|
||||
StageType string `json:"stage_type"` // deprecated — kept for backward-compat deserialization
|
||||
AutoTransition bool `json:"auto_transition"`
|
||||
AssignmentTeamID *string `json:"assignment_team_id,omitempty"`
|
||||
SurfacePkgID *string `json:"surface_pkg_id,omitempty"`
|
||||
|
||||
@@ -69,7 +69,7 @@ func seedWorkflowFixture(t *testing.T, s store.Stores, userID, teamID string) (s
|
||||
WorkflowID: wf.ID,
|
||||
Ordinal: 1,
|
||||
Name: "review",
|
||||
StageMode: models.StageModeReview,
|
||||
StageMode: models.StageModeForm,
|
||||
Audience: models.AudienceTeam,
|
||||
StageType: models.StageTypeSimple,
|
||||
AssignmentTeamID: &teamID,
|
||||
|
||||
@@ -202,11 +202,12 @@ func (h *WorkflowHandler) CreateStage(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
st.StageMode = models.NormalizeStageModeInput(st.StageMode)
|
||||
if st.StageMode == "" {
|
||||
st.StageMode = models.StageModeDelegated
|
||||
}
|
||||
if !models.ValidStageModes[st.StageMode] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_mode must be form, review, delegated, or automated"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_mode must be form, delegated, or automated"})
|
||||
return
|
||||
}
|
||||
if st.Audience == "" {
|
||||
@@ -219,18 +220,10 @@ func (h *WorkflowHandler) CreateStage(c *gin.Context) {
|
||||
if st.StageType == "" {
|
||||
st.StageType = models.StageTypeSimple
|
||||
}
|
||||
if !models.ValidStageTypes[st.StageType] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_type must be simple, dynamic, or automated"})
|
||||
return
|
||||
}
|
||||
if st.StageMode == models.StageModeDelegated && st.SurfacePkgID == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "delegated mode requires surface_pkg_id"})
|
||||
return
|
||||
}
|
||||
if (st.StageType == models.StageTypeDynamic || st.StageType == models.StageTypeAutomated) && st.StarlarkHook == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dynamic/automated stage_type requires starlark_hook"})
|
||||
return
|
||||
}
|
||||
if st.Ordinal == 0 {
|
||||
existing, _ := h.stores.Workflows.ListStages(c.Request.Context(), st.WorkflowID)
|
||||
st.Ordinal = len(existing)
|
||||
@@ -252,26 +245,19 @@ func (h *WorkflowHandler) UpdateStage(c *gin.Context) {
|
||||
}
|
||||
st.ID = c.Param("sid")
|
||||
st.WorkflowID = c.Param("id")
|
||||
st.StageMode = models.NormalizeStageModeInput(st.StageMode)
|
||||
if st.StageMode != "" && !models.ValidStageModes[st.StageMode] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_mode must be form, review, delegated, or automated"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_mode must be form, delegated, or automated"})
|
||||
return
|
||||
}
|
||||
if st.Audience != "" && !models.ValidAudiences[st.Audience] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "audience must be team, public, or system"})
|
||||
return
|
||||
}
|
||||
if st.StageType != "" && !models.ValidStageTypes[st.StageType] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stage_type must be simple, dynamic, or automated"})
|
||||
return
|
||||
}
|
||||
if st.StageMode == models.StageModeDelegated && st.SurfacePkgID == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "delegated mode requires surface_pkg_id"})
|
||||
return
|
||||
}
|
||||
if (st.StageType == models.StageTypeDynamic || st.StageType == models.StageTypeAutomated) && st.StarlarkHook == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dynamic/automated stage_type requires starlark_hook"})
|
||||
return
|
||||
}
|
||||
if err := h.stores.Workflows.UpdateStage(c.Request.Context(), &st); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update stage"})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user