Update test scope and remove obsolete E2E tests

- Change the test scope identifier from "@test" to "@yaoagents" in client tests for consistency.
- Remove the outdated E2E test file `e2e_test.go` from the manager directory, as it is no longer needed.
- Enhance the agent manager to utilize the MCP manager for dependency installations, improving the handling of MCP-type dependencies.
This commit is contained in:
Max 2026-03-03 08:48:45 +08:00
parent 44af6ba759
commit 634bc1d093
8 changed files with 1749 additions and 825 deletions

View file

@ -12,7 +12,7 @@ import (
)
const (
testScope = "@test"
testScope = "@yaoagents"
)
func serverURL() string {

View file

@ -6,6 +6,7 @@ import (
"strings"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
)
// AddOptions configures the Add operation.
@ -69,6 +70,11 @@ func (m *Manager) Add(pkgID string, opts AddOptions) error {
if err := m.installDependencies(manifest.Dependencies, lf, pkgID, map[string]bool{pkgID: true}); err != nil {
return err
}
// Reload lockfile — dependency managers write their own entries to disk
lf, err = common.LoadLockfile(m.appRoot)
if err != nil {
return err
}
}
// Unpack to destination
@ -113,6 +119,8 @@ func (m *Manager) Add(pkgID string, opts AddOptions) error {
}
// installDependencies recursively installs missing dependencies.
// For MCP-type dependencies, delegates to the MCP manager which handles
// script extraction to the project root correctly.
func (m *Manager) installDependencies(deps map[string]string, lf *common.RegistryYao, parentID string, installing map[string]bool) error {
missing, conflicts, _ := common.CheckDependencies(deps, lf)
@ -130,10 +138,8 @@ func (m *Manager) installDependencies(deps map[string]string, lf *common.Registr
choice := m.prompter.Choose(msg, options)
switch choice {
case 0:
// Upgrade: treat as missing so it gets reinstalled
missing = append(missing, c)
case 1:
// Keep current
continue
default:
return fmt.Errorf("installation aborted by user")
@ -144,7 +150,6 @@ func (m *Manager) installDependencies(deps map[string]string, lf *common.Registr
return nil
}
// Build summary
var summary strings.Builder
summary.WriteString("The following dependencies need to be installed:\n")
for _, dep := range missing {
@ -160,63 +165,23 @@ func (m *Manager) installDependencies(deps map[string]string, lf *common.Registr
}
installing[dep.PackageID] = true
// Determine type from package ID by trying to pull and reading manifest
depScope, depName, err := common.ParsePackageID(dep.PackageID)
if err != nil {
if _, _, err := common.ParsePackageID(dep.PackageID); err != nil {
return err
}
// Try assistant type first, then mcp
var installed bool
for _, regType := range []string{common.TypeDirAssistants, common.TypeDirMCPs} {
zipData, digest, err := m.client.Pull(regType, "@"+depScope, depName, "latest")
if err != nil {
continue
}
manifest, err := common.ReadManifest(zipData)
if err != nil {
continue
}
pkgType := manifest.Type
destDir := common.PackageDir(pkgType, depScope, depName, m.appRoot)
if err := os.MkdirAll(destDir, 0755); err != nil {
return err
}
if _, err := common.UnpackTo(zipData, destDir); err != nil {
return fmt.Errorf("unpack dependency %s: %w", dep.PackageID, err)
}
relDir := common.PackageDirRel(pkgType, depScope, depName)
files, err := common.HashDir(destDir, relDir)
if err != nil {
return err
}
info := common.PackageInfo{
Type: pkgType,
Version: manifest.Version,
Integrity: digest,
Dependencies: manifest.Dependencies,
Files: files,
}
lf.SetPackage(dep.PackageID, info)
// Recursively install this dep's dependencies
if len(manifest.Dependencies) > 0 {
if err := m.installDependencies(manifest.Dependencies, lf, dep.PackageID, installing); err != nil {
return err
}
}
fmt.Printf(" ✓ Dependency %s@%s installed\n", dep.PackageID, manifest.Version)
installed = true
break
// Try MCP type first (most agent dependencies are MCPs), then assistant.
// Delegate to the appropriate manager so MCP script extraction is handled.
if err := m.mcpMgr.Add(dep.PackageID, mcpmgr.AddOptions{}); err == nil {
fmt.Printf(" ✓ Dependency %s installed (mcp)\n", dep.PackageID)
continue
}
if !installed {
return fmt.Errorf("failed to install dependency %s: not found in registry", dep.PackageID)
if err := m.Add(dep.PackageID, AddOptions{}); err == nil {
fmt.Printf(" ✓ Dependency %s installed (assistant)\n", dep.PackageID)
continue
}
return fmt.Errorf("failed to install dependency %s: not found in registry", dep.PackageID)
}
return nil

View file

@ -4,6 +4,7 @@ package agent
import (
"github.com/yaoapp/yao/registry"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
)
// Manager handles assistant package operations (add, update, push, fork).
@ -11,6 +12,7 @@ type Manager struct {
client *registry.Client
appRoot string
prompter common.Prompter
mcpMgr *mcpmgr.Manager
}
// New creates an agent Manager.
@ -22,5 +24,6 @@ func New(client *registry.Client, appRoot string, prompter common.Prompter) *Man
client: client,
appRoot: appRoot,
prompter: prompter,
mcpMgr: mcpmgr.New(client, appRoot, prompter),
}
}

View file

@ -0,0 +1,464 @@
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)
}
// 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")
// 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)
}
}

View file

@ -0,0 +1,361 @@
package manager_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/yaoapp/yao/registry"
"github.com/yaoapp/yao/registry/manager/common"
)
// testScope is the scope aligned with registry CI credentials (yaoagents:yaoagents).
const testScope = "yaoagents"
func registryURL() string {
if u := os.Getenv("YAO_REGISTRY_URL"); u != "" {
return u
}
return "http://localhost:8080"
}
func authClient() *registry.Client {
return registry.New(registryURL(), registry.WithAuth(testScope, testScope))
}
func cleanupPkg(c *registry.Client, pkgType, scope, name, version string) {
c.DeleteVersion(pkgType, scope, name, version)
}
// appRoot returns the path to yao-dev-app, which contains the real test fixtures
// under assistants/yaoagents/, mcps/yaoagents/, scripts/yaoagents/.
//
// Resolution order:
// 1. YAO_TEST_APPLICATION env var (set by CI and local env.local.sh)
// 2. ../yao-dev-app (local development layout)
// 3. ../app (CI layout after "Move Dependencies" step)
func appRoot(t *testing.T) string {
t.Helper()
check := func(root string) bool {
_, err := os.Stat(filepath.Join(root, "assistants", testScope, "registry-agent", "package.yao"))
return err == nil
}
if root := os.Getenv("YAO_TEST_APPLICATION"); root != "" {
abs, _ := filepath.Abs(root)
if check(abs) {
return abs
}
t.Logf("YAO_TEST_APPLICATION=%s exists but missing registry test fixtures", root)
}
for _, rel := range []string{
filepath.Join("..", "..", "..", "yao-dev-app"),
filepath.Join("..", "..", "..", "..", "app"),
filepath.Join("..", "yao-dev-app"),
} {
abs, _ := filepath.Abs(rel)
if check(abs) {
return abs
}
}
t.Skip("yao-dev-app with registry test fixtures not found; set YAO_TEST_APPLICATION")
return ""
}
// mustMkdir creates a directory tree, failing the test on error.
func mustMkdir(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
}
// mustWriteFile writes content to a file, creating parent directories as needed.
func mustWriteFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// requireFileExists asserts that a file exists on disk.
func requireFileExists(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); err != nil {
t.Fatalf("expected file to exist: %s", path)
}
}
// requireFileNotExists asserts that a file does NOT exist on disk.
func requireFileNotExists(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); err == nil {
t.Fatalf("expected file NOT to exist: %s", path)
}
}
// requireFileContains asserts that a file exists and its content contains substr.
func requireFileContains(t *testing.T, path, substr string) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("cannot read %s: %v", path, err)
}
if !strings.Contains(string(data), substr) {
t.Errorf("expected %s to contain %q, got:\n%s", filepath.Base(path), substr, data)
}
}
// requireFileNotContains asserts that a file's content does NOT contain substr.
func requireFileNotContains(t *testing.T, path, substr string) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("cannot read %s: %v", path, err)
}
if strings.Contains(string(data), substr) {
t.Errorf("expected %s NOT to contain %q, got:\n%s", filepath.Base(path), substr, data)
}
}
// requireLockfileHas asserts that the lockfile at appRoot has a package with the given ID.
func requireLockfileHas(t *testing.T, appRoot, pkgID string) common.PackageInfo {
t.Helper()
lf, err := common.LoadLockfile(appRoot)
if err != nil {
t.Fatalf("load lockfile: %v", err)
}
pkg, ok := lf.GetPackage(pkgID)
if !ok {
t.Fatalf("expected %s in lockfile, packages: %v", pkgID, lockfileKeys(lf))
}
return pkg
}
// requireLockfileNotHas asserts that the lockfile does NOT contain the given package.
func requireLockfileNotHas(t *testing.T, appRoot, pkgID string) {
t.Helper()
lf, err := common.LoadLockfile(appRoot)
if err != nil {
t.Fatalf("load lockfile: %v", err)
}
if _, ok := lf.GetPackage(pkgID); ok {
t.Fatalf("expected %s NOT in lockfile", pkgID)
}
}
// lockfileKeys returns all package IDs in a lockfile (for debug output).
func lockfileKeys(lf *common.RegistryYao) []string {
var keys []string
for k := range lf.Packages {
keys = append(keys, k)
}
return keys
}
// buildV2AgentApp creates a v2 variant of the agent+MCP fixtures in a temp directory.
// Content differs from v1 in yao-dev-app to verify update logic.
func buildV2AgentApp(t *testing.T) string {
t.Helper()
root := t.TempDir()
// Agent v2: updated description, new tools.ts
assistDir := filepath.Join(root, "assistants", testScope, "registry-agent")
mustMkdir(t, assistDir)
mustWriteFile(t, filepath.Join(assistDir, "package.yao"), `{
"name": "Registry Test Agent v2",
"avatar": "/api/__yao/app/icons/app.png",
"connector": "gpt-4o",
"description": "Enhanced v2 test assistant for registry E2E verification",
"options": { "temperature": 0.5 },
"public": false,
"mcp": {
"servers": {
"registry-mcp": { "server_id": "`+testScope+`.registry-mcp" }
}
},
"tags": ["Test", "Registry", "V2"],
"sort": 999,
"readonly": true,
"automated": false,
"mentionable": false
}`)
mustWriteFile(t, filepath.Join(assistDir, "prompts.yml"),
"system: |\n You are the v2 registry test assistant with enhanced capabilities.\n")
mustWriteFile(t, filepath.Join(assistDir, "tools.ts"),
`export function newV2Tool(): number { return 42; }`)
// MCP v2: added "suggest" tool
mcpDir := filepath.Join(root, "mcps", testScope, "registry-mcp")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "server.mcp.yao"), `{
"label": "Registry Test MCP v2",
"description": "Enhanced v2 MCP for registry E2E testing",
"transport": "process",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false }
},
"tools": {
"ping": "scripts.`+testScope+`.registry_mcp.Ping",
"echo": "scripts.`+testScope+`.registry_mcp.Echo",
"suggest": "scripts.`+testScope+`.registry_mcp.Suggest"
}
}`)
scriptDir := filepath.Join(root, "scripts", testScope)
mustMkdir(t, scriptDir)
mustWriteFile(t, filepath.Join(scriptDir, "registry_mcp.ts"), `/**
* Registry MCP test script v2
*/
function Ping(): string {
return "pong-v2";
}
function Echo(input: string): string {
return input;
}
function Suggest(prefix: string): string[] {
return ["v2-suggestion1", "v2-suggestion2"];
}
`)
return root
}
// buildV2MCPApp creates a v2 variant of the data-tools MCP for update testing.
func buildV2MCPApp(t *testing.T) string {
t.Helper()
root := t.TempDir()
mcpDir := filepath.Join(root, "mcps", testScope, "data-tools")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "server.mcp.yao"), `{
"label": "Data Tools MCP v2",
"description": "Enhanced v2 data tools with new merge capability",
"transport": "process",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false }
},
"tools": {
"aggregate": "scripts.`+testScope+`.data_tools.Aggregate",
"transform": "scripts.`+testScope+`.data_tools.Transform",
"validate": "scripts.`+testScope+`.data_tools.Validate",
"merge": "scripts.`+testScope+`.data_tools.Merge",
"format_csv": "scripts.`+testScope+`.data_utils.FormatCSV",
"format_json": "scripts.`+testScope+`.data_utils.FormatJSON"
}
}`)
scriptDir := filepath.Join(root, "scripts", testScope)
mustMkdir(t, scriptDir)
mustWriteFile(t, filepath.Join(scriptDir, "data_tools.ts"), `/**
* Data Tools MCP v2 - primary script
*/
function Aggregate(data: any[]): Record<string, number> {
return { count: data.length, version: 2 };
}
function Transform(input: string): string {
return input.toUpperCase() + "-v2";
}
function Validate(schema: string, data: any): boolean {
return schema !== "" && data !== null;
}
function Merge(a: any, b: any): any {
return { ...a, ...b };
}
`)
mustWriteFile(t, filepath.Join(scriptDir, "data_utils.ts"), `/**
* Data Tools MCP v2 - utility script
*/
function FormatCSV(rows: string[][]): string {
return "header\\n" + rows.map((r) => r.join(",")).join("\\n");
}
function FormatJSON(data: any): string {
return JSON.stringify(data, null, 2);
}
`)
return root
}
// buildV2AnalyticsApp creates a v2 variant of the analytics agent for update testing.
func buildV2AnalyticsApp(t *testing.T) string {
t.Helper()
root := t.TempDir()
assistDir := filepath.Join(root, "assistants", testScope, "analytics")
mustMkdir(t, assistDir)
mustWriteFile(t, filepath.Join(assistDir, "package.yao"), `{
"name": "Analytics Agent v2",
"avatar": "/api/__yao/app/icons/app.png",
"connector": "gpt-4o",
"description": "Enhanced v2 analytics assistant with charting support",
"options": { "temperature": 0.2 },
"public": false,
"mcp": {
"servers": {
"registry-mcp": { "server_id": "`+testScope+`.registry-mcp" },
"data-tools": { "server_id": "`+testScope+`.data-tools" }
}
},
"tags": ["Test", "Registry", "Analytics", "V2"],
"sort": 998,
"readonly": true,
"automated": false,
"mentionable": false
}`)
mustWriteFile(t, filepath.Join(assistDir, "prompts.yml"),
"system: |\n You are the v2 analytics assistant with charting capabilities.\n")
mustWriteFile(t, filepath.Join(assistDir, "chart_helper.ts"),
`export function renderChart(): string { return "chart-v2"; }`)
return root
}
// buildRobotZip creates a robot package zip and pushes it to the registry.
func buildAndPushRobotZip(t *testing.T, c *registry.Client, name string, robot interface{}, version string) {
t.Helper()
robotBytes, _ := json.Marshal(robot)
robotZipRoot := t.TempDir()
robotDir := filepath.Join(robotZipRoot, "package")
mustMkdir(t, robotDir)
mustWriteFile(t, filepath.Join(robotDir, "pkg.yao"), `{
"type": "robot",
"scope": "@`+testScope+`",
"name": "`+name+`",
"version": "`+version+`",
"description": "E2E test robot"
}`)
mustWriteFile(t, filepath.Join(robotDir, "robot.json"), string(robotBytes))
robotZip, err := common.PackDir(robotDir, &common.PkgManifest{
Type: common.TypeRobot,
Scope: "@" + testScope,
Name: name,
Version: version,
}, nil)
if err != nil {
t.Fatalf("pack robot %s: %v", name, err)
}
if _, err := c.Push("robots", "@"+testScope, name, version, robotZip); err != nil {
t.Fatalf("push robot %s: %v", name, err)
}
}

