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

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:
2026-04-03 18:00:33 +00:00
committed by xcaliber
parent 75d7abc089
commit ac7286f83b
27 changed files with 1231 additions and 228 deletions

View File

@@ -0,0 +1,13 @@
-- Armature — 018 Stage Mode Collapse
-- Deprecate stage_type validation. Collapse stage_mode: review → form.
-- 1. Migrate existing review rows to form
UPDATE workflow_stages SET stage_mode = 'form' WHERE stage_mode = 'review';
-- 2. Replace CHECK constraint on stage_mode (drop review)
ALTER TABLE workflow_stages DROP CONSTRAINT IF EXISTS workflow_stages_stage_mode_check;
ALTER TABLE workflow_stages ADD CONSTRAINT workflow_stages_stage_mode_check
CHECK (stage_mode IN ('form', 'delegated', 'automated'));
-- 3. Drop CHECK constraint on stage_type (any value accepted)
ALTER TABLE workflow_stages DROP CONSTRAINT IF EXISTS workflow_stages_stage_type_check;

View File

@@ -0,0 +1,5 @@
-- Armature — 018 Stage Mode Collapse
-- Deprecate stage_type validation. Collapse stage_mode: review → form.
-- SQLite cannot ALTER CHECK constraints; app layer prevents new "review" inserts.
UPDATE workflow_stages SET stage_mode = 'form' WHERE stage_mode = 'review';

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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,

View File

@@ -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

View File

