Files
weichenandCursor f168e44ce7 feat(projectinstance): allow live workflow config edits except switching
Activated projects can update duties, approval policy, and document enablement. Changing the workflow definition stays rejected, and project 143 remains frozen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 05:20:01 +00:00

116 lines
3.5 KiB
Go

package projectinstance
import (
"context"
"errors"
"testing"
"chat-agent-system/internal/workflowdef"
)
type countingMaterializer struct {
calls int
projectID int64
}
func (c *countingMaterializer) MaterializeTemplateDocs(_ context.Context, projectID int64, _ WorkflowPlan) error {
c.calls++
c.projectID = projectID
return nil
}
func TestUpdateLivePlanKeepsWorkflowAndMaterializes(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 42, LifecyclePlanning, "", 1, 0)
mat := &countingMaterializer{}
svc := NewService(store, testResolver(), nil, nil)
svc.SetTemplateDocMaterializer(mat)
if _, err := svc.SaveWorkflowPlan(ctx, 42, 1, true, SavePlanInput{
Plan: validPlan(), ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
}); err != nil {
t.Fatal(err)
}
preview, err := svc.ActivationPreview(ctx, 42, 1, true)
if err != nil {
t.Fatal(err)
}
if _, err := svc.Activate(ctx, 42, 1, true, ActivateInput{
ConfirmationToken: preview.ConfirmationToken,
ExpectedProjectVersion: preview.ExpectedProjectVersion,
ExpectedPlanRevision: preview.ExpectedPlanRevision,
}); err != nil {
t.Fatal(err)
}
if mat.calls != 1 || mat.projectID != 42 {
t.Fatalf("activate materialize calls=%d project=%d", mat.calls, mat.projectID)
}
settings, err := svc.GetSettings(ctx, 42, 1, true)
if err != nil {
t.Fatal(err)
}
if settings.PlanEditMode != PlanEditLive {
t.Fatalf("plan_edit_mode=%s", settings.PlanEditMode)
}
plan := *settings.Plan
plan.Stages[0].DocTasks[0].Duties.ApproverUserID = 4
updated, err := svc.UpdateLivePlan(ctx, 42, 1, true, SavePlanInput{
Plan: plan, ExpectedPlanRevision: settings.WorkflowPlanRevision, LiveEdit: true,
})
if err != nil {
t.Fatal(err)
}
if updated.Plan.Stages[0].DocTasks[0].Duties.ApproverUserID != 4 {
t.Fatalf("duty not updated: %+v", updated.Plan.Stages[0].DocTasks[0].Duties)
}
if updated.Plan.WorkflowDefinitionKey != plan.WorkflowDefinitionKey {
t.Fatal("workflow key changed")
}
if mat.calls != 2 {
t.Fatalf("live update materialize calls=%d", mat.calls)
}
switched := plan
switched.WorkflowDefinitionKey = "other"
_, err = svc.UpdateLivePlan(ctx, 42, 1, true, SavePlanInput{
Plan: switched, ExpectedPlanRevision: updated.WorkflowPlanRevision,
})
if !errors.Is(err, ErrWorkflowSwitchFrozen) {
t.Fatalf("expected switch frozen, got %v", err)
}
}
func TestUpdateLivePlanRejectsReadonlyProject(t *testing.T) {
svc := NewService(newMemStore(), testResolver(), nil, nil)
_, err := svc.UpdateLivePlan(context.Background(), readonlyLivePlanProjectID, 1, true, SavePlanInput{
Plan: validPlan(), ExpectedPlanRevision: 1, LiveEdit: true,
})
if !errors.Is(err, ErrPlanFrozen) {
t.Fatalf("expected frozen, got %v", err)
}
}
func TestPlanEditModeFrozenForReadonlyLiveProject(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, readonlyLivePlanProjectID, LifecycleActive, workflowdef.OfficialKeyMain, 2, 1)
store.live[readonlyLivePlanProjectID] = &WorkflowInstance{
ID: 89,
ProjectID: readonlyLivePlanProjectID,
WorkflowVersionID: 1,
Status: InstanceActive,
ConfigRevision: 3,
ConfigSnapshot: validPlan(),
}
svc := NewService(store, testResolver(), nil, nil)
settings, err := svc.GetSettings(ctx, readonlyLivePlanProjectID, 1, true)
if err != nil {
t.Fatal(err)
}
if settings.PlanEditMode != PlanEditFrozen {
t.Fatalf("plan_edit_mode=%s", settings.PlanEditMode)
}
}