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:
weichen
2026-09-21 05:03:08 +00:00
co-authored by Cursor
parent e91ffbca1d
commit 8d22fa3827
4 changed files with 72 additions and 4 deletions
+4 -2
View File
@@ -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 = &copyDoc
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]
}
}
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -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
}
}