@@ -56,9 +56,9 @@ type WorkflowStage struct {
Name string `json:"name"`
AssignmentTeamID *string `json:"assignment_team_id,omitempty"`
FormTemplate json.RawMessage `json:"form_template"`
StageMode string `json:"stage_mode"` // form | review | delegated | automated
StageMode string `json:"stage_mode"` // form | delegated | automated
Audience string `json:"audience"` // team | public | system
StageType string `json:"stage_type"` // simple | dynamic | automated
StageType string `json:"stage_type"` // deprecated — retained for backward compatibility
AutoTransition bool `json:"auto_transition"`
StageConfig json.RawMessage `json:"stage_config"`
BranchRules json.RawMessage `json:"branch_rules"`
@@ -72,7 +72,6 @@ type WorkflowStage struct {
const (
StageModeForm = "form"
StageModeReview = "review"
StageModeDelegated = "delegated"
StageModeAutomated = "automated"
)
@@ -80,12 +79,22 @@ const (
// ValidStageModes is the set of valid stage_mode values.
var ValidStageModes = map[string]bool{
StageModeForm: true,
StageModeReview: true,
StageModeDelegated: true,
StageModeAutomated: true,
}
// ── Stage Type Constants ────────────────────
// NormalizeStageModeInput maps deprecated stage_mode values to their
// replacements. "review" → "form"; all others pass through unchanged.
func NormalizeStageModeInput(mode string) string {
if mode == "review" {
return StageModeForm
}
return mode
}
// ── Stage Type Constants (deprecated) ───────
// stage_type is redundant with starlark_hook presence and is no longer
// validated. Constants are retained for backward-compatible references.
const (
StageTypeSimple = "simple"
@@ -93,13 +102,6 @@ const (
StageTypeAutomated = "automated"
)
// ValidStageTypes is the set of valid stage_type values.
var ValidStageTypes = map[string]bool{
StageTypeSimple: true,
StageTypeDynamic: true,
StageTypeAutomated: true,
}
// ── Audience Constants ──────────────────────
const (

View File

@@ -921,7 +921,7 @@ type WorkflowPageData struct {
WorkflowDescription string
SessionID string
SessionName string
StageMode string // form | review | delegated | automated
StageMode string // form | delegated | automated
StageName string
FormTemplateJSON string // typed form template JSON (empty if delegated)
TotalStages int
@@ -948,7 +948,7 @@ type WorkflowLandingPageData struct {
PersonaName string
PersonaIcon string
StageCount int
FirstStageMode string // form | review | delegated | automated
FirstStageMode string // form | delegated | automated
ResumeURL string // non-empty if visitor has an active session
}

View File

@@ -154,7 +154,7 @@
{{end}}
<button class="wf-start-btn" id="startBtn" onclick="startWorkflow()">
{{if eq .Data.FirstStageMode "form"}}Fill Out Form{{else if eq .Data.FirstStageMode "review"}}Start Review{{else}}Start{{end}}
{{if eq .Data.FirstStageMode "form"}}Fill Out Form{{else}}Start{{end}}
</button>
{{if .Data.ResumeURL}}

View File

@@ -178,7 +178,7 @@
</div>
{{end}}
<!-- Stage surface: form, review, delegated (custom surface), or automated -->
<!-- Stage surface: form, delegated (custom surface), or automated -->
{{if .Data.AudienceMismatch}}
<div class="wf-submitted">
@@ -195,11 +195,6 @@
<div id="customSurfaceMount" style="flex:1;overflow-y:auto"></div>
{{else if eq .Data.StageMode "form"}}
<div class="wf-form" id="formArea"></div>
{{else if eq .Data.StageMode "review"}}
<div id="reviewArea" style="flex:1;overflow-y:auto;display:flex">
<div id="reviewDataPanel" style="flex:1;overflow-y:auto;border-right:1px solid var(--border)"></div>
<div id="reviewActionPanel" style="flex:1;overflow-y:auto;display:flex;flex-direction:column"></div>
</div>
{{else if eq .Data.Status "completed"}}
<div class="wf-form-success" style="flex:1;display:flex;flex-direction:column;justify-content:center">
<h3>Completed</h3>
@@ -212,7 +207,7 @@
{{end}}
<div class="wf-session-info">
{{if .Data.SurfacePkgID}}Session:{{else if eq .Data.StageMode "form"}}Submitting as{{else if eq .Data.StageMode "review"}}Reviewing as{{else}}Session:{{end}} <strong>{{.Data.SessionName}}</strong>
{{if .Data.SurfacePkgID}}Session:{{else if eq .Data.StageMode "form"}}Submitting as{{else}}Session:{{end}} <strong>{{.Data.SessionName}}</strong>
</div>
</div>
@@ -542,116 +537,6 @@
});
}
// ── Review surface (structured review with side-by-side + comments) ──
if (STAGE_MODE === 'review') {
var dataPanel = document.getElementById('reviewDataPanel');
var actionPanel = document.getElementById('reviewActionPanel');
if (dataPanel && actionPanel) {
loadReviewSurface(dataPanel, actionPanel);
}
}
async function loadReviewSurface(dataPanel, actionPanel) {
dataPanel.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text-3)">Loading\u2026</div>';
// Left panel: structured data card
var html = '<div style="padding:24px">';
html += '<h3 style="margin-bottom:16px">Collected Data</h3>';
try {
var resp = await fetch(BASE + '/api/v1/public/workflows/resume/' + API_ID, {
headers: { 'Content-Type': 'application/json' },
});
if (resp.ok) {
var status = await resp.json();
if (status.stage_data && typeof status.stage_data === 'object') {
html += '<table style="width:100%;border-collapse:collapse">';
for (var key in status.stage_data) {
if (!status.stage_data.hasOwnProperty(key) || key.startsWith('_')) continue;
html += '<tr style="border-bottom:1px solid var(--border)">';
html += '<td style="padding:8px;font-weight:600;font-size:13px;width:30%;vertical-align:top">' + escHtml(key) + '</td>';
var val = status.stage_data[key];
var display = typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
html += '<td style="padding:8px;font-size:13px;white-space:pre-wrap">' + escHtml(display) + '</td>';
html += '</tr>';
}
html += '</table>';
} else {
html += '<p style="color:var(--text-3)">No data collected yet.</p>';
}
}
} catch(e) {
html += '<p style="color:var(--danger)">Failed to load review data.</p>';
}
html += '</div>';
dataPanel.innerHTML = html;
// Right panel: comment input + approve/reject buttons
var actHtml = '<div style="padding:24px;flex:1;display:flex;flex-direction:column">';
actHtml += '<h3 style="margin-bottom:16px">Review Actions</h3>';
actHtml += '<div style="margin-bottom:16px">';
actHtml += '<label style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">Add Comment</label>';
actHtml += '<textarea id="reviewComment" rows="3" style="width:100%;padding:8px;font-size:14px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text);font-family:var(--font);resize:vertical" placeholder="Optional comment\u2026"></textarea>';
actHtml += '</div>';
actHtml += '<div style="display:flex;gap:8px">';
actHtml += '<button id="reviewAdvanceBtn" style="background:var(--accent);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Approve &amp; Advance</button>';
actHtml += '<button id="reviewRejectBtn" style="background:var(--danger,#e74c3c);color:#fff;border:none;border-radius:8px;padding:10px 24px;font-weight:600;cursor:pointer;font-size:14px">Reject</button>';
actHtml += '</div>';
actHtml += '<div style="font-size:12px;color:var(--text-3);margin-top:8px">Ctrl+Enter: Approve &middot; Ctrl+Shift+Enter: Reject</div>';
actHtml += '</div>';
actionPanel.innerHTML = actHtml;
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
if (e.shiftKey) {
document.getElementById('reviewRejectBtn').click();
} else {
document.getElementById('reviewAdvanceBtn').click();
}
}
});
document.getElementById('reviewAdvanceBtn').addEventListener('click', async function() {
this.disabled = true;
try {
var r = await fetch(BASE + '/api/v1/public/workflows/advance/' + API_ID, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: {} }),
});
if (r.ok) {
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Approved</h3><p style="color:var(--text-2)">Stage advanced.</p></div>';
setTimeout(function() { window.location.reload(); }, 1500);
} else {
var err = await r.json().catch(function() { return {}; });
alert('Failed: ' + (err.error || 'unknown'));
document.getElementById('reviewAdvanceBtn').disabled = false;
}
} catch(e) { alert('Error: ' + e.message); }
});
document.getElementById('reviewRejectBtn').addEventListener('click', async function() {
var comment = document.getElementById('reviewComment').value;
var reason = comment || prompt('Rejection reason:');
if (!reason) return;
this.disabled = true;
try {
var r = await fetch(BASE + '/api/v1/public/workflows/advance/' + API_ID, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: { _action: 'reject', reason: reason } }),
});
if (r.ok) {
actionPanel.innerHTML = '<div style="padding:40px;text-align:center"><h3>Rejected</h3><p style="color:var(--text-2)">Sent back for revision.</p></div>';
setTimeout(function() { window.location.reload(); }, 1500);
} else {
var err = await r.json().catch(function() { return {}; });
alert('Failed: ' + (err.error || 'unknown'));
document.getElementById('reviewRejectBtn').disabled = false;
}
} catch(e) { alert('Error: ' + e.message); }
});
}
})();
</script>
</body>

