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>
137 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|