Files

704 lines
25 KiB
Go

//go:build sqlite || sqliteonly
package sqlitestore
import (
"context"
"database/sql"
"errors"
"path/filepath"
"reflect"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
func TestSQLiteSkillStore_StoreMissingDeps_PersistsForCustomSkills(t *testing.T) {
ctx, skillStore := newTestSQLiteSkillStore(t)
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Custom Skill",
Slug: "custom-skill",
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "custom-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
missing := []string{"pip:requests", "npm:tsx"}
if err := skillStore.StoreMissingDeps(ctx, skillID, missing); err != nil {
t.Fatalf("StoreMissingDeps error: %v", err)
}
info, ok := skillStore.GetSkillByID(ctx, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if !reflect.DeepEqual(info.MissingDeps, missing) {
t.Fatalf("MissingDeps = %v, want %v", info.MissingDeps, missing)
}
}
func TestSQLiteSkillStore_CreateSkillManaged_PersistsArchivedDependencyState(t *testing.T) {
ctx, skillStore := newTestSQLiteSkillStore(t)
missing := []string{"pip:requests"}
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Archived Skill",
Slug: "archived-skill",
OwnerID: "user-1",
Visibility: "private",
Status: "archived",
MissingDeps: missing,
FilePath: filepath.Join(t.TempDir(), "archived-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
info, ok := skillStore.GetSkillByID(ctx, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if info.Status != "archived" {
t.Fatalf("Status = %q, want archived", info.Status)
}
if !reflect.DeepEqual(info.MissingDeps, missing) {
t.Fatalf("MissingDeps = %v, want %v", info.MissingDeps, missing)
}
}
func TestSQLiteSkillStore_UpsertSystemSkill_DoesNotReclassifyCustomSkill(t *testing.T) {
ctx, skillStore := newTestSQLiteSkillStore(t)
customID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Lark PM",
Slug: "lark-pm",
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "lark-pm", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
returnedID, changed, _, err := skillStore.UpsertSystemSkill(ctx, store.SkillCreateParams{
Name: "Bundled Lark PM",
Slug: "lark-pm",
Status: "active",
Version: 2,
FilePath: filepath.Join(t.TempDir(), "lark-pm", "2"),
})
if !errors.Is(err, store.ErrSystemSkillSlugConflict) {
t.Fatalf("UpsertSystemSkill error = %v, want ErrSystemSkillSlugConflict", err)
}
if changed {
t.Fatal("UpsertSystemSkill changed custom skill")
}
if returnedID != customID {
t.Fatalf("UpsertSystemSkill ID = %s, want custom skill ID %s", returnedID, customID)
}
custom, ok := skillStore.GetSkillByID(ctx, customID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if custom.IsSystem {
t.Fatal("custom skill was reclassified as a system skill")
}
if custom.Version != 1 {
t.Fatalf("custom version = %d, want 1", custom.Version)
}
if custom.Visibility != "private" {
t.Fatalf("custom visibility = %q, want private", custom.Visibility)
}
}
func TestSQLiteSkillStore_UpsertSystemSkill_DoesNotReclassifyOtherTenantCustomSkill(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantID, _ := seedSQLiteTenantAgent(t, db)
customCtx := store.WithTenantID(context.Background(), tenantID)
customID, err := skillStore.CreateSkillManaged(customCtx, store.SkillCreateParams{
Name: "Lark Playbook",
Slug: "lark-playbook",
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "tenant-lark-playbook", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
_, changed, _, err := skillStore.UpsertSystemSkill(context.Background(), store.SkillCreateParams{
Name: "Bundled Lark Playbook",
Slug: "lark-playbook",
Status: "active",
Version: 1,
FilePath: filepath.Join(t.TempDir(), "bundled-lark-playbook", "1"),
})
if err != nil {
t.Fatalf("UpsertSystemSkill error: %v", err)
}
if !changed {
t.Fatal("UpsertSystemSkill did not add the master system skill")
}
custom, ok := skillStore.GetSkillByID(customCtx, customID)
if !ok || custom.IsSystem {
t.Fatalf("custom skill = %+v, found = %t; want unchanged custom skill", custom, ok)
}
var systemCount int
if err := db.QueryRow("SELECT COUNT(*) FROM skills WHERE slug = ? AND is_system = 1", "lark-playbook").Scan(&systemCount); err != nil {
t.Fatalf("count system skills: %v", err)
}
if systemCount != 1 {
t.Fatalf("system skill count = %d, want 1", systemCount)
}
}
func TestEnsureSchema_RestoresMisclassifiedCustomSkills(t *testing.T) {
db, err := OpenDB(filepath.Join(t.TempDir(), "skills.db"))
if err != nil {
t.Fatalf("OpenDB error: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if err := EnsureSchema(db); err != nil {
t.Fatalf("EnsureSchema initial error: %v", err)
}
if _, err := db.Exec(
`INSERT INTO skills (id, name, slug, owner_id, tenant_id, visibility, version, status, is_system, file_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
uuid.New().String(), "Lark PM", "lark-pm", "user-1", store.MasterTenantID.String(), "public", 5, "active", true, filepath.Join(t.TempDir(), "lark-pm", "5"),
); err != nil {
t.Fatalf("insert misclassified skill: %v", err)
}
if _, err := db.Exec("UPDATE schema_version SET version = 57"); err != nil {
t.Fatalf("set prior schema version: %v", err)
}
if err := EnsureSchema(db); err != nil {
t.Fatalf("EnsureSchema migration error: %v", err)
}
var isSystem bool
var visibility string
var version int
var status string
var recoveryMarker string
var filePath string
if err := db.QueryRow(`SELECT is_system, visibility, version, status,
COALESCE(json_extract(frontmatter, '$._goclaw_recovery'), ''), file_path
FROM skills WHERE slug = ?`, "lark-pm").Scan(&isSystem, &visibility, &version, &status, &recoveryMarker, &filePath); err != nil {
t.Fatalf("query repaired skill: %v", err)
}
if isSystem {
t.Fatal("misclassified custom skill remained a system skill")
}
if version != 4 {
t.Fatalf("repaired skill version = %d, want 4", version)
}
if visibility != "private" {
t.Fatalf("repaired skill visibility = %q, want private", visibility)
}
if status != "archived" {
t.Fatalf("repaired skill status = %q, want archived", status)
}
if recoveryMarker != store.SkillRecoveryBundledSlugCollision {
t.Fatalf("recovery marker = %q, want bundled_slug_collision", recoveryMarker)
}
if filePath != filepath.Join(filepath.Dir(filePath), "4") {
t.Fatalf("repaired skill path = %q, want version 4 directory", filePath)
}
skillStore := NewSQLiteSkillStore(db, t.TempDir())
repairedID, changed, _, err := skillStore.UpsertSystemSkill(context.Background(), store.SkillCreateParams{
Name: "Bundled Lark PM",
Slug: "lark-pm",
Status: "active",
Version: 5,
FilePath: filepath.Join(filepath.Dir(filePath), "5"),
})
if !errors.Is(err, store.ErrMisclassifiedCustomSkill) {
t.Fatalf("UpsertSystemSkill error = %v, want ErrMisclassifiedCustomSkill", err)
}
if changed || repairedID == uuid.Nil {
t.Fatalf("UpsertSystemSkill = (%s, %t), want repaired custom ID and unchanged row", repairedID, changed)
}
}
func TestSQLiteSkillStore_GetSkill_ArchivedFoundByUUIDAndSlugAndName(t *testing.T) {
// "archived" means a system skill has missing dependencies (see internal/skills/seeder.go);
// it is still enabled and must be discoverable the same way as via ListSkills() and
// GetSkillByID(), which both include active + archived skills. Regression test for a bug
// where archived-but-enabled skills (e.g. docx, pdf, pptx, skill-creator, xlsx when deps
// are missing) 404'd when looked up by name/slug while UUID lookup worked fine.
ctx, skillStore := newTestSQLiteSkillStore(t)
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Archived Detail",
Slug: "archived-detail",
OwnerID: "user-1",
Visibility: "private",
Status: "archived",
FilePath: filepath.Join(t.TempDir(), "archived-detail", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
info, ok := skillStore.GetSkill(ctx, skillID.String())
if !ok {
t.Fatal("GetSkill by UUID returned !ok for archived skill")
}
if info.Status != "archived" {
t.Fatalf("Status = %q, want archived", info.Status)
}
slugInfo, ok := skillStore.GetSkill(ctx, "archived-detail")
if !ok {
t.Fatal("GetSkill by slug returned !ok for archived skill; want consistency with ListSkills/GetSkillByID inclusion policy")
}
if slugInfo.Status != "archived" {
t.Fatalf("Status = %q, want archived", slugInfo.Status)
}
nameInfo, ok := skillStore.GetSkill(ctx, "Archived Detail")
if !ok {
t.Fatal("GetSkill by name returned !ok for archived skill; want consistency with ListSkills/GetSkillByID inclusion policy")
}
if nameInfo.Status != "archived" {
t.Fatalf("Status = %q, want archived", nameInfo.Status)
}
}
func TestSQLiteSkillStore_ListSkills_ResolvesCreatorAgentWithinTenant(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantID, agentID := seedSQLiteTenantAgent(t, db)
if _, err := db.Exec(`UPDATE agents SET display_name = ? WHERE id = ?`, "Creator Agent", agentID.String()); err != nil {
t.Fatalf("update agent display_name: %v", err)
}
ctx := store.WithTenantID(context.Background(), tenantID)
if _, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Verified Creator",
Slug: "verified-creator",
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "verified-creator", "1"),
Frontmatter: map[string]string{
"created_by_agent_id": agentID.String(),
"created_by_agent": "Spoofed Name",
},
}); err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
if _, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Spoofed Creator",
Slug: "spoofed-creator",
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "spoofed-creator", "1"),
Frontmatter: map[string]string{
"created_by_agent": "Only A String",
},
}); err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
list := skillStore.ListSkills(ctx)
bySlug := map[string]store.SkillInfo{}
for _, info := range list {
bySlug[info.Slug] = info
}
verified := bySlug["verified-creator"].CreatorAgent
if verified == nil {
t.Fatal("CreatorAgent = nil, want verified creator")
}
if verified.ID != agentID.String() || verified.DisplayName != "Creator Agent" {
t.Fatalf("CreatorAgent = %+v, want resolved DB agent", verified)
}
if got := bySlug["spoofed-creator"].CreatorAgent; got != nil {
t.Fatalf("CreatorAgent = %+v, want nil for display-only spoof", got)
}
}
func TestSQLiteSkillStore_GrantToAgentRejectsCrossTenantSkill(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantA, agentA := seedSQLiteTenantAgent(t, db)
tenantB, _ := seedSQLiteTenantAgent(t, db)
ctxA := store.WithTenantID(context.Background(), tenantA)
ctxB := store.WithTenantID(context.Background(), tenantB)
skillID, err := skillStore.CreateSkillManaged(ctxB, store.SkillCreateParams{
Name: "Tenant B Skill",
Slug: "tenant-b-skill-" + tenantB.String()[:8],
OwnerID: "user-1",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "tenant-b-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
if err := skillStore.GrantToAgent(ctxA, skillID, agentA, 1, "user-1", true); err == nil {
t.Fatal("GrantToAgent allowed tenant A to grant tenant B skill")
}
grants, err := skillStore.ListAgentGrantsForSkill(ctxB, skillID)
if err != nil {
t.Fatalf("ListAgentGrantsForSkill error: %v", err)
}
if len(grants) != 0 {
t.Fatalf("cross-tenant grant was inserted: %+v", grants)
}
got, ok := skillStore.GetSkillByID(ctxB, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if got.Visibility != "private" {
t.Fatalf("cross-tenant grant changed visibility to %q, want private", got.Visibility)
}
}
func TestSQLiteSkillStore_RevokeFromAgentDoesNotDemoteCrossTenantSkill(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantA, agentA := seedSQLiteTenantAgent(t, db)
tenantB, _ := seedSQLiteTenantAgent(t, db)
ctxA := store.WithTenantID(context.Background(), tenantA)
ctxB := store.WithTenantID(context.Background(), tenantB)
skillID, err := skillStore.CreateSkillManaged(ctxB, store.SkillCreateParams{
Name: "Tenant B Skill",
Slug: "tenant-b-revoke-skill-" + tenantB.String()[:8],
OwnerID: "user-1",
Visibility: "internal",
FilePath: filepath.Join(t.TempDir(), "tenant-b-revoke-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
if err := skillStore.RevokeFromAgent(ctxA, skillID, agentA); err == nil {
t.Fatal("RevokeFromAgent allowed tenant A to revoke tenant B skill")
}
got, ok := skillStore.GetSkillByID(ctxB, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if got.Visibility != "internal" {
t.Fatalf("cross-tenant revoke demoted visibility to %q, want internal", got.Visibility)
}
}
func TestSQLiteSkillStore_RevokeFromAgentKeepsInternalWhenUserGrantRemains(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantID, agentID := seedSQLiteTenantAgent(t, db)
ctx := store.WithTenantID(context.Background(), tenantID)
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Shared Internal Skill",
Slug: "shared-internal-skill-" + tenantID.String()[:8],
OwnerID: "owner-user",
Visibility: "internal",
FilePath: filepath.Join(t.TempDir(), "shared-internal-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
if err := skillStore.GrantToAgent(ctx, skillID, agentID, 1, "owner-user", true); err != nil {
t.Fatalf("GrantToAgent error: %v", err)
}
if err := skillStore.GrantToUser(ctx, skillID, "granted-user", "owner-user"); err != nil {
t.Fatalf("GrantToUser error: %v", err)
}
if err := skillStore.RevokeFromAgent(ctx, skillID, agentID); err != nil {
t.Fatalf("RevokeFromAgent error: %v", err)
}
got, ok := skillStore.GetSkillByID(ctx, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if got.Visibility != "internal" {
t.Fatalf("visibility after revoking last agent grant = %q, want internal because a user grant remains", got.Visibility)
}
}
func TestSQLiteSkillStore_UserGrantListPromotesAndDemotesVisibility(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantID, _ := seedSQLiteTenantAgent(t, db)
ctx := store.WithTenantID(context.Background(), tenantID)
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "User Shared Skill",
Slug: "user-shared-skill-" + tenantID.String()[:8],
OwnerID: "owner-user",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "user-shared-skill", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
if err := skillStore.GrantToUser(ctx, skillID, "granted-user", "owner-user"); err != nil {
t.Fatalf("GrantToUser error: %v", err)
}
grants, err := skillStore.ListUserGrantsForSkill(ctx, skillID)
if err != nil {
t.Fatalf("ListUserGrantsForSkill error: %v", err)
}
if len(grants) != 1 || grants[0].UserID != "granted-user" || grants[0].GrantedBy != "owner-user" {
t.Fatalf("user grants = %+v", grants)
}
got, ok := skillStore.GetSkillByID(ctx, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok")
}
if got.Visibility != "internal" {
t.Fatalf("visibility after user grant = %q, want internal", got.Visibility)
}
if err := skillStore.RevokeFromUser(ctx, skillID, "granted-user"); err != nil {
t.Fatalf("RevokeFromUser error: %v", err)
}
got, ok = skillStore.GetSkillByID(ctx, skillID)
if !ok {
t.Fatal("GetSkillByID returned !ok after revoke")
}
if got.Visibility != "private" {
t.Fatalf("visibility after last user grant revoke = %q, want private", got.Visibility)
}
}
func TestSQLiteSkillStore_UserGrantsAreTenantScopedForSystemSkill(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantA, _ := seedSQLiteTenantAgent(t, db)
tenantB, _ := seedSQLiteTenantAgent(t, db)
ctxA := store.WithTenantID(context.Background(), tenantA)
ctxB := store.WithTenantID(context.Background(), tenantB)
skillID := uuid.New()
slug := "system-user-grant-" + skillID.String()[:8]
if _, err := db.Exec(
`INSERT INTO skills (id, name, slug, owner_id, visibility, version, status, file_path, is_system, tenant_id)
VALUES (?, 'System User Grant Skill', ?, 'system', 'private', 1, 'active', ?, 1, ?)`,
skillID.String(), slug, filepath.Join(t.TempDir(), "system-user-grant", "1"), store.MasterTenantID.String(),
); err != nil {
t.Fatalf("insert system skill: %v", err)
}
if err := skillStore.GrantToUser(ctxA, skillID, "same-user", "tenant-a-admin"); err != nil {
t.Fatalf("GrantToUser tenant A error: %v", err)
}
if err := skillStore.GrantToUser(ctxB, skillID, "same-user", "tenant-b-admin"); err != nil {
t.Fatalf("GrantToUser tenant B error: %v", err)
}
grantsA, err := skillStore.ListUserGrantsForSkill(ctxA, skillID)
if err != nil {
t.Fatalf("ListUserGrantsForSkill tenant A error: %v", err)
}
grantsB, err := skillStore.ListUserGrantsForSkill(ctxB, skillID)
if err != nil {
t.Fatalf("ListUserGrantsForSkill tenant B error: %v", err)
}
if len(grantsA) != 1 || grantsA[0].GrantedBy != "tenant-a-admin" {
t.Fatalf("tenant A grants = %+v", grantsA)
}
if len(grantsB) != 1 || grantsB[0].GrantedBy != "tenant-b-admin" {
t.Fatalf("tenant B grants = %+v", grantsB)
}
}
func TestSQLiteSkillStore_ListWithGrantStatusIgnoresForeignTenantGrant(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantA, _ := seedSQLiteTenantAgent(t, db)
tenantB, agentB := seedSQLiteTenantAgent(t, db)
ctxA := store.WithTenantID(context.Background(), tenantA)
skillID := uuid.New()
if _, err := db.Exec(
`INSERT INTO skills (id, name, slug, owner_id, visibility, version, status, file_path, is_system, tenant_id)
VALUES (?, 'System Skill', ?, 'system', 'internal', 1, 'active', ?, 1, ?)`,
skillID.String(), "system-grant-status-"+skillID.String()[:8], filepath.Join(t.TempDir(), "system-skill", "1"), store.MasterTenantID.String(),
); err != nil {
t.Fatalf("insert system skill: %v", err)
}
if _, err := db.Exec(
`INSERT INTO skill_agent_grants (id, skill_id, agent_id, pinned_version, granted_by, can_manage, tenant_id)
VALUES (?, ?, ?, 1, 'tenant-b-admin', 1, ?)`,
uuid.New().String(), skillID.String(), agentB.String(), tenantB.String(),
); err != nil {
t.Fatalf("insert foreign tenant grant: %v", err)
}
skills, err := skillStore.ListWithGrantStatus(ctxA, agentB)
if err != nil {
t.Fatalf("ListWithGrantStatus error: %v", err)
}
for _, skill := range skills {
if skill.ID == skillID {
if skill.Granted || skill.CanManage {
t.Fatalf("foreign tenant grant leaked into tenant A status: granted=%v canManage=%v", skill.Granted, skill.CanManage)
}
return
}
}
t.Fatalf("system skill %s not returned for tenant A", skillID)
}
func TestSQLiteSkillStore_ListAccessibleHonorsAccessModes(t *testing.T) {
_, skillStore, db := newTestSQLiteSkillStoreWithDB(t)
tenantID, agentA := seedSQLiteTenantAgent(t, db)
agentB := uuid.New()
if _, err := db.Exec(
`INSERT INTO agents (id, tenant_id, agent_key, agent_type, status, provider, model, owner_id)
VALUES (?, ?, ?, 'predefined', 'active', 'test', 'test-model', 'user-2')`,
agentB.String(), tenantID.String(), "agent-"+agentB.String()[:8],
); err != nil {
t.Fatalf("insert second agent: %v", err)
}
ctx := store.WithTenantID(context.Background(), tenantID)
privateID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Owner Only Skill",
Slug: "owner-only-skill-" + tenantID.String()[:8],
OwnerID: "owner-user",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "owner-only", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged private error: %v", err)
}
internalID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Granted Agents Skill",
Slug: "granted-agents-skill-" + tenantID.String()[:8],
OwnerID: "owner-user",
Visibility: "internal",
FilePath: filepath.Join(t.TempDir(), "granted-agents", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged internal error: %v", err)
}
publicID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Tenant Wide Skill",
Slug: "tenant-wide-skill-" + tenantID.String()[:8],
OwnerID: "owner-user",
Visibility: "public",
FilePath: filepath.Join(t.TempDir(), "tenant-wide", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged public error: %v", err)
}
if err := skillStore.GrantToAgent(ctx, internalID, agentA, 1, "owner-user", true); err != nil {
t.Fatalf("GrantToAgent internal error: %v", err)
}
viewerA := listAccessibleSlugs(t, skillStore, ctx, agentA, "viewer-user")
if !viewerA["granted-agents-skill-"+tenantID.String()[:8]] {
t.Fatalf("granted agent did not receive internal skill; got %v", viewerA)
}
if !viewerA["tenant-wide-skill-"+tenantID.String()[:8]] {
t.Fatalf("agent did not receive public skill; got %v", viewerA)
}
if viewerA["owner-only-skill-"+tenantID.String()[:8]] {
t.Fatalf("non-owner received private skill; got %v", viewerA)
}
viewerB := listAccessibleSlugs(t, skillStore, ctx, agentB, "viewer-user")
if viewerB["granted-agents-skill-"+tenantID.String()[:8]] {
t.Fatalf("ungranted agent received internal skill; got %v", viewerB)
}
if !viewerB["tenant-wide-skill-"+tenantID.String()[:8]] {
t.Fatalf("ungranted agent did not receive public skill; got %v", viewerB)
}
owner := listAccessibleSlugs(t, skillStore, ctx, agentB, "owner-user")
if !owner["owner-only-skill-"+tenantID.String()[:8]] {
t.Fatalf("owner did not receive private skill %s; got %v", privateID, owner)
}
if !owner["tenant-wide-skill-"+tenantID.String()[:8]] {
t.Fatalf("owner did not receive public skill %s; got %v", publicID, owner)
}
if owner["granted-agents-skill-"+tenantID.String()[:8]] {
t.Fatalf("owner received internal skill without grant; got %v", owner)
}
}
func TestSQLiteSkillStore_ListAllSkillsIncludesOwnerID(t *testing.T) {
ctx, skillStore := newTestSQLiteSkillStore(t)
skillID, err := skillStore.CreateSkillManaged(ctx, store.SkillCreateParams{
Name: "Owner Projection Skill",
Slug: "owner-projection-skill",
OwnerID: "owner-user",
Visibility: "private",
FilePath: filepath.Join(t.TempDir(), "owner-projection", "1"),
})
if err != nil {
t.Fatalf("CreateSkillManaged error: %v", err)
}
for _, skill := range skillStore.ListAllSkills(ctx) {
if skill.ID == skillID.String() {
if skill.OwnerID != "owner-user" {
t.Fatalf("OwnerID = %q, want owner-user", skill.OwnerID)
}
return
}
}
t.Fatalf("created skill %s not found", skillID)
}
func listAccessibleSlugs(t *testing.T, skillStore *SQLiteSkillStore, ctx context.Context, agentID uuid.UUID, userID string) map[string]bool {
t.Helper()
skills, err := skillStore.ListAccessible(ctx, agentID, userID)
if err != nil {
t.Fatalf("ListAccessible error: %v", err)
}
slugs := make(map[string]bool, len(skills))
for _, skill := range skills {
slugs[skill.Slug] = true
}
return slugs
}
func newTestSQLiteSkillStore(t *testing.T) (context.Context, *SQLiteSkillStore) {
ctx, skillStore, _ := newTestSQLiteSkillStoreWithDB(t)
return ctx, skillStore
}
func newTestSQLiteSkillStoreWithDB(t *testing.T) (context.Context, *SQLiteSkillStore, *sql.DB) {
t.Helper()
db, err := OpenDB(filepath.Join(t.TempDir(), "skills.db"))
if err != nil {
t.Fatalf("OpenDB error: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if err := EnsureSchema(db); err != nil {
t.Fatalf("EnsureSchema error: %v", err)
}
return store.WithTenantID(context.Background(), store.MasterTenantID), NewSQLiteSkillStore(db, t.TempDir()), db
}
func seedSQLiteTenantAgent(t *testing.T, db *sql.DB) (uuid.UUID, uuid.UUID) {
t.Helper()
tenantID := uuid.New()
agentID := uuid.New()
if _, err := db.Exec(
`INSERT INTO tenants (id, name, slug, status) VALUES (?, ?, ?, 'active')`,
tenantID.String(), "tenant-"+tenantID.String()[:8], "t"+tenantID.String()[:8],
); err != nil {
t.Fatalf("insert tenant: %v", err)
}
if _, err := db.Exec(
`INSERT INTO agents (id, tenant_id, agent_key, agent_type, status, provider, model, owner_id)
VALUES (?, ?, ?, 'predefined', 'active', 'test', 'test-model', 'user-1')`,
agentID.String(), tenantID.String(), "agent-"+agentID.String()[:8],
); err != nil {
t.Fatalf("insert agent: %v", err)
}
return tenantID, agentID
}