View File

@@ -60,7 +60,7 @@ func workflowGetDef(ctx context.Context, stores store.Stores) func(*starlark.Thr
d.SetKey(starlark.String("ordinal"), starlark.MakeInt(s.Ordinal))
d.SetKey(starlark.String("stage_mode"), starlark.String(s.StageMode))
d.SetKey(starlark.String("audience"), starlark.String(s.Audience))
d.SetKey(starlark.String("stage_type"), starlark.String(s.StageType))
d.SetKey(starlark.String("stage_type"), starlark.String(s.StageType)) // deprecated — kept for backward compat
d.SetKey(starlark.String("auto_transition"), starlark.Bool(s.AutoTransition))
if s.StarlarkHook != nil {
d.SetKey(starlark.String("starlark_hook"), starlark.String(*s.StarlarkHook))

View File

@@ -437,13 +437,14 @@ components:
type: integer
stage_mode:
type: string
enum: [form, review, delegated, automated]
enum: [form, delegated, automated]
audience:
type: string
enum: [team, public, system]
stage_type:
type: string
enum: [simple, dynamic, automated]
deprecated: true
description: Deprecated — retained for backward compatibility. Automation is determined by starlark_hook presence.
form_template:
type: object
stage_config:
@@ -1777,7 +1778,7 @@ paths:
enum: [full, summary, fresh]
stage_mode:
type: string
enum: [form, review, delegated, automated]
enum: [form, delegated, automated]
form_template:
type: object
transition_rules:
@@ -1817,7 +1818,7 @@ paths:
enum: [full, summary, fresh]
stage_mode:
type: string
enum: [form, review, delegated, automated]
enum: [form, delegated, automated]
form_template:
type: object
transition_rules:
@@ -3209,7 +3210,7 @@ paths:
enum: [full, summary, fresh]
stage_mode:
type: string
enum: [form, review, delegated, automated]
enum: [form, delegated, automated]
form_template:
type: object
transition_rules:
@@ -3250,7 +3251,7 @@ paths:
enum: [full, summary, fresh]
stage_mode:
type: string
enum: [form, review, delegated, automated]
enum: [form, delegated, automated]
form_template:
type: object
transition_rules:

View File

@@ -194,7 +194,7 @@ func TestStageCRUD(t *testing.T) {
WorkflowID: wf.ID,
Ordinal: 1,
Name: "Review",
StageMode: "review",
StageMode: "form",
}
s.CreateStage(ctx, stage2)
@@ -208,7 +208,7 @@ func TestStageCRUD(t *testing.T) {
if stages[0].Name != "Intake" || stages[0].StageMode != "form" {
t.Fatalf("stage 0: got %s/%s", stages[0].Name, stages[0].StageMode)
}
if stages[1].Name != "Review" || stages[1].StageMode != "review" {
if stages[1].Name != "Review" || stages[1].StageMode != "form" {
t.Fatalf("stage 1: got %s/%s", stages[1].Name, stages[1].StageMode)
}
@@ -243,7 +243,7 @@ func TestStageReorder(t *testing.T) {
s.Create(ctx, wf)
s1 := &models.WorkflowStage{WorkflowID: wf.ID, Ordinal: 0, Name: "First", StageMode: "form"}
s2 := &models.WorkflowStage{WorkflowID: wf.ID, Ordinal: 1, Name: "Second", StageMode: "review"}
s2 := &models.WorkflowStage{WorkflowID: wf.ID, Ordinal: 1, Name: "Second", StageMode: "form"}
s.CreateStage(ctx, s1)
s.CreateStage(ctx, s2)