yao/registry/manager/agent_e2e_test.go
Max dae35cfe86 Implement force push option for agent and MCP commands
- Add a `--force` flag to the `push` command in both agent and MCP modules, allowing users to overwrite existing versions by deleting them before pushing.
- Update the `PushOptions` struct to include the `Force` field, ensuring the functionality is integrated into the push logic.
- Enhance E2E tests to validate the behavior of the force push feature, ensuring that existing versions are deleted as expected when the flag is used.
- Introduce tests for packing directories to respect `.yaoignore` rules, improving file exclusion handling during the packaging process.
2026-03-03 10:31:50 +08:00

477 lines
18 KiB
Go

package manager_test
import (
"os"
"path/filepath"
"strings"
"testing"
agentmgr "github.com/yaoapp/yao/registry/manager/agent"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
)
// =============================================================================
// Agent with 1 MCP dep: Full lifecycle — Push → Add (auto dep) → Update → Fork
// =============================================================================
func TestE2EAgent_SingleDepLifecycle(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
defer cleanupPkg(c, "assistants", "@"+testScope, "registry-agent", "1.0.0")
defer cleanupPkg(c, "assistants", "@"+testScope, "registry-agent", "2.0.0")
// Push MCP dependency first
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := mcpMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push MCP: %v", err)
}
// Verify .yaoignore fixtures exist in source before push
srcAgent := filepath.Join(devApp, "assistants", testScope, "registry-agent")
requireFileExists(t, filepath.Join(srcAgent, ".yaoignore"))
requireFileExists(t, filepath.Join(srcAgent, "dev-notes.md"))
requireFileExists(t, filepath.Join(srcAgent, "wireframe.sketch"))
requireFileExists(t, filepath.Join(srcAgent, "debug", "trace.log"))
// Push agent
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push agent: %v", err)
}
packument, err := c.GetPackument("assistants", "@"+testScope, "registry-agent")
if err != nil {
t.Fatalf("GetPackument: %v", err)
}
if packument.DistTags["latest"] != "1.0.0" {
t.Errorf("expected latest=1.0.0, got %s", packument.DistTags["latest"])
}
// Add to fresh app — MCP should auto-install
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installAgent.Add("@"+testScope+"/registry-agent", agentmgr.AddOptions{}); err != nil {
t.Fatalf("Add agent: %v", err)
}
// Agent on disk
agentDir := filepath.Join(installApp, "assistants", testScope, "registry-agent")
requireFileExists(t, filepath.Join(agentDir, "package.yao"))
requireFileContains(t, filepath.Join(agentDir, "prompts.yml"), "registry E2E testing")
// .yaoignore: excluded files must NOT appear in the installed package
requireFileNotExists(t, filepath.Join(agentDir, ".yaoignore"))
requireFileNotExists(t, filepath.Join(agentDir, "dev-notes.md"))
requireFileNotExists(t, filepath.Join(agentDir, "wireframe.sketch"))
requireFileNotExists(t, filepath.Join(agentDir, "debug", "trace.log"))
// Lockfile: agent entry
agentPkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-agent")
if agentPkg.Version != "1.0.0" {
t.Errorf("want version 1.0.0, got %s", agentPkg.Version)
}
if agentPkg.Type != common.TypeAssistant {
t.Errorf("want type assistant, got %s", agentPkg.Type)
}
if len(agentPkg.Files) == 0 {
t.Error("expected file hashes in lockfile")
}
// MCP dependency auto-installed
depPkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
if depPkg.Version != "1.0.0" {
t.Errorf("dependency version: want 1.0.0, got %s", depPkg.Version)
}
found := false
for _, rb := range depPkg.RequiredBy {
if rb == "@"+testScope+"/registry-agent" {
found = true
}
}
if !found {
t.Errorf("expected agent in MCP's required_by, got %v", depPkg.RequiredBy)
}
// Update: push v2, locally modify, then update
v2App := buildV2AgentApp(t)
pushAgentV2 := agentmgr.New(c, v2App, &common.AutoConfirmPrompter{})
if err := pushAgentV2.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Push agent v2: %v", err)
}
customPrompt := "My custom prompt - DO NOT OVERWRITE."
os.WriteFile(filepath.Join(agentDir, "prompts.yml"), []byte(customPrompt), 0644)
if err := installAgent.Update("@"+testScope+"/registry-agent", agentmgr.UpdateOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Update agent: %v", err)
}
agentPkg = requireLockfileHas(t, installApp, "@"+testScope+"/registry-agent")
if agentPkg.Version != "2.0.0" {
t.Errorf("expected v2.0.0, got %s", agentPkg.Version)
}
// Local modification preserved
preservedData, _ := os.ReadFile(filepath.Join(agentDir, "prompts.yml"))
if string(preservedData) != customPrompt {
t.Errorf("local modification should be preserved, got: %s", preservedData)
}
requireFileExists(t, filepath.Join(agentDir, "prompts.yml.new"))
requireFileContains(t, filepath.Join(agentDir, "prompts.yml.new"), "v2 registry test assistant")
// New file added by v2
requireFileExists(t, filepath.Join(agentDir, "tools.ts"))
// Fork
if err := installAgent.Fork("@"+testScope+"/registry-agent", agentmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork agent: %v", err)
}
forkDir := filepath.Join(installApp, "assistants", "local", "registry-agent")
requireFileExists(t, filepath.Join(forkDir, "package.yao"))
forkedPkg := requireLockfileHas(t, installApp, "@local/registry-agent")
if forkedPkg.ForkedFrom != "@"+testScope+"/registry-agent" {
t.Errorf("expected forked_from=@%s/registry-agent, got %s", testScope, forkedPkg.ForkedFrom)
}
if forkedPkg.IsManaged() {
t.Error("forked package should not be managed")
}
}
// =============================================================================
// Agent with 2 MCP deps: both auto-installed
// =============================================================================
func TestE2EAgent_MultiDepLifecycle(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
defer cleanupPkg(c, "mcps", "@"+testScope, "data-tools", "1.0.0")
defer cleanupPkg(c, "assistants", "@"+testScope, "analytics", "1.0.0")
// Push both MCP dependencies
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := mcpMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push registry-mcp: %v", err)
}
if err := mcpMgr.Push(testScope+".data-tools", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push data-tools: %v", err)
}
// Push analytics agent
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := agentMgr.Push(testScope+".analytics", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push analytics: %v", err)
}
// Install to fresh app
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installAgent.Add("@"+testScope+"/analytics", agentmgr.AddOptions{}); err != nil {
t.Fatalf("Add analytics: %v", err)
}
// Agent on disk
requireFileExists(t, filepath.Join(installApp, "assistants", testScope, "analytics", "package.yao"))
requireFileContains(t, filepath.Join(installApp, "assistants", testScope, "analytics", "prompts.yml"),
"analytics assistant")
// Both MCP deps auto-installed
requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
requireLockfileHas(t, installApp, "@"+testScope+"/data-tools")
// MCP files on disk
requireFileExists(t, filepath.Join(installApp, "mcps", testScope, "registry-mcp", "server.mcp.yao"))
requireFileExists(t, filepath.Join(installApp, "mcps", testScope, "data-tools", "server.mcp.yao"))
// Scripts from both MCPs
requireFileExists(t, filepath.Join(installApp, "scripts", testScope, "registry_mcp.ts"))
requireFileExists(t, filepath.Join(installApp, "scripts", testScope, "data_tools.ts"))
requireFileExists(t, filepath.Join(installApp, "scripts", testScope, "data_utils.ts"))
// required_by on both MCPs should reference analytics
lf, _ := common.LoadLockfile(installApp)
for _, mcpID := range []string{"@" + testScope + "/registry-mcp", "@" + testScope + "/data-tools"} {
mcpPkg, _ := lf.GetPackage(mcpID)
found := false
for _, rb := range mcpPkg.RequiredBy {
if rb == "@"+testScope+"/analytics" {
found = true
}
}
if !found {
t.Errorf("expected analytics in %s's required_by, got %v", mcpID, mcpPkg.RequiredBy)
}
}
// Analytics lockfile entry
agentPkg := requireLockfileHas(t, installApp, "@"+testScope+"/analytics")
if agentPkg.Version != "1.0.0" {
t.Errorf("want version 1.0.0, got %s", agentPkg.Version)
}
}
// =============================================================================
// Agent with zero deps: standalone push/add/fork
// =============================================================================
func TestE2EAgent_NoDep(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "assistants", "@"+testScope, "simple-greeter", "1.0.0")
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := agentMgr.Push(testScope+".simple-greeter", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push simple-greeter: %v", err)
}
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installAgent.Add("@"+testScope+"/simple-greeter", agentmgr.AddOptions{}); err != nil {
t.Fatalf("Add simple-greeter: %v", err)
}
requireFileExists(t, filepath.Join(installApp, "assistants", testScope, "simple-greeter", "package.yao"))
requireFileContains(t, filepath.Join(installApp, "assistants", testScope, "simple-greeter", "prompts.yml"),
"friendly greeter")
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/simple-greeter")
if pkg.Version != "1.0.0" {
t.Errorf("want version 1.0.0, got %s", pkg.Version)
}
if pkg.Type != common.TypeAssistant {
t.Errorf("want type assistant, got %s", pkg.Type)
}
// No MCP deps should be installed
lf, _ := common.LoadLockfile(installApp)
for id := range lf.Packages {
if id != "@"+testScope+"/simple-greeter" {
t.Errorf("unexpected package in lockfile: %s (standalone agent should have no deps)", id)
}
}
// Fork
if err := installAgent.Fork("@"+testScope+"/simple-greeter", agentmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork simple-greeter: %v", err)
}
requireFileExists(t, filepath.Join(installApp, "assistants", "local", "simple-greeter", "package.yao"))
forkedPkg := requireLockfileHas(t, installApp, "@local/simple-greeter")
if forkedPkg.ForkedFrom != "@"+testScope+"/simple-greeter" {
t.Errorf("expected forked_from, got %s", forkedPkg.ForkedFrom)
}
}
// =============================================================================
// Add already installed — reject without --force
// =============================================================================
func TestE2EAgent_AddAlreadyInstalled(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "assistants", "@"+testScope, "simple-greeter", "1.0.0")
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr.Push(testScope+".simple-greeter", agentmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installAgent.Add("@"+testScope+"/simple-greeter", agentmgr.AddOptions{})
err := installAgent.Add("@"+testScope+"/simple-greeter", agentmgr.AddOptions{})
if err == nil {
t.Fatal("expected already-installed error")
}
if !strings.Contains(err.Error(), "already installed") {
t.Errorf("expected 'already installed' error, got: %v", err)
}
}
// =============================================================================
// Fork to custom scope
// =============================================================================
func TestE2EAgent_ForkToCustomScope(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "assistants", "@"+testScope, "simple-greeter", "1.0.0")
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr.Push(testScope+".simple-greeter", agentmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installAgent.Add("@"+testScope+"/simple-greeter", agentmgr.AddOptions{})
if err := installAgent.Fork("@"+testScope+"/simple-greeter", agentmgr.ForkOptions{TargetScope: "acme"}); err != nil {
t.Fatalf("Fork to custom scope: %v", err)
}
requireFileExists(t, filepath.Join(installApp, "assistants", "acme", "simple-greeter", "package.yao"))
pkg := requireLockfileHas(t, installApp, "@acme/simple-greeter")
if pkg.ForkedFrom != "@"+testScope+"/simple-greeter" {
t.Errorf("expected forked_from, got %s", pkg.ForkedFrom)
}
}
// =============================================================================
// Fork target already exists
// =============================================================================
func TestE2EAgent_ForkTargetExists(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "assistants", "@"+testScope, "simple-greeter", "1.0.0")
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr.Push(testScope+".simple-greeter", agentmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installAgent.Add("@"+testScope+"/simple-greeter", agentmgr.AddOptions{})
// Pre-create target
targetDir := filepath.Join(installApp, "assistants", "local", "simple-greeter")
mustMkdir(t, targetDir)
err := installAgent.Fork("@"+testScope+"/simple-greeter", agentmgr.ForkOptions{TargetScope: "local"})
if err == nil {
t.Fatal("expected fork to fail when target exists")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error, got: %v", err)
}
}
// =============================================================================
// Push @local is rejected
// =============================================================================
func TestE2EAgent_PushLocalRejected(t *testing.T) {
c := authClient()
devApp := t.TempDir()
localDir := filepath.Join(devApp, "assistants", "local", "my-thing")
mustMkdir(t, localDir)
mustWriteFile(t, filepath.Join(localDir, "package.yao"), `{"name":"my-thing"}`)
mgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mgr.Push("local.my-thing", agentmgr.PushOptions{Version: "1.0.0"})
if err == nil {
t.Fatal("expected push of @local to be rejected")
}
if !strings.Contains(err.Error(), "@local") {
t.Errorf("expected @local rejection error, got: %v", err)
}
}
// =============================================================================
// Shared MCP dep: two agents share same MCP, verify required_by
// =============================================================================
func TestE2EAgent_SharedMCPDep(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
defer cleanupPkg(c, "assistants", "@"+testScope, "registry-agent", "1.0.0")
defer cleanupPkg(c, "assistants", "@"+testScope, "analytics", "1.0.0")
defer cleanupPkg(c, "mcps", "@"+testScope, "data-tools", "1.0.0")
// Push all dependencies
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
mcpMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
mcpMgr.Push(testScope+".data-tools", mcpmgr.PushOptions{Version: "1.0.0"})
agentPushMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentPushMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"})
agentPushMgr.Push(testScope+".analytics", agentmgr.PushOptions{Version: "1.0.0"})
// Install agent A (depends on registry-mcp)
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installAgent.Add("@"+testScope+"/registry-agent", agentmgr.AddOptions{}); err != nil {
t.Fatalf("Add registry-agent: %v", err)
}
// registry-mcp should be installed with required_by=[registry-agent]
mcpPkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
if mcpPkg.Version != "1.0.0" {
t.Errorf("want MCP version 1.0.0, got %s", mcpPkg.Version)
}
// Install agent B (depends on registry-mcp AND data-tools)
if err := installAgent.Add("@"+testScope+"/analytics", agentmgr.AddOptions{}); err != nil {
t.Fatalf("Add analytics: %v", err)
}
// registry-mcp should NOT be reinstalled, but required_by should include both agents
lf, _ := common.LoadLockfile(installApp)
mcpPkg2, _ := lf.GetPackage("@" + testScope + "/registry-mcp")
requiredBySet := map[string]bool{}
for _, rb := range mcpPkg2.RequiredBy {
requiredBySet[rb] = true
}
if !requiredBySet["@"+testScope+"/registry-agent"] {
t.Error("expected registry-agent in registry-mcp's required_by")
}
if !requiredBySet["@"+testScope+"/analytics"] {
t.Error("expected analytics in registry-mcp's required_by")
}
// data-tools should also be installed
requireLockfileHas(t, installApp, "@"+testScope+"/data-tools")
// Verify disk files are not duplicated — only one copy of each MCP
requireFileExists(t, filepath.Join(installApp, "mcps", testScope, "registry-mcp", "server.mcp.yao"))
requireFileExists(t, filepath.Join(installApp, "mcps", testScope, "data-tools", "server.mcp.yao"))
}
// =============================================================================
// Agent Fork from registry: not installed locally, pull then fork
// =============================================================================
func TestE2EAgent_ForkFromRegistry(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "assistants", "@"+testScope, "simple-greeter", "1.0.0")
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr.Push(testScope+".simple-greeter", agentmgr.PushOptions{Version: "1.0.0"})
// Fresh app — nothing installed
forkApp := t.TempDir()
forkAgent := agentmgr.New(c, forkApp, &common.AutoConfirmPrompter{})
if err := forkAgent.Fork("@"+testScope+"/simple-greeter", agentmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork from registry: %v", err)
}
requireFileExists(t, filepath.Join(forkApp, "assistants", "local", "simple-greeter", "package.yao"))
requireFileContains(t, filepath.Join(forkApp, "assistants", "local", "simple-greeter", "prompts.yml"),
"friendly greeter")
pkg := requireLockfileHas(t, forkApp, "@local/simple-greeter")
if pkg.ForkedFrom != "@"+testScope+"/simple-greeter" {
t.Errorf("expected forked_from, got %s", pkg.ForkedFrom)
}
}