639 lines
29 KiB
Go
639 lines
29 KiB
Go
package instances
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
"github.com/kandev/kandev/internal/db"
|
|
userstore "github.com/kandev/kandev/internal/user/store"
|
|
)
|
|
|
|
func newTestStore(t *testing.T) *Store {
|
|
t.Helper()
|
|
conn, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "instances.db"))
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
conn.SetMaxOpenConns(1)
|
|
pool := db.NewPool(sqlx.NewDb(conn, "sqlite3"), sqlx.NewDb(conn, "sqlite3"))
|
|
t.Cleanup(func() { _ = pool.Close() })
|
|
store, err := NewStore(pool)
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
return store
|
|
}
|
|
|
|
func TestValidateScopeRequiresOnlyIdentifiersForScope(t *testing.T) {
|
|
if err := ValidateScope(ScopeTask, ScopeIdentifiers{WorkspaceID: "workspace-1", TaskID: "task-1"}); err != nil {
|
|
t.Fatalf("task scope unexpectedly invalid: %v", err)
|
|
}
|
|
if err := ValidateScope(ScopeTask, ScopeIdentifiers{TaskID: "task-1"}); !errors.Is(err, ErrInvalidScope) {
|
|
t.Fatalf("missing workspace error = %v, want ErrInvalidScope", err)
|
|
}
|
|
if err := ValidateScope(ScopeWorkspace, ScopeIdentifiers{WorkspaceID: "workspace-1", TaskID: "task-1"}); !errors.Is(err, ErrInvalidScope) {
|
|
t.Fatalf("mixed workspace error = %v, want ErrInvalidScope", err)
|
|
}
|
|
}
|
|
|
|
func TestInstanceDataScopeIsIndependentAndDefaultsToPlacement(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "workspace-data", PluginID: "canvas-board", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask, DataScopeKind: ScopeWorkspace, WorkspaceID: "workspace-1",
|
|
TaskID: "task-1", Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("Create workspace-data task canvas: %v", err)
|
|
}
|
|
workspaceData, err := store.Get(ctx, "workspace-data")
|
|
if err != nil {
|
|
t.Fatalf("Get workspace-data task canvas: %v", err)
|
|
}
|
|
if workspaceData.ScopeKind != ScopeTask || workspaceData.EffectiveDataScopeKind() != ScopeWorkspace {
|
|
t.Fatalf("scopes = placement %q, effective data %q, want task and workspace", workspaceData.ScopeKind, workspaceData.EffectiveDataScopeKind())
|
|
}
|
|
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "legacy", PluginID: "canvas-board", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-2", Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("Create legacy task canvas: %v", err)
|
|
}
|
|
legacy, err := store.Get(ctx, "legacy")
|
|
if err != nil {
|
|
t.Fatalf("Get legacy task canvas: %v", err)
|
|
}
|
|
if legacy.DataScopeKind != "" || legacy.EffectiveDataScopeKind() != ScopeTask {
|
|
t.Fatalf("legacy scopes = explicit %q, effective %q, want empty and task", legacy.DataScopeKind, legacy.EffectiveDataScopeKind())
|
|
}
|
|
}
|
|
|
|
func TestInstanceRejectsUntrustedDataScopeCombinations(t *testing.T) {
|
|
store := newTestStore(t)
|
|
for _, instance := range []Instance{
|
|
{ID: "installed", PluginID: "plugin", SourceKind: SourceInstalled, ScopeKind: ScopeTask, DataScopeKind: ScopeWorkspace, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusActive},
|
|
{ID: "session-data", PluginID: "canvas", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, DataScopeKind: ScopeSession, WorkspaceID: "workspace-1", TaskID: "task-2", Status: StatusActive},
|
|
{ID: "instance", PluginID: "canvas", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, DataScopeKind: ScopeInstance, WorkspaceID: "workspace-1", TaskID: "task-3", Status: StatusActive},
|
|
} {
|
|
if err := store.Create(context.Background(), instance); !errors.Is(err, ErrInvalidScope) {
|
|
t.Errorf("Create(%s) = %v, want ErrInvalidScope", instance.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExistingInstanceTableMigratesWithTaskDataScopeFallback(t *testing.T) {
|
|
connection, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "legacy-instances.db"))
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
connection.SetMaxOpenConns(1)
|
|
pool := db.NewPool(sqlx.NewDb(connection, "sqlite3"), sqlx.NewDb(connection, "sqlite3"))
|
|
t.Cleanup(func() { _ = pool.Close() })
|
|
if _, err := pool.Writer().Exec(`CREATE TABLE plugin_instances (
|
|
id TEXT PRIMARY KEY, plugin_id TEXT NOT NULL, source_kind TEXT NOT NULL, scope_kind TEXT NOT NULL,
|
|
workspace_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', session_id TEXT NOT NULL DEFAULT '',
|
|
repository_id TEXT NOT NULL DEFAULT '', status TEXT NOT NULL, active_release_id TEXT NOT NULL DEFAULT '',
|
|
grant_generation INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
|
|
)`); err != nil {
|
|
t.Fatalf("create legacy plugin_instances schema: %v", err)
|
|
}
|
|
created := time.Now().UTC().Format(time.RFC3339Nano)
|
|
if _, err := pool.Writer().Exec(`INSERT INTO plugin_instances
|
|
(id, plugin_id, source_kind, scope_kind, workspace_id, task_id, status, created_at, updated_at)
|
|
VALUES ('legacy', 'canvas', 'local_canvas', 'task', 'workspace-1', 'task-1', 'active', ?, ?)`, created, created); err != nil {
|
|
t.Fatalf("insert legacy instance: %v", err)
|
|
}
|
|
store, err := NewStore(pool)
|
|
if err != nil {
|
|
t.Fatalf("NewStore migrates legacy table: %v", err)
|
|
}
|
|
instance, err := store.Get(context.Background(), "legacy")
|
|
if err != nil {
|
|
t.Fatalf("Get legacy instance: %v", err)
|
|
}
|
|
if instance.DataScopeKind != "" || instance.EffectiveDataScopeKind() != ScopeTask {
|
|
t.Fatalf("legacy scopes = explicit %q, effective %q, want empty and task", instance.DataScopeKind, instance.EffectiveDataScopeKind())
|
|
}
|
|
}
|
|
|
|
func TestEnableWorkspaceDataRequiresCurrentOwnerReview(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if _, err := store.db.Exec(`CREATE TABLE workspaces (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL DEFAULT '')`); err != nil {
|
|
t.Fatalf("create workspaces table: %v", err)
|
|
}
|
|
if _, err := store.db.Exec(`INSERT INTO workspaces (id, owner_id) VALUES ('workspace-1', 'owner-1')`); err != nil {
|
|
t.Fatalf("insert workspace: %v", err)
|
|
}
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-1", PluginID: "canvas", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("create task canvas: %v", err)
|
|
}
|
|
declared := json.RawMessage(`{"reads":["tasks"]}`)
|
|
if err := store.CreateRelease(ctx, Release{
|
|
ID: "release-1", PluginID: "canvas", InstanceID: "instance-1", PackageDigest: "digest-1",
|
|
SourceKind: SourceLocalCanvas, ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: declared,
|
|
ArtifactPath: "releases/one", ArtifactBytes: 1, ValidationStatus: ValidationValid,
|
|
}); err != nil {
|
|
t.Fatalf("create active release: %v", err)
|
|
}
|
|
if err := store.AddGrant(ctx, Grant{InstanceID: "instance-1", PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: ScopeTask, ApprovedBy: "owner-1"}); err != nil {
|
|
t.Fatalf("add task grant: %v", err)
|
|
}
|
|
if err := store.AddGrant(ctx, Grant{InstanceID: "instance-1", PermissionKind: "api_write", Resource: "messages", ScopeCeiling: ScopeTask, ApprovedBy: "owner-1"}); err != nil {
|
|
t.Fatalf("add stale task grant: %v", err)
|
|
}
|
|
if err := store.SetActiveRelease(ctx, "instance-1", "release-1"); err != nil {
|
|
t.Fatalf("activate release: %v", err)
|
|
}
|
|
instanceBeforeReview, err := store.Get(ctx, "instance-1")
|
|
if err != nil {
|
|
t.Fatalf("get instance before review: %v", err)
|
|
}
|
|
|
|
workspaceGrant := []Grant{{InstanceID: "instance-1", PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: ScopeWorkspace}}
|
|
apply := func(userID string, generation int64, grants []Grant) error {
|
|
return store.WithTransaction(ctx, func(tx *sqlx.Tx) error {
|
|
return store.EnableWorkspaceDataReviewedTx(ctx, tx, "instance-1", "workspace-1", userID, grants, "release-1", permissionDigest(string(declared)), generation)
|
|
})
|
|
}
|
|
if err := apply("owner-2", instanceBeforeReview.GrantGeneration, workspaceGrant); !errors.Is(err, ErrWorkspaceOwnerRequired) {
|
|
t.Fatalf("foreign owner confirmation = %v, want ErrWorkspaceOwnerRequired", err)
|
|
}
|
|
if err := apply("owner-1", instanceBeforeReview.GrantGeneration+1, workspaceGrant); !errors.Is(err, ErrStaleWorkspaceDataReview) {
|
|
t.Fatalf("stale review confirmation = %v, want ErrStaleWorkspaceDataReview", err)
|
|
}
|
|
undeclared := append(append([]Grant(nil), workspaceGrant...), Grant{InstanceID: "instance-1", PermissionKind: "api_write", Resource: "messages", ScopeCeiling: ScopeWorkspace})
|
|
if err := apply("owner-1", instanceBeforeReview.GrantGeneration, undeclared); !errors.Is(err, ErrInvalidScope) {
|
|
t.Fatalf("undeclared grant confirmation = %v, want ErrInvalidScope", err)
|
|
}
|
|
if err := apply("owner-1", instanceBeforeReview.GrantGeneration, workspaceGrant); err != nil {
|
|
t.Fatalf("owner confirmation: %v", err)
|
|
}
|
|
|
|
instance, err := store.Get(ctx, "instance-1")
|
|
if err != nil {
|
|
t.Fatalf("get upgraded instance: %v", err)
|
|
}
|
|
if instance.ScopeKind != ScopeTask || instance.DataScopeKind != ScopeWorkspace || instance.ActiveReleaseID != "release-1" || instance.GrantGeneration != instanceBeforeReview.GrantGeneration+1 {
|
|
t.Fatalf("upgraded instance = %+v, want task placement, workspace data, same release, generation increment", instance)
|
|
}
|
|
grants, err := store.ListGrants(ctx, "instance-1")
|
|
if err != nil {
|
|
t.Fatalf("list upgraded grants: %v", err)
|
|
}
|
|
if len(grants) != 1 || grants[0].ScopeCeiling != ScopeWorkspace || grants[0].ApprovedBy != "owner-1" {
|
|
t.Fatalf("upgraded grants = %+v, want one owner-approved workspace task grant", grants)
|
|
}
|
|
}
|
|
|
|
func TestVerifyWorkspaceOwnerAllowsAuthDisabledUnownedWorkspace(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if _, err := store.db.Exec(`CREATE TABLE workspaces (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL DEFAULT '')`); err != nil {
|
|
t.Fatalf("create workspaces table: %v", err)
|
|
}
|
|
if _, err := store.db.Exec(`INSERT INTO workspaces (id, owner_id) VALUES ('workspace-1', '')`); err != nil {
|
|
t.Fatalf("insert unowned workspace: %v", err)
|
|
}
|
|
if err := store.WithTransaction(ctx, func(tx *sqlx.Tx) error {
|
|
return verifyWorkspaceOwnerTx(ctx, tx, "workspace-1", userstore.DefaultUserID)
|
|
}); err != nil {
|
|
t.Fatalf("default user review of unowned workspace: %v", err)
|
|
}
|
|
if err := store.WithTransaction(ctx, func(tx *sqlx.Tx) error {
|
|
return verifyWorkspaceOwnerTx(ctx, tx, "workspace-1", "member-1")
|
|
}); !errors.Is(err, ErrWorkspaceOwnerRequired) {
|
|
t.Fatalf("ordinary user review of unowned workspace = %v, want ErrWorkspaceOwnerRequired", err)
|
|
}
|
|
}
|
|
|
|
func TestCreateInstanceAdmissionIsAtomicAtTaskAndWorkspaceLimits(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
var (
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
created int
|
|
lastErr error
|
|
)
|
|
for i := 0; i < MaxTaskInstances+5; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
instance := Instance{
|
|
ID: "instance-" + time.Now().Format("150405.000000000") + "-" + string(rune('a'+i)),
|
|
PluginID: "canvas-board",
|
|
SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask,
|
|
WorkspaceID: "workspace-1",
|
|
TaskID: "task-1",
|
|
Status: StatusActive,
|
|
}
|
|
if err := store.Create(ctx, instance); err != nil {
|
|
mu.Lock()
|
|
lastErr = err
|
|
mu.Unlock()
|
|
return
|
|
}
|
|
mu.Lock()
|
|
created++
|
|
mu.Unlock()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if created != MaxTaskInstances {
|
|
t.Fatalf("created = %d, want %d (last error %v)", created, MaxTaskInstances, lastErr)
|
|
}
|
|
count, err := store.CountActive(ctx, ScopeTask, "task-1")
|
|
if err != nil {
|
|
t.Fatalf("CountActive: %v", err)
|
|
}
|
|
if count != MaxTaskInstances {
|
|
t.Fatalf("task count = %d, want %d", count, MaxTaskInstances)
|
|
}
|
|
}
|
|
|
|
func TestArchiveRestoreUsesTheSameAdmission(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
for i := 0; i < MaxTaskInstances-1; i++ {
|
|
if err := store.Create(ctx, Instance{ID: "instance-" + string(rune('a'+i)), PluginID: "p", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, WorkspaceID: "w", TaskID: "t", Status: StatusActive}); err != nil {
|
|
t.Fatalf("Create(%d): %v", i, err)
|
|
}
|
|
}
|
|
if err := store.Archive(ctx, "instance-a"); err != nil {
|
|
t.Fatalf("Archive: %v", err)
|
|
}
|
|
if err := store.Restore(ctx, "instance-a"); err != nil {
|
|
t.Fatalf("Restore: %v", err)
|
|
}
|
|
if err := store.Create(ctx, Instance{ID: "instance-z", PluginID: "p", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, WorkspaceID: "w", TaskID: "t", Status: StatusActive}); err != nil {
|
|
t.Fatalf("fill task limit: %v", err)
|
|
}
|
|
if err := store.Archive(ctx, "instance-b"); err != nil {
|
|
t.Fatalf("second Archive: %v", err)
|
|
}
|
|
if err := store.Restore(ctx, "instance-b"); !errors.Is(err, ErrTaskCanvasLimit) {
|
|
t.Fatalf("second Restore = %v, want ErrTaskCanvasLimit", err)
|
|
}
|
|
}
|
|
|
|
func TestReserveBytesRejectsWorkspaceAndInstallationOverages(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
first, err := store.ReserveBytes(ctx, "workspace-1", 8, 10, 100)
|
|
if err != nil {
|
|
t.Fatalf("first reservation: %v", err)
|
|
}
|
|
if _, err := store.ReserveBytes(ctx, "workspace-1", 3, 10, 100); !errors.Is(err, ErrWorkspaceStorageLimit) {
|
|
t.Fatalf("workspace overage = %v, want ErrWorkspaceStorageLimit", err)
|
|
}
|
|
if _, err := store.ReserveBytes(ctx, "workspace-2", 93, 100, 100); !errors.Is(err, ErrInstallationStorageLimit) {
|
|
t.Fatalf("installation overage = %v, want ErrInstallationStorageLimit", err)
|
|
}
|
|
if err := store.ReleaseBytes(ctx, first.ID); err != nil {
|
|
t.Fatalf("release reservation: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReserveBytesCountsRetainedReleaseArtifactsAfterReservationRelease(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-1",
|
|
PluginID: "canvas-board",
|
|
SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeWorkspace,
|
|
WorkspaceID: "workspace-1",
|
|
Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{
|
|
ID: "release-1",
|
|
PluginID: "canvas-board",
|
|
InstanceID: "instance-1",
|
|
PackageDigest: "digest-1",
|
|
SourceKind: SourceLocalCanvas,
|
|
SourceActorKind: "agent",
|
|
ArtifactPath: "releases/digest-1",
|
|
ArtifactBytes: 8,
|
|
ValidationStatus: ValidationValid,
|
|
}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
|
|
reservation, err := store.ReserveBytes(ctx, "workspace-1", 2, 10, 10)
|
|
if err != nil {
|
|
t.Fatalf("reserve remaining capacity: %v", err)
|
|
}
|
|
if err := store.ReleaseBytes(ctx, reservation.ID); err != nil {
|
|
t.Fatalf("release reservation: %v", err)
|
|
}
|
|
if _, err := store.ReserveBytes(ctx, "workspace-1", 3, 10, 10); !errors.Is(err, ErrWorkspaceStorageLimit) {
|
|
t.Fatalf("workspace reservation after release = %v, want ErrWorkspaceStorageLimit", err)
|
|
}
|
|
if _, err := store.ReserveBytes(ctx, "workspace-2", 3, 10, 10); !errors.Is(err, ErrInstallationStorageLimit) {
|
|
t.Fatalf("installation reservation after release = %v, want ErrInstallationStorageLimit", err)
|
|
}
|
|
}
|
|
|
|
func TestReserveBytesDoesNotDoubleCountSharedRetainedArtifacts(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
for _, instance := range []Instance{
|
|
{ID: "instance-1", PluginID: "canvas-board", SourceKind: SourceLocalCanvas, ScopeKind: ScopeWorkspace, WorkspaceID: "workspace-1", Status: StatusActive},
|
|
{ID: "instance-2", PluginID: "canvas-board", SourceKind: SourceLocalCanvas, ScopeKind: ScopeWorkspace, WorkspaceID: "workspace-2", Status: StatusActive},
|
|
} {
|
|
if err := store.Create(ctx, instance); err != nil {
|
|
t.Fatalf("Create(%s): %v", instance.ID, err)
|
|
}
|
|
}
|
|
for _, release := range []Release{
|
|
{ID: "release-1", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-shared", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/shared", ArtifactBytes: 8, ValidationStatus: ValidationValid},
|
|
{ID: "release-2", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-shared", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/shared", ArtifactBytes: 8, ValidationStatus: ValidationValid},
|
|
{ID: "release-3", PluginID: "canvas-board", InstanceID: "instance-2", PackageDigest: "digest-shared", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/shared", ArtifactBytes: 8, ValidationStatus: ValidationValid},
|
|
} {
|
|
if err := store.CreateRelease(ctx, release); err != nil {
|
|
t.Fatalf("CreateRelease(%s): %v", release.ID, err)
|
|
}
|
|
}
|
|
|
|
workspaceReservation, err := store.ReserveBytes(ctx, "workspace-1", 2, 10, 10)
|
|
if err != nil {
|
|
t.Fatalf("reserve shared workspace capacity: %v", err)
|
|
}
|
|
if err := store.ReleaseBytes(ctx, workspaceReservation.ID); err != nil {
|
|
t.Fatalf("release workspace reservation: %v", err)
|
|
}
|
|
installationReservation, err := store.ReserveBytes(ctx, "workspace-2", 2, 10, 10)
|
|
if err != nil {
|
|
t.Fatalf("reserve shared installation capacity: %v", err)
|
|
}
|
|
if err := store.ReleaseBytes(ctx, installationReservation.ID); err != nil {
|
|
t.Fatalf("release installation reservation: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPruneReleasesFreesQuotaAfterRemovingSupersededArtifact(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-1",
|
|
PluginID: "canvas-board",
|
|
SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeWorkspace,
|
|
WorkspaceID: "workspace-1",
|
|
Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
base := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
for _, release := range []Release{
|
|
{ID: "release-old", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-old", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/old", ArtifactBytes: 4, ValidationStatus: ValidationValid, CreatedAt: base.Add(time.Second)},
|
|
{ID: "release-prior", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-prior", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/prior", ArtifactBytes: 4, ValidationStatus: ValidationValid, CreatedAt: base.Add(2 * time.Second)},
|
|
{ID: "release-active", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-active", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/active", ArtifactBytes: 4, ValidationStatus: ValidationValid, CreatedAt: base.Add(3 * time.Second)},
|
|
} {
|
|
if err := store.CreateRelease(ctx, release); err != nil {
|
|
t.Fatalf("CreateRelease(%s): %v", release.ID, err)
|
|
}
|
|
}
|
|
if err := store.SetActiveRelease(ctx, "instance-1", "release-active"); err != nil {
|
|
t.Fatalf("SetActiveRelease: %v", err)
|
|
}
|
|
if _, err := store.ReserveBytes(ctx, "workspace-1", 1, 10, 10); !errors.Is(err, ErrWorkspaceStorageLimit) {
|
|
t.Fatalf("reservation before prune = %v, want ErrWorkspaceStorageLimit", err)
|
|
}
|
|
if err := store.PruneReleases(ctx, "instance-1"); err != nil {
|
|
t.Fatalf("PruneReleases: %v", err)
|
|
}
|
|
reservation, err := store.ReserveBytes(ctx, "workspace-1", 1, 10, 10)
|
|
if err != nil {
|
|
t.Fatalf("reservation after prune: %v", err)
|
|
}
|
|
if err := store.ReleaseBytes(ctx, reservation.ID); err != nil {
|
|
t.Fatalf("release reservation: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReconcileArtifactsMarksRetainedReleaseUnavailable(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{ID: "instance-1", PluginID: "canvas-board", SourceKind: SourceLocalCanvas, ScopeKind: ScopeWorkspace, WorkspaceID: "workspace-1", Status: StatusActive}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{ID: "release-1", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-1", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ArtifactPath: "releases/digest-1", ValidationStatus: ValidationValid}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
marked, err := store.ReconcileArtifacts(ctx, func(path, digest string, bytes int64) (ArtifactCheck, error) {
|
|
if path != "releases/digest-1" || digest != "digest-1" || bytes != 0 {
|
|
t.Fatalf("checker arguments = %q, %q, %d", path, digest, bytes)
|
|
}
|
|
return ArtifactCheck{Reason: "missing"}, nil
|
|
})
|
|
if err != nil || marked != 1 {
|
|
t.Fatalf("ReconcileArtifacts() = %d, %v; want one mark", marked, err)
|
|
}
|
|
release, err := store.GetRelease(ctx, "release-1")
|
|
if err != nil {
|
|
t.Fatalf("GetRelease: %v", err)
|
|
}
|
|
if release.ValidationStatus != ValidationUnavailable || release.ValidationError != "missing" {
|
|
t.Fatalf("release after reconcile = %+v", release)
|
|
}
|
|
}
|
|
|
|
func TestCleanupJobsCanBeClaimedRetriedAndCompleted(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.AddCleanupJob(ctx, CleanupJob{ID: "cleanup-1", InstanceID: "instance-1", ArtifactPath: "releases/artifact"}); err != nil {
|
|
t.Fatalf("AddCleanupJob: %v", err)
|
|
}
|
|
|
|
claimed, ok, err := store.ClaimCleanupJob(ctx, time.Now().UTC())
|
|
if err != nil || !ok {
|
|
t.Fatalf("ClaimCleanupJob() = %+v, %v, %t; want a claim", claimed, err, ok)
|
|
}
|
|
if claimed.Status != CleanupRunning || claimed.Attempts != 1 {
|
|
t.Fatalf("claimed job = %+v, want running attempt 1", claimed)
|
|
}
|
|
if _, ok, err := store.ClaimCleanupJob(ctx, time.Now().UTC()); err != nil || ok {
|
|
t.Fatalf("second ClaimCleanupJob() = %v, %t; want no duplicate claim", err, ok)
|
|
}
|
|
|
|
next := time.Now().UTC().Add(time.Hour)
|
|
if err := store.RetryCleanupJob(ctx, claimed.ID, next, errors.New("remove failed")); err != nil {
|
|
t.Fatalf("RetryCleanupJob: %v", err)
|
|
}
|
|
if _, ok, err := store.ClaimCleanupJob(ctx, time.Now().UTC()); err != nil || ok {
|
|
t.Fatalf("early retry claim = %v, %t; want no claim", err, ok)
|
|
}
|
|
retried, ok, err := store.ClaimCleanupJob(ctx, next)
|
|
if err != nil || !ok || retried.Attempts != 2 {
|
|
t.Fatalf("retry claim = %+v, %v, %t; want attempt 2", retried, err, ok)
|
|
}
|
|
if err := store.CompleteCleanupJob(ctx, retried.ID); err != nil {
|
|
t.Fatalf("CompleteCleanupJob: %v", err)
|
|
}
|
|
jobs, err := store.ListCleanupJobs(ctx)
|
|
if err != nil {
|
|
t.Fatalf("ListCleanupJobs: %v", err)
|
|
}
|
|
if len(jobs) != 1 || jobs[0].Status != CleanupCompleted || jobs[0].LastError != "remove failed" {
|
|
t.Fatalf("cleanup jobs = %+v, want one completed job with diagnostic", jobs)
|
|
}
|
|
}
|
|
|
|
func TestReleaseActivationRejectsBroaderGrants(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{ID: "instance-1", PluginID: "canvas-board", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusActive}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
declared := json.RawMessage(`{"reads":["tasks"],"shared_state":false,"external_origins":["https://api.example.com"]}`)
|
|
if err := store.CreateRelease(ctx, Release{ID: "release-1", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-1", SourceKind: SourceLocalCanvas, SourceActorKind: "agent", ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: declared, ArtifactPath: "releases/digest-1", ArtifactBytes: 1, ValidationStatus: ValidationValid}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
if err := store.AddGrant(ctx, Grant{InstanceID: "instance-1", PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: ScopeTask, ApprovedBy: "user-1"}); err != nil {
|
|
t.Fatalf("AddGrant: %v", err)
|
|
}
|
|
if err := store.AddGrant(ctx, Grant{InstanceID: "instance-1", PermissionKind: "network", NetworkOrigin: "https://api.example.com", ScopeCeiling: ScopeWorkspace, ApprovedBy: "user-1"}); err != nil {
|
|
t.Fatalf("AddGrant network preapproval: %v", err)
|
|
}
|
|
if err := store.ActivateRelease(ctx, "instance-1", "release-1"); !errors.Is(err, ErrInvalidRelease) {
|
|
t.Fatalf("ActivateRelease with broader grant = %v, want ErrInvalidRelease", err)
|
|
}
|
|
instance, err := store.Get(ctx, "instance-1")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if instance.ActiveReleaseID != "" {
|
|
t.Fatalf("active release = %q after rejected activation", instance.ActiveReleaseID)
|
|
}
|
|
}
|
|
|
|
func TestReleaseActivationContractsRemovedPermissions(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{ID: "instance-1", PluginID: "canvas-board", SourceKind: SourceLocalCanvas, ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusActive}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.AddGrant(ctx, Grant{InstanceID: "instance-1", PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: ScopeTask, ApprovedBy: "user-1"}); err != nil {
|
|
t.Fatalf("AddGrant: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{ID: "release-1", PluginID: "canvas-board", InstanceID: "instance-1", PackageDigest: "digest-1", SourceKind: SourceLocalCanvas, ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: json.RawMessage(`{}`), ArtifactPath: "releases/digest-1", ArtifactBytes: 1, ValidationStatus: ValidationValid}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
if err := store.ActivateRelease(ctx, "instance-1", "release-1"); err != nil {
|
|
t.Fatalf("ActivateRelease: %v", err)
|
|
}
|
|
instance, err := store.Get(ctx, "instance-1")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if instance.ActiveReleaseID != "release-1" {
|
|
t.Fatalf("active release = %q, want release-1", instance.ActiveReleaseID)
|
|
}
|
|
}
|
|
|
|
func TestCreateReleaseIfActiveReleaseTxRejectsChangedAuthority(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-authority", PluginID: "canvas-board", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusActive,
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{
|
|
ID: "release-authority-base", PluginID: "canvas-board", InstanceID: "instance-authority",
|
|
PackageDigest: "digest-base", SourceKind: SourceLocalCanvas, SourceActorKind: "agent",
|
|
ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: json.RawMessage(`{}`),
|
|
ArtifactPath: "releases/base", ArtifactBytes: 1, ValidationStatus: ValidationValid,
|
|
}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
if err := store.SetActiveRelease(ctx, "instance-authority", "release-authority-base"); err != nil {
|
|
t.Fatalf("SetActiveRelease: %v", err)
|
|
}
|
|
captured, err := store.Get(ctx, "instance-authority")
|
|
if err != nil {
|
|
t.Fatalf("capture publish authority: %v", err)
|
|
}
|
|
expected := captured.PublishAuthority()
|
|
if err := store.SetScope(ctx, "instance-authority", ScopeWorkspace, ScopeIdentifiers{WorkspaceID: "workspace-1"}); err != nil {
|
|
t.Fatalf("promote scope: %v", err)
|
|
}
|
|
|
|
err = store.WithTransaction(ctx, func(tx *sqlx.Tx) error {
|
|
return store.CreateReleaseIfAuthorityTx(ctx, tx, "instance-authority", expected, Release{
|
|
ID: "release-authority-stale", PluginID: "canvas-board", InstanceID: "instance-authority",
|
|
PackageDigest: "digest-stale", SourceKind: SourceLocalCanvas, SourceActorKind: "agent",
|
|
ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: json.RawMessage(`{}`),
|
|
ArtifactPath: "releases/stale", ArtifactBytes: 1, ValidationStatus: ValidationValid,
|
|
})
|
|
})
|
|
if !errors.Is(err, ErrStaleCanvasPublish) {
|
|
t.Fatalf("stale task authority error = %v, want ErrStaleCanvasPublish", err)
|
|
}
|
|
}
|
|
|
|
func TestActivateReleaseRejectsArchivedInstance(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-archived-activate", PluginID: "canvas-board", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeWorkspace, WorkspaceID: "workspace-1", Status: StatusArchived,
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{
|
|
ID: "release-archived-activate", PluginID: "canvas-board", InstanceID: "instance-archived-activate",
|
|
PackageDigest: "digest-archived-activate", SourceKind: SourceLocalCanvas, SourceActorKind: "agent",
|
|
ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: json.RawMessage(`{}`),
|
|
ArtifactPath: "releases/archived-activate", ArtifactBytes: 1, ValidationStatus: ValidationValid,
|
|
}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
if err := store.ActivateRelease(ctx, "instance-archived-activate", "release-archived-activate"); err == nil {
|
|
t.Fatal("archived instance was reactivated without Restore")
|
|
}
|
|
}
|
|
|
|
func TestApproveReleaseRejectsArchivedInstance(t *testing.T) {
|
|
store := newTestStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, Instance{
|
|
ID: "instance-archived-approve", PluginID: "canvas-board", SourceKind: SourceLocalCanvas,
|
|
ScopeKind: ScopeTask, WorkspaceID: "workspace-1", TaskID: "task-1", Status: StatusArchived,
|
|
}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if err := store.CreateRelease(ctx, Release{
|
|
ID: "release-archived-approve", PluginID: "canvas-board", InstanceID: "instance-archived-approve",
|
|
PackageDigest: "digest-archived-approve", SourceKind: SourceLocalCanvas, SourceActorKind: "agent",
|
|
ManifestJSON: json.RawMessage(`{}`), DeclaredPermissionsJSON: json.RawMessage(`{"reads":["tasks"]}`),
|
|
ArtifactPath: "releases/archived-approve", ArtifactBytes: 1, ValidationStatus: ValidationPendingPermission,
|
|
}); err != nil {
|
|
t.Fatalf("CreateRelease: %v", err)
|
|
}
|
|
if err := store.ApproveRelease(ctx, "instance-archived-approve", "release-archived-approve", "user-1", []Grant{{
|
|
PermissionKind: "api_read", Resource: "tasks", ScopeCeiling: ScopeTask,
|
|
}}); err == nil {
|
|
t.Fatal("archived instance was approved and activated without Restore")
|
|
}
|
|
}
|