View file

@ -1,770 +0,0 @@
package manager_test
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/yaoapp/yao/registry"
agentmgr "github.com/yaoapp/yao/registry/manager/agent"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
robotmgr "github.com/yaoapp/yao/registry/manager/robot"
)
// testScope is the scope aligned with registry CI credentials (yaoagents:yaoagents).
const testScope = "yaoagents"
func registryURL() string {
if u := os.Getenv("YAO_REGISTRY_URL"); u != "" {
return u
}
return "http://localhost:8080"
}
func authClient() *registry.Client {
return registry.New(registryURL(), registry.WithAuth(testScope, testScope))
}
func cleanupPkg(c *registry.Client, pkgType, scope, name, version string) {
c.DeleteVersion(pkgType, scope, name, version)
}
// appRoot returns the path to yao-dev-app, which contains the real test fixtures
// under assistants/yaoagents/, mcps/yaoagents/, scripts/yaoagents/.
//
// Resolution order:
// 1. YAO_TEST_APPLICATION env var (set by CI and local env.local.sh)
// 2. ../yao-dev-app (local development layout)
// 3. ../app (CI layout after "Move Dependencies" step)
func appRoot(t *testing.T) string {
t.Helper()
check := func(root string) bool {
_, err := os.Stat(filepath.Join(root, "assistants", testScope, "registry-agent", "package.yao"))
return err == nil
}
if root := os.Getenv("YAO_TEST_APPLICATION"); root != "" {
abs, _ := filepath.Abs(root)
if check(abs) {
return abs
}
t.Logf("YAO_TEST_APPLICATION=%s exists but missing registry test fixtures", root)
}
// Try standard sibling layouts
for _, rel := range []string{
filepath.Join("..", "..", "..", "yao-dev-app"), // from registry/manager/ → yao-dev-app
filepath.Join("..", "..", "..", "..", "app"), // CI: from yao/registry/manager/ → ../app
filepath.Join("..", "yao-dev-app"), // from yao/ → yao-dev-app
} {
abs, _ := filepath.Abs(rel)
if check(abs) {
return abs
}
}
t.Skip("yao-dev-app with registry test fixtures not found; set YAO_TEST_APPLICATION")
return ""
}
// buildV2App creates a v2 variant of the test fixtures in a temp directory
// for update testing. The content is intentionally different from v1 in yao-dev-app.
func buildV2App(t *testing.T) string {
t.Helper()
root := t.TempDir()
// Agent v2: updated description, new prompts, added tools.ts
assistDir := filepath.Join(root, "assistants", testScope, "registry-agent")
mustMkdir(t, assistDir)
mustWriteFile(t, filepath.Join(assistDir, "package.yao"), `{
"name": "Registry Test Agent v2",
"avatar": "/api/__yao/app/icons/app.png",
"connector": "gpt-4o",
"description": "Enhanced v2 test assistant for registry E2E verification",
"options": { "temperature": 0.5 },
"public": false,
"mcp": {
"servers": {
"registry-mcp": { "server_id": "`+testScope+`.registry-mcp" }
}
},
"tags": ["Test", "Registry", "V2"],
"sort": 999,
"readonly": true,
"automated": false,
"mentionable": false
}`)
mustWriteFile(t, filepath.Join(assistDir, "prompts.yml"),
"system: |\n You are the v2 registry test assistant with enhanced capabilities.\n")
mustWriteFile(t, filepath.Join(assistDir, "tools.ts"),
`export function newV2Tool(): number { return 42; }`)
// MCP v2: added "suggest" tool
mcpDir := filepath.Join(root, "mcps", testScope, "registry-mcp")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "registry-mcp.mcp.yao"), `{
"label": "Registry Test MCP v2",
"description": "Enhanced v2 MCP for registry E2E testing",
"transport": "process",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false }
},
"tools": {
"ping": "scripts.`+testScope+`.registry_mcp.Ping",
"echo": "scripts.`+testScope+`.registry_mcp.Echo",
"suggest": "scripts.`+testScope+`.registry_mcp.Suggest"
}
}`)
// Script v2: added Suggest, changed Ping return
scriptDir := filepath.Join(root, "scripts", testScope)
mustMkdir(t, scriptDir)
mustWriteFile(t, filepath.Join(scriptDir, "registry_mcp.ts"), `/**
* Registry MCP test script v2
*/
function Ping(): string {
return "pong-v2";
}
function Echo(input: string): string {
return input;
}
function Suggest(prefix: string): string[] {
return ["v2-suggestion1", "v2-suggestion2"];
}
`)
return root
}
func mustMkdir(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
}
func mustWriteFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// =============================================================================
// E2E: MCP full lifecycle — Push from yao-dev-app → Add → Update → Fork
// =============================================================================
func TestE2EMCPRealLifecycle(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "2.0.0")
// ---- Phase 1: Push MCP v1 from yao-dev-app (real developer push) ----
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err != nil {
t.Fatalf("Push MCP v1 from yao-dev-app: %v", err)
}
// Verify in registry
packument, err := c.GetPackument("mcps", "@"+testScope, "registry-mcp")
if err != nil {
t.Fatalf("GetPackument after push: %v", err)
}
if packument.DistTags["latest"] != "1.0.0" {
t.Errorf("expected latest=1.0.0, got %s", packument.DistTags["latest"])
}
// ---- Phase 2: Add to a fresh app (simulates another developer installing) ----
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
err = installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err != nil {
t.Fatalf("Add MCP: %v", err)
}
// Verify: .mcp.yao on disk
installedMCP := filepath.Join(installApp, "mcps", testScope, "registry-mcp", "registry-mcp.mcp.yao")
if _, err := os.Stat(installedMCP); err != nil {
t.Fatal("expected registry-mcp.mcp.yao in installed dir")
}
mcpContent, _ := os.ReadFile(installedMCP)
if !strings.Contains(string(mcpContent), "scripts."+testScope+".registry_mcp.Ping") {
t.Errorf("expected process refs preserved, got: %s", mcpContent)
}
// Verify: scripts extracted to project root
installedScript := filepath.Join(installApp, "scripts", testScope, "registry_mcp.ts")
if _, err := os.Stat(installedScript); err != nil {
t.Fatalf("expected scripts/%s/registry_mcp.ts extracted", testScope)
}
scriptContent, _ := os.ReadFile(installedScript)
if !strings.Contains(string(scriptContent), "pong") {
t.Errorf("expected v1 script with 'pong', got: %s", scriptContent)
}
// Verify: lockfile (registry.yao)
lf, err := common.LoadLockfile(installApp)
if err != nil {
t.Fatal(err)
}
pkg, ok := lf.GetPackage("@" + testScope + "/registry-mcp")
if !ok {
t.Fatal("expected package in lockfile")
}
if pkg.Version != "1.0.0" {
t.Errorf("lockfile version: want 1.0.0, got %s", pkg.Version)
}
if pkg.Type != common.TypeMCP {
t.Errorf("lockfile type: want mcp, got %s", pkg.Type)
}
if pkg.Integrity == "" {
t.Error("expected integrity digest in lockfile")
}
// lockfile.Files must track both MCP dir files and script files
hasScript, hasMCP := false, false
for path := range pkg.Files {
if strings.HasPrefix(path, "scripts/") {
hasScript = true
}
if strings.HasPrefix(path, "mcps/") {
hasMCP = true
}
}
if !hasScript {
t.Error("lockfile missing script file entries")
}
if !hasMCP {
t.Error("lockfile missing MCP file entries")
}
// ---- Phase 3: Push v2 from temp dir, then Update ----
v2App := buildV2App(t)
pushMgrV2 := mcpmgr.New(c, v2App, &common.AutoConfirmPrompter{})
err = pushMgrV2.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "2.0.0"})
if err != nil {
t.Fatalf("Push MCP v2: %v", err)
}
err = installMgr.Update("@"+testScope+"/registry-mcp", mcpmgr.UpdateOptions{Version: "2.0.0"})
if err != nil {
t.Fatalf("Update MCP to v2: %v", err)
}
// lockfile version should be 2.0.0
lf, _ = common.LoadLockfile(installApp)
pkg, _ = lf.GetPackage("@" + testScope + "/registry-mcp")
if pkg.Version != "2.0.0" {
t.Errorf("expected v2.0.0 after update, got %s", pkg.Version)
}
// Script should contain v2 content
scriptContent, _ = os.ReadFile(installedScript)
if !strings.Contains(string(scriptContent), "pong-v2") {
t.Errorf("expected v2 script content after update, got: %s", scriptContent)
}
// ---- Phase 4: Fork to @local ----
err = installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"})
if err != nil {
t.Fatalf("Fork MCP: %v", err)
}
// Forked MCP directory
forkedDir := filepath.Join(installApp, "mcps", "local", "registry-mcp")
if _, err := os.Stat(forkedDir); err != nil {
t.Fatal("expected forked MCP directory at mcps/local/registry-mcp/")
}
// Process refs rewritten: scripts.yaoagents.* → scripts.local.*
forkedMCPContent, _ := os.ReadFile(filepath.Join(forkedDir, "registry-mcp.mcp.yao"))
if !strings.Contains(string(forkedMCPContent), "scripts.local.registry_mcp.Ping") {
t.Errorf("expected rewritten process ref scripts.local.*, got: %s", forkedMCPContent)
}
if strings.Contains(string(forkedMCPContent), "scripts."+testScope+".") {
t.Errorf("forked MCP still references original scope: %s", forkedMCPContent)
}
// Forked scripts copied
forkedScript := filepath.Join(installApp, "scripts", "local", "registry_mcp.ts")
if _, err := os.Stat(forkedScript); err != nil {
t.Fatal("expected scripts/local/registry_mcp.ts after fork")
}
// Lockfile: forked entry is unmanaged
lf, _ = common.LoadLockfile(installApp)
forkedPkg, ok := lf.GetPackage("@local/registry-mcp")
if !ok {
t.Fatal("expected @local/registry-mcp in lockfile")
}
if forkedPkg.ForkedFrom != "@"+testScope+"/registry-mcp" {
t.Errorf("expected forked_from=@%s/registry-mcp, got %s", testScope, forkedPkg.ForkedFrom)
}
if forkedPkg.IsManaged() {
t.Error("forked package should not be managed")
}
}
// =============================================================================
// E2E: Agent full lifecycle — Push → Add (auto-installs MCP dep) → Update → Fork
// =============================================================================
func TestE2EAgentRealLifecycle(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")
// ---- Phase 1: Push MCP dependency first (agent's package.yao references it) ----
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mcpMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err != nil {
t.Fatalf("Push MCP dependency: %v", err)
}
// ---- Phase 2: Push assistant from yao-dev-app ----
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err = agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"})
if err != nil {
t.Fatalf("Push agent: %v", err)
}
packument, err := c.GetPackument("assistants", "@"+testScope, "registry-agent")
if err != nil {
t.Fatalf("GetPackument agent: %v", err)
}
if packument.DistTags["latest"] != "1.0.0" {
t.Errorf("expected latest=1.0.0, got %s", packument.DistTags["latest"])
}
// ---- Phase 3: Add agent to fresh app (MCP dependency should auto-install) ----
installApp := t.TempDir()
installAgent := agentmgr.New(c, installApp, &common.AutoConfirmPrompter{})
err = installAgent.Add("@"+testScope+"/registry-agent", agentmgr.AddOptions{})
if err != nil {
t.Fatalf("Add agent: %v", err)
}
// Agent directory on disk
agentDir := filepath.Join(installApp, "assistants", testScope, "registry-agent")
if _, err := os.Stat(agentDir); err != nil {
t.Fatal("expected assistants/" + testScope + "/registry-agent/ directory")
}
if _, err := os.Stat(filepath.Join(agentDir, "package.yao")); err != nil {
t.Error("expected package.yao in installed agent")
}
promptsContent, _ := os.ReadFile(filepath.Join(agentDir, "prompts.yml"))
if !strings.Contains(string(promptsContent), "registry E2E testing") {
t.Errorf("expected original prompts content, got: %s", promptsContent)
}
// Lockfile: agent entry
lf, err := common.LoadLockfile(installApp)
if err != nil {
t.Fatal(err)
}
agentPkg, ok := lf.GetPackage("@" + testScope + "/registry-agent")
if !ok {
t.Fatal("expected agent in lockfile")
}
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, ok := lf.GetPackage("@" + testScope + "/registry-mcp")
if !ok {
t.Fatal("expected MCP dependency @" + testScope + "/registry-mcp auto-installed")
}
if depPkg.Version != "1.0.0" {
t.Errorf("dependency version: want 1.0.0, got %s", depPkg.Version)
}
// required_by set correctly
found := false
for _, rb := range depPkg.RequiredBy {
if rb == "@"+testScope+"/registry-agent" {
found = true
}
}
if !found {
t.Errorf("expected @%s/registry-agent in dependency's required_by, got %v", testScope, depPkg.RequiredBy)
}
// ---- Phase 4: Push agent v2 and Update (local modification preserved) ----
v2App := buildV2App(t)
pushAgentV2 := agentmgr.New(c, v2App, &common.AutoConfirmPrompter{})
err = pushAgentV2.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "2.0.0"})
if err != nil {
t.Fatalf("Push agent v2: %v", err)
}
// Locally modify prompts.yml before update (simulates developer customization)
customPrompt := "My custom prompt - DO NOT OVERWRITE."
os.WriteFile(filepath.Join(agentDir, "prompts.yml"), []byte(customPrompt), 0644)
err = installAgent.Update("@"+testScope+"/registry-agent", agentmgr.UpdateOptions{Version: "2.0.0"})
if err != nil {
t.Fatalf("Update agent to v2: %v", err)
}
// Lockfile updated to v2
lf, _ = common.LoadLockfile(installApp)
agentPkg, _ = lf.GetPackage("@" + testScope + "/registry-agent")
if agentPkg.Version != "2.0.0" {
t.Errorf("expected v2.0.0 after update, got %s", agentPkg.Version)
}
// Locally modified file PRESERVED (not overwritten)
preservedData, _ := os.ReadFile(filepath.Join(agentDir, "prompts.yml"))
if string(preservedData) != customPrompt {
t.Errorf("local modification should be preserved, got: %s", preservedData)
}
// .new file created with upstream v2 content
newFile := filepath.Join(agentDir, "prompts.yml.new")
if _, err := os.Stat(newFile); err != nil {
t.Error("expected prompts.yml.new with upstream content")
}
newContent, _ := os.ReadFile(newFile)
if !strings.Contains(string(newContent), "v2 registry test assistant") {
t.Errorf("expected v2 content in .new file, got: %s", newContent)
}
// New file tools.ts added by v2
if _, err := os.Stat(filepath.Join(agentDir, "tools.ts")); err != nil {
t.Error("expected new file tools.ts added during update")
}
// ---- Phase 5: Fork to @local ----
err = installAgent.Fork("@"+testScope+"/registry-agent", agentmgr.ForkOptions{TargetScope: "local"})
if err != nil {
t.Fatalf("Fork agent: %v", err)
}
forkDir := filepath.Join(installApp, "assistants", "local", "registry-agent")
if _, err := os.Stat(forkDir); err != nil {
t.Fatal("expected assistants/local/registry-agent/ after fork")
}
if _, err := os.Stat(filepath.Join(forkDir, "package.yao")); err != nil {
t.Error("expected package.yao in forked dir")
}
lf, _ = common.LoadLockfile(installApp)
forkedPkg, ok := lf.GetPackage("@local/registry-agent")
if !ok {
t.Fatal("expected @local/registry-agent in lockfile")
}
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")
}
}
// =============================================================================
// E2E: Push→Pull roundtrip (byte-for-byte content verification)
// =============================================================================
func TestE2EPushPullRoundtrip(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err != nil {
t.Fatalf("Push: %v", err)
}
pullApp := t.TempDir()
pullMgr := mcpmgr.New(c, pullApp, &common.AutoConfirmPrompter{})
err = pullMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err != nil {
t.Fatalf("Add: %v", err)
}
// Script byte-for-byte comparison
origScript, _ := os.ReadFile(filepath.Join(devApp, "scripts", testScope, "registry_mcp.ts"))
pulledScript, _ := os.ReadFile(filepath.Join(pullApp, "scripts", testScope, "registry_mcp.ts"))
if string(origScript) != string(pulledScript) {
t.Errorf("script mismatch.\nOriginal:\n%s\nPulled:\n%s", origScript, pulledScript)
}
// MCP definition byte-for-byte comparison
origMCP, _ := os.ReadFile(filepath.Join(devApp, "mcps", testScope, "registry-mcp", "registry-mcp.mcp.yao"))
pulledMCP, _ := os.ReadFile(filepath.Join(pullApp, "mcps", testScope, "registry-mcp", "registry-mcp.mcp.yao"))
if string(origMCP) != string(pulledMCP) {
t.Errorf("MCP mismatch.\nOriginal:\n%s\nPulled:\n%s", origMCP, pulledMCP)
}
}
// =============================================================================
// E2E: Robot Add with dependency resolution
// =============================================================================
func TestE2ERobotRealLifecycle(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, "robots", "@"+testScope, "test-bot", "1.0.0")
// Push MCP and Agent that the robot depends on
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr := agentmgr.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)
}
if err := agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push agent: %v", err)
}
// Build and push robot package (robots are DB records, so we build zip manually)
robotJSON := map[string]interface{}{
"display_name": "E2E Test Bot",
"system_prompt": "You are an E2E test robot.",
"language_model": "gpt-4o",
"robot_config": map[string]interface{}{
"resources": map[string]interface{}{
"phases": map[string]string{
"host": testScope + ".registry-agent",
},
},
},
"mcp_servers": []string{testScope + ".registry-mcp"},
}
robotBytes, _ := json.Marshal(robotJSON)
robotZipRoot := t.TempDir()
robotDir := filepath.Join(robotZipRoot, "package")
mustMkdir(t, robotDir)
mustWriteFile(t, filepath.Join(robotDir, "pkg.yao"), `{
"type": "robot",
"scope": "@`+testScope+`",
"name": "test-bot",
"version": "1.0.0",
"description": "E2E test robot"
}`)
mustWriteFile(t, filepath.Join(robotDir, "robot.json"), string(robotBytes))
robotZip, err := common.PackDir(robotDir, &common.PkgManifest{
Type: common.TypeRobot,
Scope: "@" + testScope,
Name: "test-bot",
Version: "1.0.0",
}, nil)
if err != nil {
t.Fatalf("Pack robot: %v", err)
}
if _, err := c.Push("robots", "@"+testScope, "test-bot", "1.0.0", robotZip); err != nil {
t.Fatalf("Push robot: %v", err)
}
// ---- Add robot to fresh app (dependencies should auto-install) ----
installApp := t.TempDir()
rMgr := robotmgr.New(c, installApp, &common.AutoConfirmPrompter{})
robot, err := rMgr.Add("@"+testScope+"/test-bot", robotmgr.AddOptions{TeamID: "team-e2e"})
if err != nil {
t.Fatalf("Add robot: %v", err)
}
if robot.DisplayName != "E2E Test Bot" {
t.Errorf("want display_name 'E2E Test Bot', got %q", robot.DisplayName)
}
if robot.SystemPrompt != "You are an E2E test robot." {
t.Errorf("unexpected system_prompt: %s", robot.SystemPrompt)
}
lf, _ := common.LoadLockfile(installApp)
robotPkg, ok := lf.GetPackage("@" + testScope + "/test-bot")
if !ok {
t.Fatal("expected robot in lockfile")
}
if robotPkg.Type != common.TypeRobot {
t.Errorf("want robot type, got %s", robotPkg.Type)
}
if robotPkg.TeamID != "team-e2e" {
t.Errorf("want team_id team-e2e, got %s", robotPkg.TeamID)
}
// Dependencies auto-installed
if _, ok := lf.GetPackage("@" + testScope + "/registry-agent"); !ok {
t.Error("expected agent dependency auto-installed")
}
if _, ok := lf.GetPackage("@" + testScope + "/registry-mcp"); !ok {
t.Error("expected MCP dependency auto-installed")
}
// Files on disk
if _, err := os.Stat(filepath.Join(installApp, "assistants", testScope, "registry-agent", "package.yao")); err != nil {
t.Error("expected agent package.yao on disk after robot add")
}
if _, err := os.Stat(filepath.Join(installApp, "mcps", testScope, "registry-mcp", "registry-mcp.mcp.yao")); err != nil {
t.Error("expected MCP .mcp.yao on disk after robot add")
}
// required_by
agentPkg, _ := lf.GetPackage("@" + testScope + "/registry-agent")
foundRB := false
for _, rb := range agentPkg.RequiredBy {
if rb == "@"+testScope+"/test-bot" {
foundRB = true
}
}
if !foundRB {
t.Errorf("expected robot in agent's required_by, got %v", agentPkg.RequiredBy)
}
}
// =============================================================================
// E2E: @local push is rejected
// =============================================================================
func TestE2EPushLocalRejected(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)
}
}
// =============================================================================
// E2E: MCP Push rejects scripts in wrong scope
// =============================================================================
func TestE2EMCPPushWrongScriptScope(t *testing.T) {
c := authClient()
devApp := t.TempDir()
mcpDir := filepath.Join(devApp, "mcps", testScope, "bad-mcp")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "bad.mcp.yao"), `{
"transport": "process",
"tools": {
"run": "scripts.other.bad.Run"
}
}`)
mustWriteFile(t, filepath.Join(devApp, "scripts", "other", "bad.ts"), "export function Run() {}")
mgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mgr.Push(testScope+".bad-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err == nil {
t.Fatal("expected push to be rejected due to script scope mismatch")
}
if !strings.Contains(err.Error(), "scope mismatch") {
t.Errorf("expected scope mismatch error, got: %v", err)
}
}
// =============================================================================
// E2E: Update of forked package is rejected
// =============================================================================
func TestE2EUpdateForkedRejected(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
// Fork it
installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"})
// Update forked should fail
err := installMgr.Update("@local/registry-mcp", mcpmgr.UpdateOptions{Version: "2.0.0"})
if err == nil {
t.Fatal("expected update of forked package to be rejected")
}
if !strings.Contains(err.Error(), "forked") {
t.Errorf("expected 'forked' in error, got: %v", err)
}
}
// =============================================================================
// E2E: Directory conflict detection
// =============================================================================
func TestE2EDirectoryConflict(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
// Pre-create an unmanaged directory at the install path
installApp := t.TempDir()
conflictDir := filepath.Join(installApp, "mcps", testScope, "registry-mcp")
mustMkdir(t, conflictDir)
mustWriteFile(t, filepath.Join(conflictDir, "my-custom.mcp.yao"), `{"transport":"stdio"}`)
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err == nil {
t.Fatal("expected directory conflict error")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error, got: %v", err)
}
}

