739eabc49a
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
344 lines
13 KiB
Go
344 lines
13 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/canvas"
|
|
"github.com/kandev/kandev/internal/orchestrator"
|
|
"github.com/kandev/kandev/internal/plugins/instances"
|
|
"github.com/kandev/kandev/internal/plugins/manifest"
|
|
"github.com/kandev/kandev/internal/plugins/webapp"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
taskservice "github.com/kandev/kandev/internal/task/service"
|
|
"github.com/kandev/kandev/internal/worktree/copyfiles"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
func TestCanvasEditTargetDoesNotAuthorizeAnotherCanvas(t *testing.T) {
|
|
target := canvasEditSessionTarget{
|
|
Origin: canvasEditOrigin,
|
|
TaskID: "task-edit",
|
|
CanvasID: "canvas-a",
|
|
ReleaseID: "release-a",
|
|
}
|
|
|
|
if !canvasEditTargetMatches(target, "task-edit", "canvas-a") {
|
|
t.Fatal("expected the trusted target to authorize its canvas")
|
|
}
|
|
if canvasEditTargetMatches(target, "task-edit", "canvas-b") {
|
|
t.Fatal("trusted edit target authorized a different canvas")
|
|
}
|
|
if canvasEditTargetMatches(target, "other-task", "canvas-a") {
|
|
t.Fatal("trusted edit target authorized a different task")
|
|
}
|
|
}
|
|
|
|
func TestTaskCanvasAuthorizationUsesTaskIdentityNotCreatorSession(t *testing.T) {
|
|
task := &models.Task{ID: "task-current", WorkspaceID: "workspace-a"}
|
|
item := &canvas.Canvas{
|
|
ID: "canvas-task",
|
|
WorkspaceID: task.WorkspaceID,
|
|
TaskID: task.ID,
|
|
ScopeKind: canvas.ScopeTask,
|
|
CreatedBySessionID: "replacement-session-source",
|
|
}
|
|
if !taskCanvasMatchesTask(item, task) {
|
|
t.Fatal("same-task replacement session was denied by creator-session provenance")
|
|
}
|
|
item.TaskID = "other-task"
|
|
if taskCanvasMatchesTask(item, task) {
|
|
t.Fatal("canvas from another task was authorized")
|
|
}
|
|
item.TaskID = task.ID
|
|
item.WorkspaceID = "other-workspace"
|
|
if taskCanvasMatchesTask(item, task) {
|
|
t.Fatal("canvas from another workspace was authorized")
|
|
}
|
|
}
|
|
|
|
func TestCanvasEditSourceEntriesUseAssignedCanvasRoot(t *testing.T) {
|
|
entries := canvasEditSourceEntries("canvas-a", map[string][]byte{
|
|
"manifest.yaml": []byte("manifest"),
|
|
"index.html": []byte("index"),
|
|
})
|
|
|
|
got := make(map[string][]byte, len(entries))
|
|
for _, entry := range entries {
|
|
got[entry.RelPath] = entry.Content
|
|
if entry.Mode.Perm() != 0o600 {
|
|
t.Errorf("entry %q mode = %o, want 600", entry.RelPath, entry.Mode.Perm())
|
|
}
|
|
}
|
|
if string(got[".kandev/canvases/canvas-a/manifest.yaml"]) != "manifest" {
|
|
t.Fatalf("manifest was not rooted in the assigned canvas directory: %#v", got)
|
|
}
|
|
if string(got[".kandev/canvases/canvas-a/index.html"]) != "index" {
|
|
t.Fatalf("index was not rooted in the assigned canvas directory: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeBindingPermissionsIntersectCurrentGrants(t *testing.T) {
|
|
declared := &manifest.Manifest{Capabilities: manifest.Capabilities{
|
|
APIRead: []string{"tasks", "workflows"},
|
|
APIWrite: []string{"messages"},
|
|
State: true,
|
|
}}
|
|
grants := []instances.Grant{
|
|
{PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: instances.ScopeWorkspace},
|
|
{PermissionKind: "api_write", Resource: "messages", ScopeCeiling: instances.ScopeTask},
|
|
}
|
|
|
|
got := runtimeGrantedPermissions(declared, instances.ScopeWorkspace, grants)
|
|
if len(got) != 1 || got[0] != "api_read:tasks" {
|
|
t.Fatalf("runtime permissions = %#v, want only declared permissions covered by grants", got)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeNetworkOriginsUseSelectedWebAppDeclaration(t *testing.T) {
|
|
app := manifest.WebApp{NetworkOrigins: []string{
|
|
"https://api.example.com",
|
|
"https://unused.example.com",
|
|
}}
|
|
grants := []instances.Grant{
|
|
{PermissionKind: "network", NetworkOrigin: "https://api.example.com", ScopeCeiling: instances.ScopeTask},
|
|
{PermissionKind: "network", NetworkOrigin: "https://unrelated.example.com", ScopeCeiling: instances.ScopeTask},
|
|
}
|
|
got := runtimeNetworkOrigins(app, instances.ScopeTask, grants)
|
|
if len(got) != 1 || got[0] != "https://api.example.com" {
|
|
t.Fatalf("runtime network origins = %#v, want only the declared granted origin", got)
|
|
}
|
|
}
|
|
|
|
func TestCanvasEditLaunchFailureDeletesEphemeralTask(t *testing.T) {
|
|
tasks, canvasReader, releases, artifacts := newCanvasEditTestDependencies()
|
|
launcher := &fakeCanvasEditLauncher{launchErr: errors.New("agent unavailable")}
|
|
service := newCanvasEditService(
|
|
tasks, canvasReader, releases, artifacts, launcher,
|
|
&fakeCanvasEditSessionStore{}, &fakeCanvasEditExecutionResolver{},
|
|
)
|
|
|
|
if _, err := service.start(context.Background(), "canvas-a", "change the title"); err == nil {
|
|
t.Fatal("expected launch failure")
|
|
}
|
|
if len(tasks.deleted) != 1 || tasks.deleted[0] != "task-edit" {
|
|
t.Fatalf("deleted tasks = %#v, want the newly-created ephemeral task", tasks.deleted)
|
|
}
|
|
}
|
|
|
|
func TestCanvasEditMaterializesSourceAndDispatchesPrompt(t *testing.T) {
|
|
tasks, canvasReader, releases, artifacts := newCanvasEditTestDependencies()
|
|
launcher := &fakeCanvasEditLauncher{
|
|
launchResponse: &orchestrator.LaunchSessionResponse{
|
|
Success: true,
|
|
TaskID: "task-edit",
|
|
SessionID: "session-edit",
|
|
AgentExecutionID: "execution-edit",
|
|
},
|
|
}
|
|
sessions := &fakeCanvasEditSessionStore{}
|
|
copier := &fakeCanvasEditAgentCtl{}
|
|
resolver := &fakeCanvasEditExecutionResolver{client: copier}
|
|
service := newCanvasEditService(tasks, canvasReader, releases, artifacts, launcher, sessions, resolver)
|
|
|
|
result, err := service.start(context.Background(), "canvas-a", "change the title")
|
|
if err != nil {
|
|
t.Fatalf("start: %v", err)
|
|
}
|
|
if result.TaskID != "task-edit" || result.SessionID != "session-edit" || result.CanvasID != "canvas-a" {
|
|
t.Fatalf("result = %#v, want task/session/canvas IDs", result)
|
|
}
|
|
if tasks.createRequest == nil || !tasks.createRequest.IsEphemeral || tasks.createRequest.Origin != canvasEditOrigin {
|
|
t.Fatalf("create request = %#v, want ephemeral canvas-edit task", tasks.createRequest)
|
|
}
|
|
if tasks.createRequest.Metadata["origin"] != canvasEditOrigin || tasks.createRequest.Metadata["canvas_id"] != "canvas-a" {
|
|
t.Fatalf("task metadata = %#v, want canvas edit origin and canvas", tasks.createRequest.Metadata)
|
|
}
|
|
if launcher.launchRequest == nil {
|
|
t.Fatal("launch request was not recorded")
|
|
}
|
|
if launcher.launchRequest.AgentProfileID != "profile-default" || launcher.launchRequest.ExecutorID != "executor-default" {
|
|
t.Fatalf("launch defaults = profile %q executor %q", launcher.launchRequest.AgentProfileID, launcher.launchRequest.ExecutorID)
|
|
}
|
|
if launcher.prompt == "" || !containsCanvasEditText(launcher.prompt, "change the title") {
|
|
t.Fatalf("prompt = %q, want requested edit", launcher.prompt)
|
|
}
|
|
if !containsCanvasEditText(launcher.prompt, "api_read:tasks") || !containsCanvasEditText(launcher.prompt, "scope workspace") {
|
|
t.Fatalf("prompt = %q, want the effective grant projection", launcher.prompt)
|
|
}
|
|
target, ok := sessions.values[canvasEditSessionTargetMetadataKey].(canvasEditSessionTarget)
|
|
if !ok || !canvasEditTargetMatches(target, "task-edit", "canvas-a") {
|
|
t.Fatalf("trusted target metadata = %#v", sessions.values[canvasEditSessionTargetMetadataKey])
|
|
}
|
|
if len(copier.entries) != 2 {
|
|
t.Fatalf("copied entries = %#v, want active release source", copier.entries)
|
|
}
|
|
for _, entry := range copier.entries {
|
|
if entry.RelPath != ".kandev/canvases/canvas-a/index.html" && entry.RelPath != ".kandev/canvases/canvas-a/manifest.yaml" {
|
|
t.Fatalf("copied entry %q escaped assigned canvas root", entry.RelPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCanvasEditMaterializationFailureDeletesEphemeralTask(t *testing.T) {
|
|
tasks, canvasReader, releases, artifacts := newCanvasEditTestDependencies()
|
|
launcher := &fakeCanvasEditLauncher{
|
|
launchResponse: &orchestrator.LaunchSessionResponse{
|
|
Success: true,
|
|
TaskID: "task-edit",
|
|
SessionID: "session-edit",
|
|
AgentExecutionID: "execution-edit",
|
|
},
|
|
}
|
|
resolver := &fakeCanvasEditExecutionResolver{client: &fakeCanvasEditAgentCtl{err: errors.New("copy failed")}}
|
|
service := newCanvasEditService(
|
|
tasks, canvasReader, releases, artifacts, launcher,
|
|
&fakeCanvasEditSessionStore{}, resolver,
|
|
)
|
|
|
|
if _, err := service.start(context.Background(), "canvas-a", "change the title"); err == nil {
|
|
t.Fatal("expected source materialization failure")
|
|
}
|
|
if len(tasks.deleted) != 1 || tasks.deleted[0] != "task-edit" {
|
|
t.Fatalf("deleted tasks = %#v, want the newly-created ephemeral task", tasks.deleted)
|
|
}
|
|
}
|
|
|
|
func containsCanvasEditText(value, want string) bool {
|
|
return len(value) >= len(want) && (value == want || len(value) > len(want) && stringContains(value, want))
|
|
}
|
|
|
|
func stringContains(value, want string) bool {
|
|
for i := 0; i+len(want) <= len(value); i++ {
|
|
if value[i:i+len(want)] == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newCanvasEditTestDependencies() (*fakeCanvasEditTaskStore, *fakeCanvasEditCanvasStore, *fakeCanvasEditReleaseStore, *fakeCanvasEditArtifactStore) {
|
|
return &fakeCanvasEditTaskStore{
|
|
workspace: &models.Workspace{
|
|
ID: "workspace-a",
|
|
DefaultAgentProfileID: stringPointer("profile-default"),
|
|
DefaultExecutorID: stringPointer("executor-default"),
|
|
},
|
|
}, &fakeCanvasEditCanvasStore{canvas: &canvas.Canvas{
|
|
ID: "canvas-a",
|
|
PluginInstanceID: "instance-a",
|
|
WorkspaceID: "workspace-a",
|
|
ScopeKind: canvas.ScopeWorkspace,
|
|
Status: instances.StatusActive,
|
|
ActiveReleaseID: "release-a",
|
|
ActiveReleaseStatus: instances.ValidationValid,
|
|
EffectiveGrants: []canvas.GrantProjection{{
|
|
PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: instances.ScopeWorkspace,
|
|
}},
|
|
}}, &fakeCanvasEditReleaseStore{release: instances.Release{
|
|
ID: "release-a",
|
|
InstanceID: "instance-a",
|
|
PackageDigest: "digest-a",
|
|
ArtifactPath: "releases/digest-a",
|
|
ValidationStatus: instances.ValidationValid,
|
|
}}, &fakeCanvasEditArtifactStore{files: map[string][]byte{
|
|
"manifest.yaml": []byte("manifest"),
|
|
"index.html": []byte("index"),
|
|
}}
|
|
}
|
|
|
|
func stringPointer(value string) *string { return &value }
|
|
|
|
type fakeCanvasEditTaskStore struct {
|
|
workspace *models.Workspace
|
|
createRequest *taskservice.CreateTaskRequest
|
|
deleted []string
|
|
}
|
|
|
|
func (f *fakeCanvasEditTaskStore) AuthorizeWorkspaceAccess(context.Context, string) error { return nil }
|
|
|
|
func (f *fakeCanvasEditTaskStore) GetWorkspace(context.Context, string) (*models.Workspace, error) {
|
|
return f.workspace, nil
|
|
}
|
|
|
|
func (f *fakeCanvasEditTaskStore) CreateTask(_ context.Context, request *taskservice.CreateTaskRequest) (taskservice.CreateTaskResult, error) {
|
|
f.createRequest = request
|
|
return taskservice.CreateTaskResult{
|
|
Task: &models.Task{ID: "task-edit", WorkspaceID: "workspace-a"},
|
|
Outcome: taskservice.CreateTaskOutcomeCreated,
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeCanvasEditTaskStore) DeleteTask(_ context.Context, taskID string) error {
|
|
f.deleted = append(f.deleted, taskID)
|
|
return nil
|
|
}
|
|
|
|
type fakeCanvasEditCanvasStore struct{ canvas *canvas.Canvas }
|
|
|
|
func (f *fakeCanvasEditCanvasStore) Get(context.Context, string) (*canvas.Canvas, error) {
|
|
return f.canvas, nil
|
|
}
|
|
|
|
type fakeCanvasEditReleaseStore struct{ release instances.Release }
|
|
|
|
func (f *fakeCanvasEditReleaseStore) GetRelease(context.Context, string) (instances.Release, error) {
|
|
return f.release, nil
|
|
}
|
|
|
|
type fakeCanvasEditArtifactStore struct{ files map[string][]byte }
|
|
|
|
func (f *fakeCanvasEditArtifactStore) ReadFiles(webapp.Artifact) (map[string][]byte, error) {
|
|
return f.files, nil
|
|
}
|
|
|
|
type fakeCanvasEditLauncher struct {
|
|
launchErr error
|
|
launchResponse *orchestrator.LaunchSessionResponse
|
|
launchRequest *orchestrator.LaunchSessionRequest
|
|
prompt string
|
|
}
|
|
|
|
func (f *fakeCanvasEditLauncher) LaunchSession(_ context.Context, request *orchestrator.LaunchSessionRequest) (*orchestrator.LaunchSessionResponse, error) {
|
|
f.launchRequest = request
|
|
return f.launchResponse, f.launchErr
|
|
}
|
|
|
|
func (f *fakeCanvasEditLauncher) PromptTask(_ context.Context, _, _, prompt, _ string, _ bool, _ []v1.MessageAttachment, _ bool) (*orchestrator.PromptResult, error) {
|
|
f.prompt = prompt
|
|
return &orchestrator.PromptResult{}, nil
|
|
}
|
|
|
|
type fakeCanvasEditSessionStore struct{ values map[string]interface{} }
|
|
|
|
func (f *fakeCanvasEditSessionStore) SetSessionMetadataKey(_ context.Context, _, key string, value interface{}) error {
|
|
if f.values == nil {
|
|
f.values = make(map[string]interface{})
|
|
}
|
|
f.values[key] = value
|
|
return nil
|
|
}
|
|
|
|
type fakeCanvasEditExecutionResolver struct{ client canvasEditAgentCtl }
|
|
|
|
func (f *fakeCanvasEditExecutionResolver) ResolveAgentCtl(string) (canvasEditAgentCtl, error) {
|
|
if f.client == nil {
|
|
return nil, errors.New("agentctl unavailable")
|
|
}
|
|
return f.client, nil
|
|
}
|
|
|
|
type fakeCanvasEditAgentCtl struct {
|
|
entries []copyfiles.Entry
|
|
err error
|
|
}
|
|
|
|
func (f *fakeCanvasEditAgentCtl) CopyFiles(_ context.Context, _ string, entries []copyfiles.Entry) (canvasCopyFilesResult, error) {
|
|
f.entries = append([]copyfiles.Entry(nil), entries...)
|
|
if f.err != nil {
|
|
return canvasCopyFilesResult{}, f.err
|
|
}
|
|
return canvasCopyFilesResult{Present: true}, nil
|
|
}
|