1035 lines
29 KiB
Go
1035 lines
29 KiB
Go
package runtime_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/krow/krow-backend/go-api/internal/authctx"
|
|
"github.com/krow/krow-backend/go-api/internal/repo"
|
|
"github.com/krow/krow-backend/go-api/internal/runtime"
|
|
"github.com/krow/krow-backend/go-api/internal/testutil"
|
|
)
|
|
|
|
type fixture struct {
|
|
h *testutil.Harness
|
|
loader *runtime.Loader
|
|
engine *runtime.Engine
|
|
defRepo *repo.DefinitionsRepo
|
|
org1 string
|
|
org2 string
|
|
userA authctx.Identity
|
|
userB authctx.Identity
|
|
userOther authctx.Identity
|
|
}
|
|
|
|
func newFixture(t *testing.T) *fixture {
|
|
t.Helper()
|
|
h := testutil.New(t)
|
|
ctx := context.Background()
|
|
|
|
var org2 string
|
|
if err := h.Pool.QueryRow(ctx,
|
|
`INSERT INTO organizations (name, slug) VALUES ('Second Org', 'second-org') RETURNING id::text`).
|
|
Scan(&org2); err != nil {
|
|
t.Fatalf("create second org: %v", err)
|
|
}
|
|
|
|
var userAID, userBID, userOtherID string
|
|
if err := h.Pool.QueryRow(ctx,
|
|
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User A', 'user-a@example.test', 'admin', 'active') RETURNING id::text`,
|
|
h.OrgID).Scan(&userAID); err != nil {
|
|
t.Fatalf("create userA: %v", err)
|
|
}
|
|
if err := h.Pool.QueryRow(ctx,
|
|
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User B', 'user-b@example.test', 'talent', 'active') RETURNING id::text`,
|
|
h.OrgID).Scan(&userBID); err != nil {
|
|
t.Fatalf("create userB: %v", err)
|
|
}
|
|
if err := h.Pool.QueryRow(ctx,
|
|
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User Other', 'user-other@example.test', 'admin', 'active') RETURNING id::text`,
|
|
org2).Scan(&userOtherID); err != nil {
|
|
t.Fatalf("create userOther: %v", err)
|
|
}
|
|
|
|
f := &fixture{
|
|
h: h,
|
|
loader: runtime.NewLoader(h.Pool),
|
|
engine: runtime.NewEngine(h.Pool),
|
|
defRepo: repo.NewDefinitionsRepo(h.Pool),
|
|
org1: h.OrgID,
|
|
org2: org2,
|
|
userA: authctx.Identity{
|
|
UserID: userAID,
|
|
OrgID: h.OrgID,
|
|
Role: "admin",
|
|
Email: "user-a@example.test",
|
|
},
|
|
userB: authctx.Identity{
|
|
UserID: userBID,
|
|
OrgID: h.OrgID,
|
|
Role: "talent",
|
|
Email: "user-b@example.test",
|
|
},
|
|
userOther: authctx.Identity{
|
|
UserID: userOtherID,
|
|
OrgID: org2,
|
|
Role: "admin",
|
|
Email: "user-other@example.test",
|
|
},
|
|
}
|
|
return f
|
|
}
|
|
|
|
/* ── 1. Loader Tests ──────────────────────────────────────────────────────── */
|
|
|
|
func TestRuntimeLoader_Agent(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
// 1. Published org agent
|
|
agentMD := `---
|
|
id: talent-scout
|
|
name: Talent Scout
|
|
description: Discovers matching candidates
|
|
status: published
|
|
version: 2
|
|
pages:
|
|
- candidates
|
|
icon: sparkles
|
|
reasoning: deep
|
|
trigger: manual
|
|
webSearch: true
|
|
skills:
|
|
- resume-evaluator
|
|
subagents:
|
|
- profile-enricher
|
|
---
|
|
|
|
## Instructions
|
|
Review candidate profiles with diligence.
|
|
`
|
|
rec, err := f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "talent-scout",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: agentMD,
|
|
Status: "published",
|
|
Version: 2,
|
|
Name: "Talent Scout",
|
|
Description: "Discovers matching candidates",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("insert agent: %v", err)
|
|
}
|
|
dbUUID := rec["id"].(string)
|
|
|
|
// Load by definition_id
|
|
agentByDefID, err := f.loader.LoadAgent(ctx, f.userB, "talent-scout")
|
|
if err != nil {
|
|
t.Fatalf("load agent by definition_id: %v", err)
|
|
}
|
|
if agentByDefID.ID != "talent-scout" || agentByDefID.DatabaseID != dbUUID {
|
|
t.Errorf("agent ID mismatch: %+v", agentByDefID)
|
|
}
|
|
if agentByDefID.Name != "Talent Scout" || agentByDefID.Version != 2 || agentByDefID.Status != "published" {
|
|
t.Errorf("agent fields mismatch: %+v", agentByDefID)
|
|
}
|
|
if agentByDefID.Icon != "sparkles" || agentByDefID.Reasoning != "deep" || !agentByDefID.WebSearch {
|
|
t.Errorf("agent config mismatch: %+v", agentByDefID)
|
|
}
|
|
if len(agentByDefID.Skills) != 1 || agentByDefID.Skills[0] != "resume-evaluator" {
|
|
t.Errorf("agent skills mismatch: %v", agentByDefID.Skills)
|
|
}
|
|
if agentByDefID.Instructions == "" || agentByDefID.RawMarkdown != agentMD {
|
|
t.Errorf("agent markdown/instructions mismatch")
|
|
}
|
|
|
|
// Load by UUID
|
|
agentByUUID, err := f.loader.LoadAgent(ctx, f.userA, dbUUID)
|
|
if err != nil {
|
|
t.Fatalf("load agent by UUID: %v", err)
|
|
}
|
|
if agentByUUID.ID != "talent-scout" {
|
|
t.Errorf("agent by UUID ID mismatch: %+v", agentByUUID)
|
|
}
|
|
|
|
// Personal agent for User B
|
|
persMD := `---
|
|
id: personal-agent
|
|
name: Personal Agent
|
|
status: draft
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
---
|
|
## Instructions
|
|
Personal instructions.
|
|
`
|
|
persRec, err := f.defRepo.InsertAgent(ctx, f.userB, repo.AgentInsertInput{
|
|
DefinitionID: "personal-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "personal",
|
|
OwnerUserID: &f.userB.UserID,
|
|
CreatedBy: &f.userB.UserID,
|
|
Markdown: persMD,
|
|
Status: "draft",
|
|
Version: 1,
|
|
Name: "Personal Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("insert personal agent: %v", err)
|
|
}
|
|
persUUID := persRec["id"].(string)
|
|
|
|
// Owner (User B) can load personal agent
|
|
persAgent, err := f.loader.LoadAgent(ctx, f.userB, "personal-agent")
|
|
if err != nil {
|
|
t.Fatalf("load own personal agent: %v", err)
|
|
}
|
|
if persAgent.DatabaseID != persUUID {
|
|
t.Errorf("personal agent uuid mismatch: %s != %s", persAgent.DatabaseID, persUUID)
|
|
}
|
|
|
|
// Other user in same org (User A) CANNOT load User B's personal agent -> ErrNotFound
|
|
_, err = f.loader.LoadAgent(ctx, f.userA, "personal-agent")
|
|
if !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Errorf("userA loading userB personal agent: got error %v, want ErrNotFound", err)
|
|
}
|
|
|
|
// Outsider CANNOT load org agent from org 1 -> ErrNotFound
|
|
_, err = f.loader.LoadAgent(ctx, f.userOther, "talent-scout")
|
|
if !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Errorf("outsider loading org1 agent: got error %v, want ErrNotFound", err)
|
|
}
|
|
|
|
// Missing agent -> ErrNotFound
|
|
_, err = f.loader.LoadAgent(ctx, f.userA, "nonexistent-agent")
|
|
if !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Errorf("load nonexistent agent: got %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeLoader_Skill(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
skillMD := `---
|
|
id: resume-evaluator
|
|
name: Resume Evaluator
|
|
description: Evaluates candidate resume text
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
category: screening
|
|
actions:
|
|
- score
|
|
- summarize
|
|
triggers:
|
|
- resume
|
|
- cv
|
|
prompt: Evaluate the candidate resume thoroughly.
|
|
---
|
|
|
|
# Resume Evaluator Body
|
|
Detailed skill instructions.
|
|
`
|
|
rec, err := f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "resume-evaluator",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: skillMD,
|
|
Status: "active",
|
|
Name: "Resume Evaluator",
|
|
Description: "Evaluates candidate resume text",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("insert skill: %v", err)
|
|
}
|
|
skillUUID := rec["id"].(string)
|
|
|
|
// Load by definition_id
|
|
skillByDefID, err := f.loader.LoadSkill(ctx, f.userB, "resume-evaluator")
|
|
if err != nil {
|
|
t.Fatalf("load skill by definition_id: %v", err)
|
|
}
|
|
if skillByDefID.ID != "resume-evaluator" || skillByDefID.DatabaseID != skillUUID {
|
|
t.Errorf("skill ID mismatch: %+v", skillByDefID)
|
|
}
|
|
if skillByDefID.Name != "Resume Evaluator" || skillByDefID.Status != "active" {
|
|
t.Errorf("skill fields mismatch: %+v", skillByDefID)
|
|
}
|
|
if skillByDefID.Category != "screening" || len(skillByDefID.Actions) != 2 || len(skillByDefID.Triggers) != 2 {
|
|
t.Errorf("skill actions/triggers mismatch: %+v", skillByDefID)
|
|
}
|
|
if skillByDefID.Prompt == nil || *skillByDefID.Prompt != "Evaluate the candidate resume thoroughly." {
|
|
t.Errorf("skill prompt mismatch: %v", skillByDefID.Prompt)
|
|
}
|
|
if skillByDefID.RawMarkdown != skillMD {
|
|
t.Errorf("skill raw markdown not verbatim")
|
|
}
|
|
|
|
// Load by UUID
|
|
skillByUUID, err := f.loader.LoadSkill(ctx, f.userA, skillUUID)
|
|
if err != nil {
|
|
t.Fatalf("load skill by UUID: %v", err)
|
|
}
|
|
if skillByUUID.ID != "resume-evaluator" {
|
|
t.Errorf("skill by UUID mismatch: %+v", skillByUUID)
|
|
}
|
|
|
|
// Missing skill -> ErrNotFound
|
|
_, err = f.loader.LoadSkill(ctx, f.userA, "nonexistent-skill")
|
|
if !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Errorf("missing skill: got %v, want ErrNotFound", err)
|
|
}
|
|
|
|
// Cross-org skill -> ErrNotFound
|
|
_, err = f.loader.LoadSkill(ctx, f.userOther, "resume-evaluator")
|
|
if !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Errorf("cross-org skill: got %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
/* ── 2. Status Eligibility Tests ─────────────────────────────────────────── */
|
|
|
|
func TestRuntime_StatusEligibility(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
// 1. Draft Agent
|
|
draftMD := `---
|
|
id: draft-agent
|
|
name: Draft Agent
|
|
status: draft
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "draft-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: draftMD,
|
|
Status: "draft",
|
|
Version: 1,
|
|
Name: "Draft Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err := f.loader.LoadExecutableAgent(ctx, f.userA, "draft-agent")
|
|
if !errors.Is(err, runtime.ErrDraftAgent) {
|
|
t.Errorf("draft agent: got %v, want ErrDraftAgent", err)
|
|
}
|
|
|
|
// 2. Archived Agent
|
|
archivedMD := `---
|
|
id: archived-agent
|
|
name: Archived Agent
|
|
status: archived
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "archived-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: archivedMD,
|
|
Status: "archived",
|
|
Version: 1,
|
|
Name: "Archived Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "archived-agent")
|
|
if !errors.Is(err, runtime.ErrArchivedAgent) {
|
|
t.Errorf("archived agent: got %v, want ErrArchivedAgent", err)
|
|
}
|
|
|
|
// 3. Published Agent without dependencies -> Success
|
|
pubMD := `---
|
|
id: published-agent
|
|
name: Published Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "published-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: pubMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Published Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
pubAgent, err := f.loader.LoadExecutableAgent(ctx, f.userA, "published-agent")
|
|
if err != nil {
|
|
t.Fatalf("load published agent: %v", err)
|
|
}
|
|
if pubAgent.Status != "published" {
|
|
t.Errorf("published agent status: %s", pubAgent.Status)
|
|
}
|
|
|
|
// 4. Inactive Skill
|
|
inactiveSkillMD := `---
|
|
id: inactive-skill
|
|
name: Inactive Skill
|
|
status: inactive
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "inactive-skill",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: inactiveSkillMD,
|
|
Status: "inactive",
|
|
Name: "Inactive Skill",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err = f.loader.LoadExecutableSkill(ctx, f.userA, "inactive-skill")
|
|
if !errors.Is(err, runtime.ErrInactiveSkill) {
|
|
t.Errorf("inactive skill: got %v, want ErrInactiveSkill", err)
|
|
}
|
|
|
|
// 5. Active Skill -> Success
|
|
activeSkillMD := `---
|
|
id: active-skill
|
|
name: Active Skill
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "active-skill",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: activeSkillMD,
|
|
Status: "active",
|
|
Name: "Active Skill",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
activeSkill, err := f.loader.LoadExecutableSkill(ctx, f.userA, "active-skill")
|
|
if err != nil {
|
|
t.Fatalf("load active skill: %v", err)
|
|
}
|
|
if activeSkill.Status != "active" {
|
|
t.Errorf("active skill status: %s", activeSkill.Status)
|
|
}
|
|
}
|
|
|
|
/* ── 3. Version Semantics Tests ──────────────────────────────────────────── */
|
|
|
|
func TestRuntime_VersionSemantics(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
v5MD := `---
|
|
id: v5-agent
|
|
name: Version 5 Agent
|
|
version: 5
|
|
status: published
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, err := f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "v5-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: v5MD,
|
|
Status: "published",
|
|
Version: 5,
|
|
Name: "Version 5 Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("insert v5 agent: %v", err)
|
|
}
|
|
|
|
agent, err := f.loader.LoadAgent(ctx, f.userA, "v5-agent")
|
|
if err != nil {
|
|
t.Fatalf("load v5 agent: %v", err)
|
|
}
|
|
if agent.Version != 5 {
|
|
t.Errorf("agent version = %d, want 5", agent.Version)
|
|
}
|
|
}
|
|
|
|
/* ── 4. Dependency Resolution Tests ──────────────────────────────────────── */
|
|
|
|
func TestRuntime_DependencyResolution(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
// 1. Create active skill 1
|
|
skill1MD := `---
|
|
id: skill-one
|
|
name: Skill One
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "skill-one",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: skill1MD,
|
|
Status: "active",
|
|
Name: "Skill One",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// 2. Create active skill 2 (personal for userB)
|
|
skill2MD := `---
|
|
id: skill-two
|
|
name: Skill Two
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userB, repo.SkillInsertInput{
|
|
DefinitionID: "skill-two",
|
|
OrgID: f.org1,
|
|
Visibility: "personal",
|
|
OwnerUserID: &f.userB.UserID,
|
|
CreatedBy: &f.userB.UserID,
|
|
Markdown: skill2MD,
|
|
Status: "active",
|
|
Name: "Skill Two",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// 3. Create inactive skill 3
|
|
skill3MD := `---
|
|
id: skill-inactive
|
|
name: Skill Inactive
|
|
status: inactive
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "skill-inactive",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: skill3MD,
|
|
Status: "inactive",
|
|
Name: "Skill Inactive",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// Agent with valid and duplicate dependencies
|
|
validDepMD := `---
|
|
id: dep-agent
|
|
name: Dependency Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
skills:
|
|
- skill-one
|
|
- skill-two
|
|
- skill-one
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userB, repo.AgentInsertInput{
|
|
DefinitionID: "dep-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "personal",
|
|
OwnerUserID: &f.userB.UserID,
|
|
CreatedBy: &f.userB.UserID,
|
|
Markdown: validDepMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Dependency Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// User B resolves dep-agent: sees skill-one (org) and skill-two (personal), deduplicates skill-one
|
|
loadedAgent, err := f.loader.LoadExecutableAgent(ctx, f.userB, "dep-agent")
|
|
if err != nil {
|
|
t.Fatalf("load executable agent with deps: %v", err)
|
|
}
|
|
if len(loadedAgent.ResolvedSkills) != 2 {
|
|
t.Fatalf("expected 2 resolved skills (deduplicated), got %d", len(loadedAgent.ResolvedSkills))
|
|
}
|
|
if loadedAgent.ResolvedSkills[0].ID != "skill-one" || loadedAgent.ResolvedSkills[1].ID != "skill-two" {
|
|
t.Errorf("resolved skill IDs mismatch: %v, %v", loadedAgent.ResolvedSkills[0].ID, loadedAgent.ResolvedSkills[1].ID)
|
|
}
|
|
|
|
// Agent with missing dependency
|
|
missingDepMD := `---
|
|
id: missing-dep-agent
|
|
name: Missing Dep Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
skills:
|
|
- nonexistent-skill
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "missing-dep-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: missingDepMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Missing Dep Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "missing-dep-agent")
|
|
if !errors.Is(err, runtime.ErrDependencyMissing) {
|
|
t.Errorf("missing dep agent: got %v, want ErrDependencyMissing", err)
|
|
}
|
|
|
|
// Agent with inactive dependency
|
|
inactiveDepMD := `---
|
|
id: inactive-dep-agent
|
|
name: Inactive Dep Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
skills:
|
|
- skill-inactive
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "inactive-dep-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: inactiveDepMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Inactive Dep Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "inactive-dep-agent")
|
|
if !errors.Is(err, runtime.ErrDependencyInactive) {
|
|
t.Errorf("inactive dep agent: got %v, want ErrDependencyInactive", err)
|
|
}
|
|
|
|
// Agent with cross-org dependency: outsider creates agent referencing org1's skill
|
|
outsiderAgentMD := `---
|
|
id: outsider-agent
|
|
name: Outsider Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
skills:
|
|
- skill-one
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userOther, repo.AgentInsertInput{
|
|
DefinitionID: "outsider-agent",
|
|
OrgID: f.org2,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userOther.UserID,
|
|
Markdown: outsiderAgentMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Outsider Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
_, err = f.loader.LoadExecutableAgent(ctx, f.userOther, "outsider-agent")
|
|
if !errors.Is(err, runtime.ErrDependencyMissing) {
|
|
t.Errorf("outsider cross-org dep: got %v, want ErrDependencyMissing", err)
|
|
}
|
|
}
|
|
|
|
/* ── 5. Executor Boundary Tests ──────────────────────────────────────────── */
|
|
|
|
type testEchoExecutor struct {
|
|
lastAgent *runtime.Agent
|
|
lastInput runtime.ExecutionInput
|
|
}
|
|
|
|
func (e *testEchoExecutor) ExecuteAgent(_ context.Context, agent *runtime.Agent, input runtime.ExecutionInput) (*runtime.ExecutionResult, error) {
|
|
e.lastAgent = agent
|
|
e.lastInput = input
|
|
return &runtime.ExecutionResult{
|
|
Success: true,
|
|
Output: fmt.Sprintf("executed %s with %s", agent.Name, input.Input),
|
|
AgentID: agent.ID,
|
|
AgentVersion: agent.Version,
|
|
}, nil
|
|
}
|
|
|
|
func (e *testEchoExecutor) ExecuteSkill(_ context.Context, skill *runtime.Skill, input runtime.ExecutionInput) (*runtime.ExecutionResult, error) {
|
|
return &runtime.ExecutionResult{
|
|
Success: true,
|
|
Output: fmt.Sprintf("executed skill %s", skill.Name),
|
|
}, nil
|
|
}
|
|
|
|
func TestRuntime_ExecutorBoundary(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
pubMD := `---
|
|
id: exec-agent
|
|
name: Exec Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
|
|
DefinitionID: "exec-agent",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: pubMD,
|
|
Status: "published",
|
|
Version: 1,
|
|
Name: "Exec Agent",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// 1. Default engine without configured executor returns ErrExecutorUnavailable
|
|
res, err := f.engine.RunAgent(ctx, f.userA, "exec-agent", runtime.ExecutionInput{
|
|
Identity: f.userA,
|
|
Input: "Hello agent",
|
|
})
|
|
if !errors.Is(err, runtime.ErrExecutorUnavailable) {
|
|
t.Errorf("default engine run agent: got %v, want ErrExecutorUnavailable", err)
|
|
}
|
|
if res.Success {
|
|
t.Errorf("default engine must not succeed without executor")
|
|
}
|
|
|
|
// 2. Custom executor plugged in
|
|
echo := &testEchoExecutor{}
|
|
customEngine := runtime.NewEngine(f.h.Pool, runtime.WithAgentExecutor(echo), runtime.WithSkillExecutor(echo))
|
|
|
|
resCustom, err := customEngine.RunAgent(ctx, f.userA, "exec-agent", runtime.ExecutionInput{
|
|
Identity: f.userA,
|
|
Input: "Hello agent",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("custom engine run agent failed: %v", err)
|
|
}
|
|
if !resCustom.Success || resCustom.Output != "executed Exec Agent with Hello agent" {
|
|
t.Errorf("custom executor output mismatch: %+v", resCustom)
|
|
}
|
|
if echo.lastAgent.ID != "exec-agent" || echo.lastInput.Input != "Hello agent" {
|
|
t.Errorf("executor received incorrect arguments: agent=%v, input=%v", echo.lastAgent, echo.lastInput)
|
|
}
|
|
}
|
|
|
|
func TestRuntime_PersonalSkillShadowing(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
// Org skill
|
|
orgSkillMD := `---
|
|
id: shadow-skill
|
|
name: Org Shadow Skill
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "shadow-skill",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: orgSkillMD,
|
|
Status: "active",
|
|
Name: "Org Shadow Skill",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// User B's personal skill with the SAME definition_id
|
|
persSkillMD := `---
|
|
id: shadow-skill
|
|
name: User B Personal Shadow Skill
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
persRec, _ := f.defRepo.InsertSkill(ctx, f.userB, repo.SkillInsertInput{
|
|
DefinitionID: "shadow-skill",
|
|
OrgID: f.org1,
|
|
Visibility: "personal",
|
|
OwnerUserID: &f.userB.UserID,
|
|
CreatedBy: &f.userB.UserID,
|
|
Markdown: persSkillMD,
|
|
Status: "active",
|
|
Name: "User B Personal Shadow Skill",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
persUUID := persRec["id"].(string)
|
|
|
|
// When User A loads shadow-skill -> gets Org Shadow Skill
|
|
skillA, err := f.loader.LoadSkill(ctx, f.userA, "shadow-skill")
|
|
if err != nil {
|
|
t.Fatalf("userA load shadow skill: %v", err)
|
|
}
|
|
if skillA.Name != "Org Shadow Skill" {
|
|
t.Errorf("userA got %s, want Org Shadow Skill", skillA.Name)
|
|
}
|
|
|
|
// When User B loads shadow-skill -> gets User B Personal Shadow Skill (shadow precedence)
|
|
skillB, err := f.loader.LoadSkill(ctx, f.userB, "shadow-skill")
|
|
if err != nil {
|
|
t.Fatalf("userB load shadow skill: %v", err)
|
|
}
|
|
if skillB.Name != "User B Personal Shadow Skill" || skillB.DatabaseID != persUUID {
|
|
t.Errorf("userB got %s, want User B Personal Shadow Skill", skillB.Name)
|
|
}
|
|
}
|
|
|
|
func TestRuntime_MalformedMarkdown(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
// Broken YAML
|
|
brokenMD := `---
|
|
id: [broken-id
|
|
name: Invalid
|
|
---
|
|
`
|
|
_, _ = f.h.Pool.Exec(ctx, `INSERT INTO agent_definitions (definition_id, org_id, visibility, created_by, markdown, status, version, name, description, pages)
|
|
VALUES ('broken-agent', $1::uuid, 'organization', $2::uuid, $3::text, 'published', 1, 'Broken', '', ARRAY['candidates'])`,
|
|
f.org1, f.userA.UserID, brokenMD)
|
|
|
|
_, err := f.loader.LoadAgent(ctx, f.userA, "broken-agent")
|
|
if !errors.Is(err, runtime.ErrInvalidDefinition) {
|
|
t.Errorf("load broken agent: got %v, want ErrInvalidDefinition", err)
|
|
}
|
|
}
|
|
|
|
func TestRuntime_SkillExecution(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
skillMD := `---
|
|
id: run-skill
|
|
name: Runnable Skill
|
|
status: active
|
|
pages:
|
|
- candidates
|
|
---
|
|
`
|
|
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
|
|
DefinitionID: "run-skill",
|
|
OrgID: f.org1,
|
|
Visibility: "organization",
|
|
CreatedBy: &f.userA.UserID,
|
|
Markdown: skillMD,
|
|
Status: "active",
|
|
Name: "Runnable Skill",
|
|
Pages: []string{"candidates"},
|
|
})
|
|
|
|
// Default engine
|
|
res, err := f.engine.RunSkill(ctx, f.userA, "run-skill", runtime.ExecutionInput{
|
|
Identity: f.userA,
|
|
Input: "skill input",
|
|
})
|
|
if !errors.Is(err, runtime.ErrExecutorUnavailable) {
|
|
t.Errorf("default engine run skill: got %v, want ErrExecutorUnavailable", err)
|
|
}
|
|
if res.Success {
|
|
t.Errorf("default engine run skill must not succeed")
|
|
}
|
|
|
|
// Custom executor
|
|
echo := &testEchoExecutor{}
|
|
customEngine := runtime.NewEngine(f.h.Pool, runtime.WithSkillExecutor(echo))
|
|
resCustom, err := customEngine.RunSkill(ctx, f.userA, "run-skill", runtime.ExecutionInput{
|
|
Identity: f.userA,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("custom engine run skill failed: %v", err)
|
|
}
|
|
if !resCustom.Success || resCustom.Output != "executed skill Runnable Skill" {
|
|
t.Errorf("custom skill output mismatch: %+v", resCustom)
|
|
}
|
|
}
|
|
|
|
/* ── Pinned versions ────────────────────────────────────────────────────── */
|
|
|
|
// TestAPinnedRunUsesTheAgentAsItWas.
|
|
//
|
|
// §3: "Running conversations pin the version they started with."
|
|
//
|
|
// The reason this matters is not tidiness. A person approves a write while
|
|
// looking at version 1; somebody publishes version 2 with different
|
|
// instructions and a different tool list; the approval is then carried out. If
|
|
// the run silently moved to version 2, the thing performed would not be the
|
|
// thing that was shown — which is the failure the whole confirmation mechanism
|
|
// exists to prevent, arriving through the registry instead of through the gate.
|
|
func TestAPinnedRunUsesTheAgentAsItWas(t *testing.T) {
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
v1 := `---
|
|
id: pinned-agent
|
|
name: Pinned Agent
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- activity
|
|
tools:
|
|
- activity_breakdown
|
|
---
|
|
|
|
## Instructions
|
|
Version one instructions.
|
|
`
|
|
v2 := `---
|
|
id: pinned-agent
|
|
name: Pinned Agent
|
|
status: published
|
|
version: 2
|
|
pages:
|
|
- activity
|
|
tools:
|
|
- activity_breakdown
|
|
- activity_signals
|
|
---
|
|
|
|
## Instructions
|
|
Version two instructions.
|
|
`
|
|
|
|
if _, err := f.h.Pool.Exec(ctx, `
|
|
INSERT INTO agent_definitions (definition_id, org_id, visibility, created_by,
|
|
markdown, status, version, name, description, pages)
|
|
VALUES ('pinned-agent', $1::uuid, 'organization', $2::uuid, $3::text,
|
|
'published', 2, 'Pinned Agent', '', ARRAY['activity'])`,
|
|
f.org1, f.userA.UserID, v2); err != nil {
|
|
t.Fatalf("seed current definition: %v", err)
|
|
}
|
|
|
|
versions := repo.NewVersionsRepo(f.h.Pool)
|
|
for _, s := range []repo.SnapshotInput{
|
|
{Kind: repo.KindAgent, DefinitionID: "pinned-agent", Version: 1, Markdown: v1, Name: "Pinned Agent"},
|
|
{Kind: repo.KindAgent, DefinitionID: "pinned-agent", Version: 2, Markdown: v2, Name: "Pinned Agent"},
|
|
} {
|
|
if err := versions.Snapshot(ctx, f.userA, s); err != nil {
|
|
t.Fatalf("snapshot v%d: %v", s.Version, err)
|
|
}
|
|
}
|
|
|
|
// Unpinned: the current definition, which is version 2.
|
|
current, fellBack, err := f.loader.LoadAgentVersion(ctx, f.userA, "pinned-agent", 0)
|
|
if err != nil {
|
|
t.Fatalf("load current: %v", err)
|
|
}
|
|
if fellBack {
|
|
t.Error("an unpinned load reported a fallback")
|
|
}
|
|
if current.Version != 2 || !strings.Contains(current.Instructions, "Version two") {
|
|
t.Errorf("current is v%d: %q", current.Version, current.Instructions)
|
|
}
|
|
if len(current.Tools) != 2 {
|
|
t.Errorf("v2 should carry 2 tools, got %v", current.Tools)
|
|
}
|
|
|
|
// Pinned to 1: the agent as it was, INCLUDING its narrower tool list. That
|
|
// last part is the one that would let an approved write reach a tool the
|
|
// approver's version never offered.
|
|
pinned, fellBack, err := f.loader.LoadAgentVersion(ctx, f.userA, "pinned-agent", 1)
|
|
if err != nil {
|
|
t.Fatalf("load v1: %v", err)
|
|
}
|
|
if fellBack {
|
|
t.Error("a version that exists reported a fallback")
|
|
}
|
|
if pinned.Version != 1 {
|
|
t.Errorf("pinned version = %d, want 1", pinned.Version)
|
|
}
|
|
if !strings.Contains(pinned.Instructions, "Version one") {
|
|
t.Errorf("pinned instructions are v2's: %q", pinned.Instructions)
|
|
}
|
|
if len(pinned.Tools) != 1 || pinned.Tools[0] != "activity_breakdown" {
|
|
t.Errorf("pinned tools are %v; v1 offered only activity_breakdown", pinned.Tools)
|
|
}
|
|
}
|
|
|
|
func TestAVersionWithNoSnapshotFallsBackAndSaysSo(t *testing.T) {
|
|
// Every definition published before versions were recorded has no snapshot.
|
|
// Refusing those would break every conversation that predates the feature,
|
|
// to enforce a rule they could not have followed. The run continues on the
|
|
// current definition and the fallback is REPORTED, because a silent
|
|
// substitution is the thing worth preventing.
|
|
f := newFixture(t)
|
|
ctx := context.Background()
|
|
|
|
md := `---
|
|
id: unversioned-agent
|
|
name: Unversioned
|
|
status: published
|
|
version: 1
|
|
pages:
|
|
- activity
|
|
---
|
|
|
|
## Instructions
|
|
Only ever existed as one thing.
|
|
`
|
|
if _, err := f.h.Pool.Exec(ctx, `
|
|
INSERT INTO agent_definitions (definition_id, org_id, visibility, created_by,
|
|
markdown, status, version, name, description, pages)
|
|
VALUES ('unversioned-agent', $1::uuid, 'organization', $2::uuid, $3::text,
|
|
'published', 1, 'Unversioned', '', ARRAY['activity'])`,
|
|
f.org1, f.userA.UserID, md); err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
|
|
agent, fellBack, err := f.loader.LoadAgentVersion(ctx, f.userA, "unversioned-agent", 7)
|
|
if err != nil {
|
|
t.Fatalf("a missing snapshot failed the load: %v", err)
|
|
}
|
|
if !fellBack {
|
|
t.Error("a version with no snapshot did not report a fallback")
|
|
}
|
|
if agent == nil || agent.Version != 1 {
|
|
t.Errorf("the fallback did not return the current definition: %+v", agent)
|
|
}
|
|
}
|