fix(projectinstance): resolve legacy task keys against namespaced snapshots
New workflow instances store bd_* task keys, but chat still sends the old keys. Scope and document access now treat those aliases as the same task so a live instance no longer fails closed on an exact string miss. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package projectinstance
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,7 +69,7 @@ func (s *Service) CheckDocTaskAccess(
|
||||
var stageKey string
|
||||
for _, st := range detail.Stages {
|
||||
for i := range st.DocTasks {
|
||||
if st.DocTasks[i].TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
|
||||
copyDoc := st.DocTasks[i]
|
||||
docInst = ©Doc
|
||||
stageKey = st.StageKey
|
||||
@@ -119,7 +121,7 @@ func (s *Service) CheckDocTaskAccess(
|
||||
func findDocTaskPlan(plan WorkflowPlan, taskKey string) *DocTaskPlan {
|
||||
for _, st := range plan.Stages {
|
||||
for i := range st.DocTasks {
|
||||
if st.DocTasks[i].TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(st.DocTasks[i].TaskKey, taskKey) {
|
||||
return &st.DocTasks[i]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -144,7 +146,7 @@ func (s *Service) ResolveMessageScopeForTask(ctx context.Context, projectID int6
|
||||
var matches []MessageScope
|
||||
for _, st := range detail.Stages {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(dt.TaskKey, taskKey) {
|
||||
matches = append(matches, MessageScope{
|
||||
ScopeKind: ScopeWorkflowStage,
|
||||
WorkflowInstanceID: gov.ID,
|
||||
|
||||
@@ -122,6 +122,70 @@ func TestMessageVisible_LegacyUnscopedOwnerWhenRestricted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMessageScopeForTask_LegacyKeyMatchesCanonicalSnapshot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
_ = SeedProjectMeta(store, 42, LifecyclePlanning, "", 1, 0)
|
||||
svc := NewService(store, testResolver(), nil, nil)
|
||||
mustActivateTestPlan(t, svc, 42, 1)
|
||||
|
||||
store.mu.Lock()
|
||||
live := store.live[42]
|
||||
live.ConfigSnapshot.Stages[0].DocTasks[0].TaskKey = "bd_intake"
|
||||
for i := range store.stages[live.ID] {
|
||||
for j := range store.stages[live.ID][i].tasks {
|
||||
if store.stages[live.ID][i].tasks[j].taskKey == "intake" {
|
||||
store.stages[live.ID][i].tasks[j].taskKey = "bd_intake"
|
||||
}
|
||||
}
|
||||
}
|
||||
store.mu.Unlock()
|
||||
|
||||
scope, err := svc.ResolveMessageScopeForTask(ctx, 42, "intake")
|
||||
if err != nil {
|
||||
t.Fatalf("legacy intake must resolve against bd_intake snapshot: %v", err)
|
||||
}
|
||||
if scope.ScopeKind != ScopeWorkflowStage || scope.StageKey != "V0.1_triage" {
|
||||
t.Fatalf("want V0.1_triage workflow_stage, got %+v", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMessageScopeForTask_CanonicalAliasAmbiguousAcrossStages(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
_ = SeedProjectMeta(store, 43, LifecyclePlanning, "", 1, 0)
|
||||
svc := NewService(store, testResolver(), nil, nil)
|
||||
mustActivateTestPlan(t, svc, 43, 1)
|
||||
|
||||
store.mu.Lock()
|
||||
live := store.live[43]
|
||||
live.ConfigSnapshot.Stages = append(live.ConfigSnapshot.Stages, StagePlan{
|
||||
StageKey: "V0.5_feasibility", Enabled: true,
|
||||
DocTasks: []DocTaskPlan{{TaskKey: "bd_intake", Enabled: true}},
|
||||
})
|
||||
store.stages[live.ID] = append(store.stages[live.ID], memStageRecord{
|
||||
id: live.ID + 100, stageKey: "V0.5_feasibility", position: 1, enabled: true, status: StagePending,
|
||||
tasks: []memDocTaskRecord{{id: 900, taskKey: "bd_intake", enabled: true, status: "pending"}},
|
||||
})
|
||||
store.mu.Unlock()
|
||||
|
||||
_, err := svc.ResolveMessageScopeForTask(ctx, 43, "intake")
|
||||
if err == nil || !errors.Is(err, ErrTaskScopeNotResolved) {
|
||||
t.Fatalf("same task in two stages must stay unresolved, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskKeyToStage_LegacyAlias(t *testing.T) {
|
||||
plan := validPlan()
|
||||
plan.Stages[0].DocTasks[0].TaskKey = "bd_user_requirements"
|
||||
if got := TaskKeyToStage(plan, "user_requirements"); got != "V0.1_triage" {
|
||||
t.Fatalf("TaskKeyToStage=%q", got)
|
||||
}
|
||||
if got := TaskKeyToStage(plan, "intake"); got != "" {
|
||||
t.Fatalf("unrelated legacy key must miss, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeFromMetadata_RoundTrip(t *testing.T) {
|
||||
meta := map[string]any{
|
||||
"scope_kind": ScopeWorkflowStage,
|
||||
|
||||
@@ -152,7 +152,7 @@ func MessageStageKey(metadata map[string]any) string {
|
||||
func TaskKeyToStage(plan WorkflowPlan, taskKey string) string {
|
||||
for _, st := range plan.Stages {
|
||||
for _, dt := range st.DocTasks {
|
||||
if dt.TaskKey == taskKey {
|
||||
if workflowdef.SameTaskKey(dt.TaskKey, taskKey) {
|
||||
return st.StageKey
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user