View file

@ -0,0 +1,645 @@
package manager_test
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
)
// =============================================================================
// Process MCP: Full lifecycle — Push → Add → Update → Fork
// =============================================================================
func TestE2EMCP_ProcessLifecycle(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "2.0.0")
// Phase 1: Push v1 from yao-dev-app
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push MCP v1: %v", err)
}
packument, err := c.GetPackument("mcps", "@"+testScope, "registry-mcp")
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"])
}
// Phase 2: Add to a fresh app
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{}); err != nil {
t.Fatalf("Add MCP: %v", err)
}
// Verify .mcp.yao on disk
installedMCP := filepath.Join(installApp, "mcps", testScope, "registry-mcp", "server.mcp.yao")
requireFileExists(t, installedMCP)
requireFileContains(t, installedMCP, "scripts."+testScope+".registry_mcp.Ping")
// Verify scripts extracted to project root
installedScript := filepath.Join(installApp, "scripts", testScope, "registry_mcp.ts")
requireFileExists(t, installedScript)
requireFileContains(t, installedScript, "pong")
// Verify lockfile
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
if pkg.Version != "1.0.0" {
t.Errorf("lockfile version: want 1.0.0, got %s", pkg.Version)
}
if pkg.Type != common.TypeMCP {
t.Errorf("lockfile type: want mcp, got %s", pkg.Type)
}
if pkg.Integrity == "" {
t.Error("expected integrity digest in lockfile")
}
hasScript, hasMCP := false, false
for path := range pkg.Files {
if strings.HasPrefix(path, "scripts/") {
hasScript = true
}
if strings.HasPrefix(path, "mcps/") {
hasMCP = true
}
}
if !hasScript {
t.Error("lockfile missing script file entries")
}
if !hasMCP {
t.Error("lockfile missing MCP file entries")
}
// Phase 3: Push v2, then Update
v2App := buildV2AgentApp(t)
pushMgrV2 := mcpmgr.New(c, v2App, &common.AutoConfirmPrompter{})
if err := pushMgrV2.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Push MCP v2: %v", err)
}
if err := installMgr.Update("@"+testScope+"/registry-mcp", mcpmgr.UpdateOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Update MCP to v2: %v", err)
}
pkg = requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
if pkg.Version != "2.0.0" {
t.Errorf("expected v2.0.0 after update, got %s", pkg.Version)
}
requireFileContains(t, installedScript, "pong-v2")
// Phase 4: Fork to @local
if err := installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork MCP: %v", err)
}
forkedDir := filepath.Join(installApp, "mcps", "local", "registry-mcp")
requireFileExists(t, forkedDir)
forkedMCPPath := filepath.Join(forkedDir, "server.mcp.yao")
requireFileContains(t, forkedMCPPath, "scripts.local.registry_mcp.Ping")
requireFileNotContains(t, forkedMCPPath, "scripts."+testScope+".")
forkedScript := filepath.Join(installApp, "scripts", "local", "registry_mcp.ts")
requireFileExists(t, forkedScript)
forkedPkg := requireLockfileHas(t, installApp, "@local/registry-mcp")
if forkedPkg.ForkedFrom != "@"+testScope+"/registry-mcp" {
t.Errorf("expected forked_from=@%s/registry-mcp, got %s", testScope, forkedPkg.ForkedFrom)
}
if forkedPkg.IsManaged() {
t.Error("forked package should not be managed")
}
}
// =============================================================================
// SSE MCP: Push → Add → Fork (no scripts, no process refs)
// =============================================================================
func TestE2EMCP_SSELifecycle(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "sse-proxy", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := pushMgr.Push(testScope+".sse-proxy", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push SSE MCP: %v", err)
}
// Add to fresh app
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installMgr.Add("@"+testScope+"/sse-proxy", mcpmgr.AddOptions{}); err != nil {
t.Fatalf("Add SSE MCP: %v", err)
}
// Verify .mcp.yao on disk
installedMCP := filepath.Join(installApp, "mcps", testScope, "sse-proxy", "server.mcp.yao")
requireFileExists(t, installedMCP)
requireFileContains(t, installedMCP, `"transport": "sse"`)
requireFileContains(t, installedMCP, "mcp.example.com")
// No scripts should be extracted for SSE
scriptsDir := filepath.Join(installApp, "scripts")
if _, err := os.Stat(scriptsDir); err == nil {
entries, _ := os.ReadDir(scriptsDir)
if len(entries) > 0 {
t.Errorf("SSE MCP should not extract any scripts, found entries under scripts/")
}
}
// Lockfile
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/sse-proxy")
if pkg.Version != "1.0.0" {
t.Errorf("want version 1.0.0, got %s", pkg.Version)
}
if pkg.Type != common.TypeMCP {
t.Errorf("want type mcp, got %s", pkg.Type)
}
// No script files tracked in lockfile
for path := range pkg.Files {
if strings.HasPrefix(path, "scripts/") {
t.Errorf("SSE MCP lockfile should not track scripts, found: %s", path)
}
}
// Fork to @local — no process ref rewriting needed
if err := installMgr.Fork("@"+testScope+"/sse-proxy", mcpmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork SSE MCP: %v", err)
}
forkedMCP := filepath.Join(installApp, "mcps", "local", "sse-proxy", "server.mcp.yao")
requireFileExists(t, forkedMCP)
requireFileContains(t, forkedMCP, `"transport": "sse"`)
forkedPkg := requireLockfileHas(t, installApp, "@local/sse-proxy")
if forkedPkg.ForkedFrom != "@"+testScope+"/sse-proxy" {
t.Errorf("expected forked_from=@%s/sse-proxy, got %s", testScope, forkedPkg.ForkedFrom)
}
}
// =============================================================================
// Multi-script MCP: Push → Add → verify all scripts unpacked
// =============================================================================
func TestE2EMCP_MultiScriptPack(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "data-tools", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := pushMgr.Push(testScope+".data-tools", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push data-tools: %v", err)
}
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installMgr.Add("@"+testScope+"/data-tools", mcpmgr.AddOptions{}); err != nil {
t.Fatalf("Add data-tools: %v", err)
}
// Both script files should be extracted
requireFileExists(t, filepath.Join(installApp, "scripts", testScope, "data_tools.ts"))
requireFileExists(t, filepath.Join(installApp, "scripts", testScope, "data_utils.ts"))
requireFileContains(t, filepath.Join(installApp, "scripts", testScope, "data_tools.ts"), "Aggregate")
requireFileContains(t, filepath.Join(installApp, "scripts", testScope, "data_utils.ts"), "FormatCSV")
// MCP definition on disk
requireFileExists(t, filepath.Join(installApp, "mcps", testScope, "data-tools", "server.mcp.yao"))
requireFileContains(t, filepath.Join(installApp, "mcps", testScope, "data-tools", "server.mcp.yao"),
"scripts."+testScope+".data_utils.FormatCSV")
// Lockfile tracks both script files
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/data-tools")
scriptCount := 0
for path := range pkg.Files {
if strings.HasPrefix(path, "scripts/") {
scriptCount++
}
}
if scriptCount < 2 {
t.Errorf("expected at least 2 script files tracked in lockfile, got %d", scriptCount)
}
}
// =============================================================================
// Multi-script MCP: Update with local modification
// =============================================================================
func TestE2EMCP_MultiScriptUpdate(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "data-tools", "1.0.0")
defer cleanupPkg(c, "mcps", "@"+testScope, "data-tools", "2.0.0")
// Push v1
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := pushMgr.Push(testScope+".data-tools", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push v1: %v", err)
}
// Install v1
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
if err := installMgr.Add("@"+testScope+"/data-tools", mcpmgr.AddOptions{}); err != nil {
t.Fatalf("Add v1: %v", err)
}
// Locally modify one of the script files
modifiedScript := filepath.Join(installApp, "scripts", testScope, "data_tools.ts")
customContent := "// MY CUSTOM DATA TOOLS\nfunction Aggregate() { return 'custom'; }\n"
os.WriteFile(modifiedScript, []byte(customContent), 0644)
// Push v2
v2App := buildV2MCPApp(t)
pushMgrV2 := mcpmgr.New(c, v2App, &common.AutoConfirmPrompter{})
if err := pushMgrV2.Push(testScope+".data-tools", mcpmgr.PushOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Push v2: %v", err)
}
// Update to v2
if err := installMgr.Update("@"+testScope+"/data-tools", mcpmgr.UpdateOptions{Version: "2.0.0"}); err != nil {
t.Fatalf("Update to v2: %v", err)
}
// Modified script should be preserved, .new file created
content, _ := os.ReadFile(modifiedScript)
if !strings.Contains(string(content), "MY CUSTOM DATA TOOLS") {
t.Error("local modification to data_tools.ts should be preserved")
}
requireFileExists(t, modifiedScript+".new")
requireFileContains(t, modifiedScript+".new", "version: 2")
// Unmodified script should be updated in-place
utilsScript := filepath.Join(installApp, "scripts", testScope, "data_utils.ts")
requireFileContains(t, utilsScript, "header")
// Lockfile version should be 2.0.0
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/data-tools")
if pkg.Version != "2.0.0" {
t.Errorf("expected v2.0.0 after update, got %s", pkg.Version)
}
}
// =============================================================================
// Push → Pull roundtrip (byte-for-byte content verification)
// =============================================================================
func TestE2EMCP_PushPullRoundtrip(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
if err := pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push: %v", err)
}
pullApp := t.TempDir()
pullMgr := mcpmgr.New(c, pullApp, &common.AutoConfirmPrompter{})
if err := pullMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{}); err != nil {
t.Fatalf("Add: %v", err)
}
origScript, _ := os.ReadFile(filepath.Join(devApp, "scripts", testScope, "registry_mcp.ts"))
pulledScript, _ := os.ReadFile(filepath.Join(pullApp, "scripts", testScope, "registry_mcp.ts"))
if string(origScript) != string(pulledScript) {
t.Errorf("script mismatch.\nOriginal:\n%s\nPulled:\n%s", origScript, pulledScript)
}
origMCP, _ := os.ReadFile(filepath.Join(devApp, "mcps", testScope, "registry-mcp", "server.mcp.yao"))
pulledMCP, _ := os.ReadFile(filepath.Join(pullApp, "mcps", testScope, "registry-mcp", "server.mcp.yao"))
if string(origMCP) != string(pulledMCP) {
t.Errorf("MCP mismatch.\nOriginal:\n%s\nPulled:\n%s", origMCP, pulledMCP)
}
}
// =============================================================================
// Push rejects wrong script scope
// =============================================================================
func TestE2EMCP_PushWrongScriptScope(t *testing.T) {
c := authClient()
devApp := t.TempDir()
mcpDir := filepath.Join(devApp, "mcps", testScope, "bad-mcp")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "bad.mcp.yao"), `{
"transport": "process",
"tools": {
"run": "scripts.other.bad.Run"
}
}`)
mustWriteFile(t, filepath.Join(devApp, "scripts", "other", "bad.ts"), "export function Run() {}")
mgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mgr.Push(testScope+".bad-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err == nil {
t.Fatal("expected push to be rejected due to script scope mismatch")
}
if !strings.Contains(err.Error(), "scope mismatch") {
t.Errorf("expected scope mismatch error, got: %v", err)
}
}
// =============================================================================
// Push rejects when referenced script file is missing
// =============================================================================
func TestE2EMCP_PushMissingScript(t *testing.T) {
c := authClient()
devApp := t.TempDir()
mcpDir := filepath.Join(devApp, "mcps", testScope, "missing-script-mcp")
mustMkdir(t, mcpDir)
mustWriteFile(t, filepath.Join(mcpDir, "server.mcp.yao"), `{
"transport": "process",
"tools": {
"run": "scripts.`+testScope+`.nonexistent.Run"
}
}`)
mgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mgr.Push(testScope+".missing-script-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
if err == nil {
t.Fatal("expected push to fail when script file is missing")
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("expected 'not found' error, got: %v", err)
}
}
// =============================================================================
// Push requires --version
// =============================================================================
func TestE2EMCP_PushNoVersion(t *testing.T) {
c := authClient()
devApp := appRoot(t)
mgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
err := mgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{})
if err == nil {
t.Fatal("expected push to fail without --version")
}
if !strings.Contains(err.Error(), "version") {
t.Errorf("expected version error, got: %v", err)
}
}
// =============================================================================
// Add: directory conflict (exists but not managed)
// =============================================================================
func TestE2EMCP_AddDirectoryConflict(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
conflictDir := filepath.Join(installApp, "mcps", testScope, "registry-mcp")
mustMkdir(t, conflictDir)
mustWriteFile(t, filepath.Join(conflictDir, "my-custom.mcp.yao"), `{"transport":"sse"}`)
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err == nil {
t.Fatal("expected directory conflict error")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error, got: %v", err)
}
}
// =============================================================================
// Add: script conflict (scripts/ file exists but not tracked)
// =============================================================================
func TestE2EMCP_AddScriptConflict(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
mustWriteFile(t, filepath.Join(installApp, "scripts", testScope, "registry_mcp.ts"),
"// my existing custom script — should block install\n")
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err == nil {
t.Fatal("expected script conflict error")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error about script, got: %v", err)
}
}
// =============================================================================
// Add: already installed — reject without --force
// =============================================================================
func TestE2EMCP_AddAlreadyInstalled(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
// Second add should fail
err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err == nil {
t.Fatal("expected already-installed error on second add")
}
if !strings.Contains(err.Error(), "already installed") {
t.Errorf("expected 'already installed' error, got: %v", err)
}
}
// =============================================================================
// Add: --force reinstall
// =============================================================================
func TestE2EMCP_AddForceReinstall(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
// Force reinstall should succeed
err := installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{Force: true})
if err != nil {
t.Fatalf("force reinstall should succeed: %v", err)
}
pkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
if pkg.Version != "1.0.0" {
t.Errorf("expected 1.0.0 after force reinstall, got %s", pkg.Version)
}
}
// =============================================================================
// Update of forked package is rejected
// =============================================================================
func TestE2EMCP_UpdateForkedRejected(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"})
err := installMgr.Update("@local/registry-mcp", mcpmgr.UpdateOptions{Version: "2.0.0"})
if err == nil {
t.Fatal("expected update of forked package to be rejected")
}
if !strings.Contains(err.Error(), "forked") {
t.Errorf("expected 'forked' in error, got: %v", err)
}
}
// =============================================================================
// Fork: target already exists
// =============================================================================
func TestE2EMCP_ForkTargetExists(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
// Pre-create target directory
targetDir := filepath.Join(installApp, "mcps", "local", "registry-mcp")
mustMkdir(t, targetDir)
err := installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"})
if err == nil {
t.Fatal("expected fork to fail when target directory exists")
}
if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected 'already exists' error, got: %v", err)
}
}
// =============================================================================
// Fork from registry: package not installed locally, pull then fork
// =============================================================================
func TestE2EMCP_ForkFromRegistry(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
// Fresh app — nothing installed
forkApp := t.TempDir()
forkMgr := mcpmgr.New(c, forkApp, &common.AutoConfirmPrompter{})
if err := forkMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "local"}); err != nil {
t.Fatalf("Fork from registry: %v", err)
}
// Forked MCP on disk
forkedMCP := filepath.Join(forkApp, "mcps", "local", "registry-mcp", "server.mcp.yao")
requireFileExists(t, forkedMCP)
requireFileContains(t, forkedMCP, "scripts.local.registry_mcp.Ping")
// Forked scripts on disk
requireFileExists(t, filepath.Join(forkApp, "scripts", "local", "registry_mcp.ts"))
// Lockfile entry
pkg := requireLockfileHas(t, forkApp, "@local/registry-mcp")
if pkg.ForkedFrom != "@"+testScope+"/registry-mcp" {
t.Errorf("expected forked_from, got %s", pkg.ForkedFrom)
}
if pkg.IsManaged() {
t.Error("forked package should not be managed")
}
}
// =============================================================================
// Fork to custom scope (not @local)
// =============================================================================
func TestE2EMCP_ForkToCustomScope(t *testing.T) {
c := authClient()
devApp := appRoot(t)
defer cleanupPkg(c, "mcps", "@"+testScope, "registry-mcp", "1.0.0")
pushMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
pushMgr.Push(testScope+".registry-mcp", mcpmgr.PushOptions{Version: "1.0.0"})
installApp := t.TempDir()
installMgr := mcpmgr.New(c, installApp, &common.AutoConfirmPrompter{})
installMgr.Add("@"+testScope+"/registry-mcp", mcpmgr.AddOptions{})
if err := installMgr.Fork("@"+testScope+"/registry-mcp", mcpmgr.ForkOptions{TargetScope: "mycompany"}); err != nil {
t.Fatalf("Fork to custom scope: %v", err)
}
forkedMCP := filepath.Join(installApp, "mcps", "mycompany", "registry-mcp", "server.mcp.yao")
requireFileExists(t, forkedMCP)
requireFileContains(t, forkedMCP, "scripts.mycompany.registry_mcp.Ping")
forkedScript := filepath.Join(installApp, "scripts", "mycompany", "registry_mcp.ts")
requireFileExists(t, forkedScript)
pkg := requireLockfileHas(t, installApp, "@mycompany/registry-mcp")
if pkg.ForkedFrom != "@"+testScope+"/registry-mcp" {
t.Errorf("unexpected forked_from: %s", pkg.ForkedFrom)
}
}

