package service import ( "context" "testing" "github.com/Tencent/WeKnora/internal/application/repository" "github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/types/interfaces" "github.com/stretchr/testify/require" ) func TestGlobalInstructionSkillCanBeReadMountedButNotOverwrittenByOthers(t *testing.T) { s, alice := instructionHarness(t) bob := instructionContext(1, "bob", types.TenantRoleContributor) skill, err := s.Import(alice, "global.md", "", "", []byte("shared rules")) require.NoError(t, err) require.NoError(t, s.db.Model(&types.TenantSkillCatalogEntity{}).Where("id = ?", skill.ID).Update("visibility", "global").Error) view, err := s.Get(bob, skill.ID) require.NoError(t, err) require.Equal(t, "shared rules", view.Instructions) require.Equal(t, "global", view.Visibility) require.True(t, view.Enabled) list, err := s.List(bob) require.NoError(t, err) require.Len(t, list, 1) agent := types.CustomAgent{ID: "bob-agent", TenantID: 1, CreatedBy: "bob", Name: "Bob"} require.NoError(t, s.db.Create(&agent).Error) _, err = s.Mount(bob, InstructionSkillMountRequest{AgentIDs: []string{agent.ID}, SkillIDs: []string{skill.ID}}) require.NoError(t, err) require.NoError(t, s.db.First(&agent, "id = ?", agent.ID).Error) prompt, err := s.ResolveAgentInstructions(bob, &agent) require.NoError(t, err) require.Contains(t, prompt, "shared rules") require.Error(t, s.Delete(bob, skill.ID, true)) copy, err := s.Import(bob, "global.md", "", "", []byte("bob rules")) require.NoError(t, err) require.NotEqual(t, skill.ID, copy.ID) original, err := s.Get(alice, skill.ID) require.NoError(t, err) require.Equal(t, "shared rules", original.Instructions) } func TestInstructionLifecycleIsCheckedAtExecutionTime(t *testing.T) { s, alice := instructionHarness(t) bob := instructionContext(1, "bob", types.TenantRoleContributor) skill, err := s.Import(alice, "rules.md", "", "", []byte("sensitive rules")) require.NoError(t, err) agent := types.CustomAgent{ID: "shared-agent", TenantID: 1, CreatedBy: "alice", Name: "Shared", Visibility: "global", Enabled: true, Config: types.CustomAgentConfig{InstructionSkillIDs: []string{skill.ID}}} prompt, err := s.ResolveAgentInstructions(bob, &agent) require.NoError(t, err, "sharing an Agent includes execution of its owner's bound rules") require.Contains(t, prompt, "sensitive rules") _, err = s.Get(bob, skill.ID) require.Error(t, err, "Agent execution must not independently expose the private skill") require.NoError(t, s.db.Model(&types.TenantSkillCatalogEntity{}).Where("id = ?", skill.ID).Update("visibility", "global").Error) prompt, err = s.ResolveAgentInstructions(bob, &agent) require.NoError(t, err) require.Contains(t, prompt, "sensitive rules") require.NoError(t, s.db.Model(&types.TenantSkillCatalogEntity{}).Where("id = ?", skill.ID).Update("enabled", false).Error) for _, ctx := range []context.Context{alice, bob, instructionContext(1, "admin", types.TenantRoleAdmin)} { prompt, err = s.ResolveAgentInstructions(ctx, &agent) require.Error(t, err) require.Empty(t, prompt) } _, err = s.Get(bob, skill.ID) require.Error(t, err) list, err := s.List(bob) require.NoError(t, err) require.Empty(t, list) updated, err := s.Import(alice, "rules.md", "", "", []byte("revised rules")) require.NoError(t, err) require.False(t, updated.Enabled, "reimport must not reactivate a disabled skill") require.Equal(t, "global", updated.Visibility) agent.Config.InstructionSkillIDs = nil agent.Enabled = false _, err = s.ResolveAgentInstructions(alice, &agent) require.ErrorContains(t, err, "停用") agent.Enabled = true agent.Config.InstructionSkillIDs = []string{skill.ID} _, err = s.ResolveAgentInstructions(instructionContext(2, "admin", types.TenantRoleAdmin), &agent) require.ErrorContains(t, err, "跨工作空间") } func TestNativeOrganizationExecutionWithoutInstructionSkillsKeepsCompatibility(t *testing.T) { shared := &types.CustomAgent{ID: "shared", TenantID: 1, CreatedBy: "alice", Visibility: "private", Enabled: true} // The organization-sharing service verifies a share before handing this // Agent to QA. There are no instruction bodies to authorize or expose. prompt, err := ResolveAgentInstructionPrompt(instructionContext(2, "bob", types.TenantRoleContributor), nil, shared) require.NoError(t, err) require.Empty(t, prompt) shared.Enabled = false _, err = ResolveAgentInstructionPrompt(instructionContext(2, "bob", types.TenantRoleAdmin), nil, shared) require.ErrorContains(t, err, "停用") } func TestDisabledInstructionSkillCannotBeMounted(t *testing.T) { s, alice := instructionHarness(t) skill, err := s.Import(alice, "disabled.md", "", "", []byte("disabled rules")) require.NoError(t, err) require.NoError(t, s.db.Model(&types.TenantSkillCatalogEntity{}).Where("id = ?", skill.ID).Update("enabled", false).Error) require.NoError(t, s.db.Create(&types.CustomAgent{ID: "a", TenantID: 1, CreatedBy: "alice", Name: "A"}).Error) _, err = s.Mount(alice, InstructionSkillMountRequest{AgentIDs: []string{"a"}, SkillIDs: []string{skill.ID}}) require.ErrorContains(t, err, "停用") require.NoError(t, s.db.Model(&types.CustomAgent{}).Where("id = ?", "a").Updates(map[string]any{"enabled": false, "config": types.CustomAgentConfig{InstructionSkillIDs: []string{skill.ID}}}).Error) _, err = s.Mount(alice, InstructionSkillMountRequest{AgentIDs: []string{"a"}, SkillIDs: []string{skill.ID}, Mode: "remove"}) require.NoError(t, err, "owners must be able to repair disabled Agent bindings") var repaired types.CustomAgent require.NoError(t, s.db.First(&repaired, "id = ?", "a").Error) require.False(t, repaired.Enabled) require.Empty(t, repaired.Config.InstructionSkillIDs) } func TestDisabledBuiltinOverrideIsNotResurrected(t *testing.T) { ids := types.GetBuiltinAgentIDs() if len(ids) == 0 { t.Skip("no builtin registry configured") } id := ids[0] repo := &lifecycleAgentRepo{rows: map[string]*types.CustomAgent{id: {ID: id, TenantID: 1, IsBuiltin: true, Visibility: "global", Enabled: false}}} s := &customAgentService{repo: repo} bob := instructionContext(1, "bob", types.TenantRoleContributor) _, err := s.GetAgentByID(bob, id) require.ErrorIs(t, err, ErrAgentNotFound) list, err := s.ListAgents(bob) require.NoError(t, err) for _, a := range list { require.NotEqual(t, id, a.ID) } admin := instructionContext(1, "admin", types.TenantRoleAdmin) a, err := s.GetAgentByID(admin, id) require.NoError(t, err) _, err = ResolveAgentInstructionPrompt(admin, nil, a) require.ErrorContains(t, err, "停用") } func TestLifecycleCreateDefaultsPreserveExplicitDisabled(t *testing.T) { s, _ := instructionHarness(t) agent := types.CustomAgent{ID: "disabled", TenantID: 1, Name: "D", Visibility: "private", Enabled: false} require.NoError(t, s.db.Create(&agent).Error) var stored types.CustomAgent require.NoError(t, s.db.First(&stored, "id = ?", agent.ID).Error) require.False(t, stored.Enabled) legacy := types.CustomAgent{ID: "legacy", TenantID: 1, Name: "L"} require.NoError(t, s.db.Create(&legacy).Error) require.True(t, legacy.Enabled) require.Equal(t, "global", legacy.Visibility) skill := types.TenantSkillCatalogEntity{ID: "disabled-skill", TenantID: 1, Name: "D", InstructionOnly: true, Visibility: "private", Enabled: false} require.NoError(t, s.db.Create(&skill).Error) var storedSkill types.TenantSkillCatalogEntity require.NoError(t, s.db.First(&storedSkill, "id = ?", skill.ID).Error) require.False(t, storedSkill.Enabled) } type lifecycleAgentRepo struct { interfaces.CustomAgentRepository rows map[string]*types.CustomAgent writes int } func (r *lifecycleAgentRepo) CreateAgent(_ context.Context, a *types.CustomAgent) error { r.writes++ r.rows[a.ID] = a return nil } func (r *lifecycleAgentRepo) GetAgentByID(_ context.Context, id string, tenant uint64) (*types.CustomAgent, error) { a := r.rows[id] if a == nil || a.TenantID != tenant { return nil, repository.ErrCustomAgentNotFound } copy := *a return ©, nil } func (r *lifecycleAgentRepo) ListAgentsByTenantID(_ context.Context, tenant uint64) ([]*types.CustomAgent, error) { out := []*types.CustomAgent{} for _, a := range r.rows { if a.TenantID == tenant { copy := *a out = append(out, ©) } } return out, nil } func (r *lifecycleAgentRepo) UpdateAgent(_ context.Context, a *types.CustomAgent) error { r.writes++ r.rows[a.ID] = a return nil } func (r *lifecycleAgentRepo) DeleteAgent(_ context.Context, id string, _ uint64) error { r.writes++ delete(r.rows, id) return nil } func TestNativeAgentLifecycleReadWriteAndTenantIsolation(t *testing.T) { repo := &lifecycleAgentRepo{rows: map[string]*types.CustomAgent{ "private": {ID: "private", TenantID: 1, CreatedBy: "alice", Name: "Private", Visibility: "private", Enabled: true}, "global": {ID: "global", TenantID: 1, CreatedBy: "alice", Name: "Global", Visibility: "global", Enabled: true}, "disabled": {ID: "disabled", TenantID: 1, CreatedBy: "alice", Name: "Disabled", Visibility: "global", Enabled: false}, "legacy": {ID: "legacy", TenantID: 1, CreatedBy: "alice", Name: "Legacy"}, }} s := &customAgentService{repo: repo} bob := instructionContext(1, "bob", types.TenantRoleContributor) for _, id := range []string{"private", "disabled"} { _, err := s.GetAgentByID(bob, id) require.ErrorIs(t, err, ErrAgentNotFound) } for _, id := range []string{"global", "legacy"} { a, err := s.GetAgentByID(bob, id) require.NoError(t, err) require.True(t, a.Enabled) } list, err := s.ListAgents(bob) require.NoError(t, err) ids := []string{} for _, a := range list { ids = append(ids, a.ID) } require.Contains(t, ids, "global") require.NotContains(t, ids, "private") require.NotContains(t, ids, "disabled") _, err = s.UpdateAgent(bob, &types.CustomAgent{ID: "global", Name: "stolen"}) require.Error(t, err) require.Error(t, s.DeleteAgent(bob, "global")) _, err = s.CopyAgent(bob, "global") require.ErrorContains(t, err, "不能复制") require.Zero(t, repo.writes) shareService := &agentShareService{agentRepo: repo} _, err = shareService.ShareAgent(bob, "private", "organization", "bob", 1, types.OrgRoleViewer) require.ErrorIs(t, err, ErrNotAgentOwner, "private Agents cannot be exposed by creating an organization share") _, err = s.GetAgentByIDAndTenant(instructionContext(2, "admin", types.TenantRoleAdmin), "global", 1) require.ErrorIs(t, err, ErrAgentNotFound) alice := instructionContext(1, "alice", types.TenantRoleContributor) a, err := s.UpdateAgent(alice, &types.CustomAgent{ID: "disabled", Name: "renamed"}) require.NoError(t, err) require.False(t, a.Enabled, "old clients must preserve lifecycle fields") require.NoError(t, s.DeleteAgent(alice, "private")) } func TestInstructionCatalogCannotLeakThroughExecutableCatalogFallback(t *testing.T) { s, alice := instructionHarness(t) require.NoError(t, s.db.AutoMigrate(&types.TenantSkillEntity{})) skill, err := s.Import(alice, "secret.md", "", "", []byte("private body")) require.NoError(t, err) require.NoError(t, s.db.Create(&types.TenantSkillEntity{ID: "installation", TenantID: 1, CatalogID: skill.ID, Name: "secret", Enabled: true}).Error) service := NewTenantSkillService(repository.NewTenantSkillRepository(s.db), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) bob := instructionContext(1, "bob", types.TenantRoleContributor) list, err := service.ListCatalog(bob, 1) require.NoError(t, err) require.Empty(t, list) for _, id := range []string{skill.ID, "installation"} { _, err = service.resolveCatalog(bob, 1, id) require.Error(t, err) } list, err = service.ListCatalog(instructionContext(2, "admin", types.TenantRoleAdmin), 1) require.NoError(t, err) require.Empty(t, list) }