Files
Agent-System/internal/projectinstance/message_scope_test.go
T
weichenandCursor 5096581651 fix(projectinstance): resolve pre-activation task scope via governing three-state
Never-activated projects were allowed by access ABAC but rejected with
TASK_SCOPE_UNRESOLVED 409; use GetGoverningInstanceForAccess so nil maps to
ScopePreActivation, live to stage scope, and closed stays unresolved.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 12:59:10 +00:00

137 lines
4.5 KiB
Go

package projectinstance
import (
"context"
"errors"
"testing"
)
func TestResolveMessageScopeForTask_NeverActivatedPreActivation(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 6071, LifecyclePlanning, "", 86, 0)
svc := NewService(store, testResolver(), nil, nil)
scope, err := svc.ResolveMessageScopeForTask(ctx, 6071, "intake")
if err != nil {
t.Fatalf("never-activated must resolve: %v", err)
}
if scope == nil || scope.ScopeKind != ScopePreActivation {
t.Fatalf("want pre_activation, got %+v", scope)
}
if !MessageVisible(*scope, &StageAccess{Restricted: false}, 86, 86) {
t.Fatal("session owner must see pre_activation task messages")
}
if MessageVisible(*scope, &StageAccess{Restricted: false}, 86, 99) {
t.Fatal("non-owner must not see pre_activation task messages")
}
}
func TestResolveMessageScopeForTask_LiveStageScope(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)
scope, err := svc.ResolveMessageScopeForTask(ctx, 42, "intake")
if err != nil {
t.Fatalf("live resolve: %v", err)
}
if scope.ScopeKind != ScopeWorkflowStage || scope.StageKey == "" || scope.WorkflowInstanceID <= 0 {
t.Fatalf("want workflow_stage with ids, got %+v", scope)
}
}
func TestResolveMessageScopeForTask_ClosedNotPreActivation(t *testing.T) {
ctx := context.Background()
for _, status := range []string{InstanceCompleted, InstanceTerminated} {
t.Run(status, func(t *testing.T) {
store := newMemStore()
_ = SeedProjectMeta(store, 71, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
mustActivateTestPlan(t, svc, 71, 1)
live, err := store.GetLiveInstance(ctx, 71)
if err != nil || live == nil {
t.Fatalf("live before close: %v", err)
}
live.Status = status
ms := store
ms.mu.Lock()
ms.live[71] = live
ms.mu.Unlock()
// access gate 先拒
d := svc.CheckDocTaskAccess(ctx, 71, 1, "intake", DocAccessActionRun, true)
if d.Allowed || d.Code != AccessCodeInstanceClosed {
t.Fatalf("access gate must close, got %+v", d)
}
// 直接调用层也不得当预激活
_, scopeErr := svc.ResolveMessageScopeForTask(ctx, 71, "intake")
if scopeErr == nil || !errors.Is(scopeErr, ErrTaskScopeNotResolved) {
t.Fatalf("closed must ErrTaskScopeNotResolved, got %v", scopeErr)
}
})
}
}
func TestResolveMessageScopeForTask_StorageErrorPropagates(t *testing.T) {
want := errors.New("db down")
svc := NewService(&errGoverningStore{err: want}, testResolver(), nil, nil)
_, err := svc.ResolveMessageScopeForTask(context.Background(), 1, "intake")
if err == nil || !errors.Is(err, want) {
t.Fatalf("want storage error %v, got %v", want, err)
}
}
func TestMessageVisible_PreActivationOwnerOnly(t *testing.T) {
access := &StageAccess{Restricted: true, AllowedStageKeys: map[string]bool{"V0.1_triage": true}}
scope := MessageScope{ScopeKind: ScopePreActivation}
if !MessageVisible(scope, access, 9, 9) {
t.Fatal("owner should see pre_activation messages")
}
if MessageVisible(scope, access, 9, 99) {
t.Fatal("non-owner must not see pre_activation messages")
}
}
func TestMessageVisible_WorkflowStageRestricted(t *testing.T) {
access := &StageAccess{
Restricted: true,
AllowedStageKeys: map[string]bool{"V0.1_triage": true},
}
scope := MessageScope{ScopeKind: ScopeWorkflowStage, StageKey: "V0.1_triage"}
if !MessageVisible(scope, access, 9, 9) {
t.Fatal("allowed stage should be visible")
}
scope.StageKey = "V0.5_feasibility"
if MessageVisible(scope, access, 9, 9) {
t.Fatal("disallowed stage must be hidden")
}
}
func TestMessageVisible_LegacyUnscopedOwnerWhenRestricted(t *testing.T) {
access := &StageAccess{Restricted: true}
scope := MessageScope{ScopeKind: ScopeLegacyUnscoped}
if !MessageVisible(scope, access, 9, 9) {
t.Fatal("owner should see legacy_unscoped when restricted")
}
if MessageVisible(scope, access, 9, 99) {
t.Fatal("non-owner must not see legacy_unscoped when restricted")
}
}
func TestScopeFromMetadata_RoundTrip(t *testing.T) {
meta := map[string]any{
"scope_kind": ScopeWorkflowStage,
"workflow_instance_id": float64(89),
"stage_instance_id": float64(201),
"stage_key": "V0.1_triage",
}
scope := ScopeFromMetadata(meta)
if scope.ScopeKind != ScopeWorkflowStage || scope.WorkflowInstanceID != 89 || scope.StageInstanceID != 201 || scope.StageKey != "V0.1_triage" {
t.Fatalf("unexpected scope: %+v", scope)
}
}