View file

@ -0,0 +1,256 @@
package manager_test
import (
"testing"
agentmgr "github.com/yaoapp/yao/registry/manager/agent"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
robotmgr "github.com/yaoapp/yao/registry/manager/robot"
)
// =============================================================================
// Robot Add with agent + MCP dependencies
// =============================================================================
func TestE2ERobot_AddWithDeps(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, "robots", "@"+testScope, "test-bot", "1.0.0")
// Push dependencies
mcpMgr := mcpmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr := agentmgr.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)
}
if err := agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
t.Fatalf("Push agent: %v", err)
}
// Build and push robot package
robotJSON := map[string]interface{}{
"display_name": "E2E Test Bot",
"system_prompt": "You are an E2E test robot.",
"language_model": "gpt-4o",
"robot_config": map[string]interface{}{
"resources": map[string]interface{}{
"phases": map[string]string{
"host": testScope + ".registry-agent",
},
},
},
"mcp_servers": []string{testScope + ".registry-mcp"},
}
buildAndPushRobotZip(t, c, "test-bot", robotJSON, "1.0.0")
// Install robot to fresh app
installApp := t.TempDir()
rMgr := robotmgr.New(c, installApp, &common.AutoConfirmPrompter{})
robot, err := rMgr.Add("@"+testScope+"/test-bot", robotmgr.AddOptions{TeamID: "team-e2e"})
if err != nil {
t.Fatalf("Add robot: %v", err)
}
if robot.DisplayName != "E2E Test Bot" {
t.Errorf("want display_name 'E2E Test Bot', got %q", robot.DisplayName)
}
if robot.SystemPrompt != "You are an E2E test robot." {
t.Errorf("unexpected system_prompt: %s", robot.SystemPrompt)
}
// Lockfile: robot entry
robotPkg := requireLockfileHas(t, installApp, "@"+testScope+"/test-bot")
if robotPkg.Type != common.TypeRobot {
t.Errorf("want robot type, got %s", robotPkg.Type)
}
if robotPkg.TeamID != "team-e2e" {
t.Errorf("want team_id team-e2e, got %s", robotPkg.TeamID)
}
// Dependencies auto-installed
requireLockfileHas(t, installApp, "@"+testScope+"/registry-agent")
requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
// Files on disk
requireFileExists(t, installApp+"/assistants/"+testScope+"/registry-agent/package.yao")
requireFileExists(t, installApp+"/mcps/"+testScope+"/registry-mcp/server.mcp.yao")
// required_by on agent from robot
lf, _ := common.LoadLockfile(installApp)
agentPkg, _ := lf.GetPackage("@" + testScope + "/registry-agent")
foundRB := false
for _, rb := range agentPkg.RequiredBy {
if rb == "@"+testScope+"/test-bot" {
foundRB = true
}
}
if !foundRB {
t.Errorf("expected robot in agent's required_by, got %v", agentPkg.RequiredBy)
}
}
// =============================================================================
// Robot Add with no dependencies
// =============================================================================
func TestE2ERobot_AddNoDeps(t *testing.T) {
c := authClient()
defer cleanupPkg(c, "robots", "@"+testScope, "simple-bot", "1.0.0")
robotJSON := map[string]interface{}{
"display_name": "Simple Bot",
"system_prompt": "You are a simple bot with no dependencies.",
"language_model": "gpt-4o-mini",
}
buildAndPushRobotZip(t, c, "simple-bot", robotJSON, "1.0.0")
installApp := t.TempDir()
rMgr := robotmgr.New(c, installApp, &common.AutoConfirmPrompter{})
robot, err := rMgr.Add("@"+testScope+"/simple-bot", robotmgr.AddOptions{TeamID: "team-simple"})
if err != nil {
t.Fatalf("Add robot: %v", err)
}
if robot.DisplayName != "Simple Bot" {
t.Errorf("want 'Simple Bot', got %q", robot.DisplayName)
}
if robot.LanguageModel != "gpt-4o-mini" {
t.Errorf("want gpt-4o-mini, got %s", robot.LanguageModel)
}
robotPkg := requireLockfileHas(t, installApp, "@"+testScope+"/simple-bot")
if robotPkg.Type != common.TypeRobot {
t.Errorf("want robot type, got %s", robotPkg.Type)
}
if robotPkg.TeamID != "team-simple" {
t.Errorf("want team_id team-simple, got %s", robotPkg.TeamID)
}
// No other packages should be installed
lf, _ := common.LoadLockfile(installApp)
for id := range lf.Packages {
if id != "@"+testScope+"/simple-bot" {
t.Errorf("unexpected package %s in lockfile (no-dep robot should be alone)", id)
}
}
}
// =============================================================================
// Robot Add: team ID is required
// =============================================================================
func TestE2ERobot_AddRequiresTeam(t *testing.T) {
c := authClient()
defer cleanupPkg(c, "robots", "@"+testScope, "simple-bot", "1.0.0")
robotJSON := map[string]interface{}{
"display_name": "Simple Bot",
"system_prompt": "You are a simple bot.",
}
buildAndPushRobotZip(t, c, "simple-bot", robotJSON, "1.0.0")
installApp := t.TempDir()
rMgr := robotmgr.New(c, installApp, &common.AutoConfirmPrompter{})
_, err := rMgr.Add("@"+testScope+"/simple-bot", robotmgr.AddOptions{})
if err == nil {
t.Fatal("expected error when team is missing")
}
if err.Error() != "--team is required for robot add" {
t.Errorf("unexpected error: %v", err)
}
}
// =============================================================================
// Robot → Agent → MCP dependency chain: required_by propagation
// =============================================================================
func TestE2ERobot_RequiredByChain(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")
defer cleanupPkg(c, "robots", "@"+testScope, "analytics-bot", "1.0.0")
// Push full dependency tree: 2 MCPs → analytics agent → robot
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"})
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
agentMgr.Push(testScope+".analytics", agentmgr.PushOptions{Version: "1.0.0"})
robotJSON := map[string]interface{}{
"display_name": "Analytics Bot",
"system_prompt": "You are an analytics bot.",
"language_model": "gpt-4o",
"robot_config": map[string]interface{}{
"resources": map[string]interface{}{
"phases": map[string]string{
"host": testScope + ".analytics",
},
},
},
}
buildAndPushRobotZip(t, c, "analytics-bot", robotJSON, "1.0.0")
// Install robot to fresh app
installApp := t.TempDir()
rMgr := robotmgr.New(c, installApp, &common.AutoConfirmPrompter{})
_, err := rMgr.Add("@"+testScope+"/analytics-bot", robotmgr.AddOptions{TeamID: "team-chain"})
if err != nil {
t.Fatalf("Add robot: %v", err)
}
// Entire dependency chain should be installed
requireLockfileHas(t, installApp, "@"+testScope+"/analytics-bot")
requireLockfileHas(t, installApp, "@"+testScope+"/analytics")
requireLockfileHas(t, installApp, "@"+testScope+"/registry-mcp")
requireLockfileHas(t, installApp, "@"+testScope+"/data-tools")
// required_by: robot → analytics
lf, _ := common.LoadLockfile(installApp)
analyticsPkg, _ := lf.GetPackage("@" + testScope + "/analytics")
foundRobot := false
for _, rb := range analyticsPkg.RequiredBy {
if rb == "@"+testScope+"/analytics-bot" {
foundRobot = true
}
}
if !foundRobot {
t.Errorf("expected analytics-bot in analytics's required_by, got %v", analyticsPkg.RequiredBy)
}
// required_by: analytics → MCPs (set by agent Add's dependency installation)
// The MCP's required_by may include analytics (set by agent add) and/or analytics-bot (set by robot add)
for _, mcpID := range []string{"@" + testScope + "/registry-mcp", "@" + testScope + "/data-tools"} {
mcpPkg, ok := lf.GetPackage(mcpID)
if !ok {
t.Errorf("MCP %s not found in lockfile", mcpID)
continue
}
if len(mcpPkg.RequiredBy) == 0 {
t.Errorf("expected required_by on %s, got empty", mcpID)
}
}
// Verify disk completeness
requireFileExists(t, installApp+"/assistants/"+testScope+"/analytics/package.yao")
requireFileExists(t, installApp+"/mcps/"+testScope+"/registry-mcp/server.mcp.yao")
requireFileExists(t, installApp+"/mcps/"+testScope+"/data-tools/server.mcp.yao")
requireFileExists(t, installApp+"/scripts/"+testScope+"/registry_mcp.ts")
requireFileExists(t, installApp+"/scripts/"+testScope+"/data_tools.ts")
requireFileExists(t, installApp+"/scripts/"+testScope+"/data_utils.ts")
}