Workflow independence audit, store tests, InstallPackage decomposition

Fix RenderWorkflow() handler (was a stub that never loaded data),
remove dead chat UI from workflow.html, align stage mode naming
with Go constants, add 17 SQLite store tests, decompose 400-line
InstallPackage into 7 phases, add E2E workflow-without-chat test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 22:08:46 +00:00
parent 3cdfdcf943
commit 5279227732
8 changed files with 1221 additions and 416 deletions

View File

@@ -25,6 +25,7 @@ import (
"github.com/gin-gonic/gin"
"armature/config"
"armature/models"
"armature/store"
)
@@ -631,18 +632,20 @@ func (e *Engine) RenderLogin() gin.HandlerFunc {
// WorkflowPageData is passed to the workflow.html template via PageData.Data.
type WorkflowPageData struct {
ChannelID string
ChannelTitle string
ChannelDescription string
SessionID string
SessionName string
StageMode string // custom | form_only | form_chat | review
StageName string
FormTemplateJSON string // typed form template JSON (empty if custom)
TotalStages int
CurrentStage int
SurfacePkgID string
BrandingJSON string
EntryToken string
WorkflowTitle string
WorkflowDescription string
SessionID string
SessionName string
StageMode string // form | review | delegated | automated
StageName string
FormTemplateJSON string // typed form template JSON (empty if delegated)
TotalStages int
CurrentStage int
SurfacePkgID string
BrandingJSON string
InstanceID string // workflow instance ID (used for API calls)
Status string // pending | active | completed | cancelled | stale
}
// WorkflowLandingPageData is passed to workflow-landing.html.
@@ -660,49 +663,91 @@ type WorkflowLandingPageData struct {
PersonaName string
PersonaIcon string
StageCount int
FirstStageMode string // custom | form_only | form_chat
FirstStageMode string // form | review | delegated | automated
ResumeURL string // non-empty if visitor has an active session
}
// RenderWorkflow renders the workflow entry page for anonymous session visitors.
// RenderWorkflow renders the workflow execution page for a running instance.
// Route: /w/:id — :id is either an instance ID or a public entry token.
// The middleware (AuthOrSession) has already created/resumed the session.
func (e *Engine) RenderWorkflow() gin.HandlerFunc {
return func(c *gin.Context) {
// :id is the public entry token (from /w/:id)
channelID := c.Param("id")
ctx := c.Request.Context()
entryToken := c.Param("id")
sessionID := c.GetString("session_id")
var title, description string
if title == "" {
title = "Workflow"
}
// Load session display name
sessionName := "Visitor"
// Load workflow stage info for form rendering
var stageMode, stageName, formTplJSON, surfacePkgID, brandingJSON string
var totalStages, currentStage int
stageMode = "custom" // default
instanceName, _, _ := e.loadBranding()
// Try to load instance by entry token, then by ID
var inst *models.WorkflowInstance
if e.stores.Workflows != nil {
inst, _ = e.stores.Workflows.GetInstanceByToken(ctx, entryToken)
if inst == nil {
inst, _ = e.stores.Workflows.GetInstance(ctx, entryToken)
}
}
if inst == nil {
c.String(http.StatusNotFound, "Workflow instance not found")
return
}
// Load workflow definition for title, description, branding
title, description := "Workflow", ""
var brandingJSON string
wf, _ := e.stores.Workflows.GetByID(ctx, inst.WorkflowID)
if wf != nil {
title = wf.Name
description = wf.Description
if len(wf.Branding) > 0 {
brandingJSON = string(wf.Branding)
}
}
// Load stages to resolve current stage info
var stageMode, stageName, formTplJSON, surfacePkgID string
var totalStages, currentStage int
stageMode = "form" // default
stages, _ := e.stores.Workflows.ListStages(ctx, inst.WorkflowID)
totalStages = len(stages)
for i, s := range stages {
if s.ID == inst.CurrentStage || s.Name == inst.CurrentStage {
currentStage = i
stageName = s.Name
stageMode = s.StageMode
if stageMode == "" {
stageMode = "form"
}
if len(s.FormTemplate) > 0 && string(s.FormTemplate) != "null" {
formTplJSON = string(s.FormTemplate)
}
if s.SurfacePkgID != nil {
surfacePkgID = *s.SurfacePkgID
}
break
}
}
e.Render(c, "workflow.html", PageData{
Surface: "workflow",
InstanceName: instanceName,
Data: WorkflowPageData{
ChannelID: channelID,
ChannelTitle: title,
ChannelDescription: description,
SessionID: sessionID,
SessionName: sessionName,
StageMode: stageMode,
StageName: stageName,
FormTemplateJSON: formTplJSON,
TotalStages: totalStages,
CurrentStage: currentStage,
SurfacePkgID: surfacePkgID,
BrandingJSON: brandingJSON,
EntryToken: entryToken,
WorkflowTitle: title,
WorkflowDescription: description,
SessionID: sessionID,
SessionName: sessionName,
StageMode: stageMode,
StageName: stageName,
FormTemplateJSON: formTplJSON,
TotalStages: totalStages,
CurrentStage: currentStage,
SurfacePkgID: surfacePkgID,
BrandingJSON: brandingJSON,
InstanceID: inst.ID,
Status: inst.Status,
},
})
}