Files
xinmi/internal/application/service/asset_lifecycle_test.go
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

259 lines
12 KiB
Go

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 &copy, 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, &copy)
}
}
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)
}