277677f330
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
148 lines
7.6 KiB
Go
148 lines
7.6 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
taskmodels "github.com/kandev/kandev/internal/task/models"
|
|
taskservice "github.com/kandev/kandev/internal/task/service"
|
|
"github.com/kandev/kandev/pkg/pluginsdk"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestPluginHost_Tasks_CreateAttachesDependencyProjection(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}, APIRead: []string{"tasks"}})
|
|
d.taskWriter.created = &taskmodels.Task{ID: "task-9", WorkspaceID: "ws-1", WorkflowID: "wf-1", Title: "Investigate"}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-9": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
|
|
task, err := d.host.Tasks().Create(context.Background(), pluginsdk.CreateTaskInput{
|
|
WorkspaceID: "ws-1", WorkflowID: "wf-1", Title: "Investigate",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, task.Blocked)
|
|
require.Equal(t, taskservice.BlockedReasonPending, task.BlockedReason)
|
|
require.Equal(t, 1, d.tasks.dependencyViewsCalls, "Create derives via the unbounded variant")
|
|
require.Equal(t, 0, d.tasks.dependencyViewsBoundedCalls)
|
|
}
|
|
|
|
func TestPluginHost_Tasks_UpdateAttachesDependencyProjection(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}, APIRead: []string{"tasks"}})
|
|
d.taskWriter.updated = &taskmodels.Task{ID: "task-1", Title: "updated"}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: false, BlockedReason: ""},
|
|
}
|
|
|
|
got, err := d.host.Tasks().Update(context.Background(), pluginsdk.UpdateTaskInput{ID: "task-1"})
|
|
require.NoError(t, err)
|
|
require.False(t, got.Blocked)
|
|
require.Equal(t, 1, d.tasks.dependencyViewsCalls)
|
|
}
|
|
|
|
func TestPluginHost_Tasks_MoveAttachesDependencyProjectionOnTheResultingTask(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}, APIRead: []string{"tasks"}})
|
|
moved := &taskmodels.Task{ID: "task-1", WorkflowStepID: "step-2"}
|
|
d.taskWriter.moveResult = &TaskMoveResult{Task: moved, Transitioned: true}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: true, BlockedReason: taskservice.BlockedReasonFailed},
|
|
}
|
|
|
|
outcome, err := d.host.Tasks().Move(context.Background(), pluginsdk.MoveTaskInput{TaskID: "task-1", WorkflowStepID: "step-2"})
|
|
require.NoError(t, err)
|
|
require.True(t, outcome.Task.Blocked)
|
|
require.Equal(t, taskservice.BlockedReasonFailed, outcome.Task.BlockedReason)
|
|
require.Equal(t, 1, d.tasks.dependencyViewsCalls, "Move derives via the unbounded variant, once, on the object that was serialized")
|
|
}
|
|
|
|
func TestPluginHost_PluginOwnedTaskTreePreviewAttachesDependencyProjectionBounded(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}, APIRead: []string{"tasks"}})
|
|
root := &taskmodels.Task{ID: "root", WorkspaceID: "ws-1", Metadata: map[string]any{taskSourceMetadataKey: "plugin:p1"}}
|
|
d.tasks.tasksByID = map[string]*taskmodels.Task{"root": root}
|
|
d.tasks.tasksByWorkspace = map[string][]*taskmodels.Task{"ws-1": {root}}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"root": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
|
|
preview, err := d.host.PluginOwnedTaskTrees().Preview(context.Background(), "root")
|
|
require.NoError(t, err)
|
|
require.Len(t, preview, 1)
|
|
require.True(t, preview[0].Blocked)
|
|
require.Equal(t, 1, d.tasks.dependencyViewsBoundedCalls, "Preview is one of the flows F46 binds to the fan-out cap")
|
|
require.Equal(t, 0, d.tasks.dependencyViewsCalls)
|
|
}
|
|
|
|
func TestPluginHost_PluginOwnedTaskTreePreviewTranslatesFanOutRefusal(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}, APIRead: []string{"tasks"}})
|
|
root := &taskmodels.Task{ID: "root", WorkspaceID: "ws-1", Metadata: map[string]any{taskSourceMetadataKey: "plugin:p1"}}
|
|
d.tasks.tasksByID = map[string]*taskmodels.Task{"root": root}
|
|
d.tasks.tasksByWorkspace = map[string][]*taskmodels.Task{"ws-1": {root}}
|
|
d.tasks.dependencyViewsErr = taskservice.ErrDependencyFanOutExceeded
|
|
|
|
_, err := d.host.PluginOwnedTaskTrees().Preview(context.Background(), "root")
|
|
require.Error(t, err)
|
|
require.Equal(t, codes.ResourceExhausted, status.Code(err))
|
|
}
|
|
|
|
// ── write-only capability (no api_read:tasks) never sees real dependency
|
|
// data through a write RPC, per AC-PLUGINS-TASK-DEPS-001.11 ─────────────
|
|
|
|
func TestPluginHost_Tasks_CreateWithholdsDependencyProjectionWithoutReadCapability(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}})
|
|
d.taskWriter.created = &taskmodels.Task{ID: "task-9", WorkspaceID: "ws-1", WorkflowID: "wf-1", Title: "Investigate"}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-9": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
|
|
task, err := d.host.Tasks().Create(context.Background(), pluginsdk.CreateTaskInput{
|
|
WorkspaceID: "ws-1", WorkflowID: "wf-1", Title: "Investigate",
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, taskservice.BlockedReasonUnknown, task.BlockedReason, "a write-only caller must not read real dependency data through Create")
|
|
require.Equal(t, 0, d.tasks.dependencyViewsCalls)
|
|
}
|
|
|
|
func TestPluginHost_Tasks_UpdateWithholdsDependencyProjectionWithoutReadCapability(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}})
|
|
d.taskWriter.updated = &taskmodels.Task{ID: "task-1", Title: "updated"}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
|
|
got, err := d.host.Tasks().Update(context.Background(), pluginsdk.UpdateTaskInput{ID: "task-1"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, taskservice.BlockedReasonUnknown, got.BlockedReason, "a write-only caller must not read real dependency data through Update")
|
|
}
|
|
|
|
func TestPluginHost_Tasks_MoveWithholdsDependencyProjectionWithoutReadCapability(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}})
|
|
moved := &taskmodels.Task{ID: "task-1", WorkflowStepID: "step-2"}
|
|
d.taskWriter.moveResult = &TaskMoveResult{Task: moved, Transitioned: true}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"task-1": {Blocked: true, BlockedReason: taskservice.BlockedReasonFailed},
|
|
}
|
|
|
|
outcome, err := d.host.Tasks().Move(context.Background(), pluginsdk.MoveTaskInput{TaskID: "task-1", WorkflowStepID: "step-2"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, taskservice.BlockedReasonUnknown, outcome.Task.BlockedReason, "a write-only caller must not read real dependency data through Move")
|
|
}
|
|
|
|
func TestPluginHost_PluginOwnedTaskTreePreviewWithholdsDependencyProjectionWithoutReadCapability(t *testing.T) {
|
|
d := newTestDataHost(manifest.Capabilities{APIWrite: []string{"tasks"}})
|
|
root := &taskmodels.Task{ID: "root", WorkspaceID: "ws-1", Metadata: map[string]any{taskSourceMetadataKey: "plugin:p1"}}
|
|
d.tasks.tasksByID = map[string]*taskmodels.Task{"root": root}
|
|
d.tasks.tasksByWorkspace = map[string][]*taskmodels.Task{"ws-1": {root}}
|
|
d.tasks.dependencyViews = map[string]taskservice.DependencyView{
|
|
"root": {Blocked: true, BlockedReason: taskservice.BlockedReasonPending},
|
|
}
|
|
|
|
preview, err := d.host.PluginOwnedTaskTrees().Preview(context.Background(), "root")
|
|
require.NoError(t, err)
|
|
require.Len(t, preview, 1)
|
|
require.Equal(t, taskservice.BlockedReasonUnknown, preview[0].BlockedReason, "a write-only caller must not read real dependency data through Preview")
|
|
require.Equal(t, 0, d.tasks.dependencyViewsBoundedCalls)
|
|
}
|