yao/registry/manager/robot/robot_test.go
Max 44af6ba759 Enhance command structure with MCP and Robot functionalities
- Introduce new MCP and Robot command groups in the CLI, allowing for better organization of package management commands.
- Add corresponding command descriptions for MCP and Robot functionalities to improve user guidance.
- Update the agent command group to include additional commands for enhanced agent management.
- Modify .gitignore to exclude specific design markdown files from version control.
- Add environment variable for test application path in GitHub workflows to streamline testing setup.
2026-03-03 08:11:43 +08:00

324 lines
7.7 KiB
Go

package robot
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/yaoapp/yao/registry"
"github.com/yaoapp/yao/registry/manager/common"
"github.com/yaoapp/yao/registry/testdata"
)
func buildRobotZip(scope, name, version string, robotJSON *RobotJSON, deps []testdata.ManifestDep) []byte {
robotBytes, _ := json.Marshal(robotJSON)
zip, err := testdata.BuildZip(&testdata.Manifest{
Type: "robot",
Scope: scope,
Name: name,
Version: version,
Dependencies: deps,
}, map[string]string{
"robot.json": string(robotBytes),
})
if err != nil {
panic(err)
}
return zip
}
func buildAgentZip(scope, name, version string) []byte {
zip, err := testdata.BuildZip(&testdata.Manifest{
Type: "assistant",
Scope: scope,
Name: name,
Version: version,
}, map[string]string{
"package.yao": `{"name":"` + name + `"}`,
})
if err != nil {
panic(err)
}
return zip
}
func buildMCPZip(scope, name, version string) []byte {
zip, err := testdata.BuildZip(&testdata.Manifest{
Type: "mcp",
Scope: scope,
Name: name,
Version: version,
}, map[string]string{
name + ".mcp.yao": `{"transport":"stdio","command":"echo"}`,
})
if err != nil {
panic(err)
}
return zip
}
func mockServer(packages map[string][]byte) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/.well-known/yao-registry" {
json.NewEncoder(w).Encode(map[string]interface{}{
"registry": map[string]string{"version": "1.0.0", "api": "/v1"},
"types": []string{"assistants", "mcps", "robots"},
})
return
}
if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/pull") {
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/"), "/")
if len(parts) >= 4 {
key := parts[0] + "/" + parts[1] + "/" + parts[2]
if zipData, ok := packages[key]; ok {
w.Header().Set("X-Digest", "sha256-test")
w.Write(zipData)
return
}
}
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
return
}
w.WriteHeader(http.StatusNotFound)
}))
}
func TestAnalyzeDeps(t *testing.T) {
robot := &RobotJSON{
RobotConfig: json.RawMessage(`{
"resources": {
"phases": {
"host": "yao.robot-host",
"goals": "yao.robot-goals",
"builtin": "__yao.default-host"
}
}
}`),
Agents: []string{"yao.keeper.fetch", "__yao.system"},
MCPServers: []string{"ark.image.text2img"},
}
deps := AnalyzeDeps(robot)
depMap := map[string]string{}
for _, d := range deps {
depMap[d.PackageID] = d.Type
}
// phases
if depMap["@yao/robot-host"] != "assistant" {
t.Error("expected @yao/robot-host as assistant")
}
if depMap["@yao/robot-goals"] != "assistant" {
t.Error("expected @yao/robot-goals as assistant")
}
// agents (first-layer)
if depMap["@yao/keeper"] != "assistant" {
t.Error("expected @yao/keeper as assistant")
}
// mcp_servers
if depMap["@ark/image.text2img"] != "mcp" {
t.Error("expected @ark/image.text2img as mcp")
}
// __yao.* should be excluded
if _, ok := depMap["@__yao/default-host"]; ok {
t.Error("expected __yao.default-host to be excluded")
}
if _, ok := depMap["@__yao/system"]; ok {
t.Error("expected __yao.system to be excluded")
}
}
func TestAnalyzeDepsEmpty(t *testing.T) {
robot := &RobotJSON{}
deps := AnalyzeDeps(robot)
if len(deps) != 0 {
t.Errorf("expected 0 deps, got %d", len(deps))
}
}
func TestAnalyzeDepsDedupe(t *testing.T) {
robot := &RobotJSON{
RobotConfig: json.RawMessage(`{
"resources": {
"phases": {
"host": "yao.robot-host"
}
}
}`),
Agents: []string{"yao.robot-host.run"},
}
deps := AnalyzeDeps(robot)
count := 0
for _, d := range deps {
if d.PackageID == "@yao/robot-host" {
count++
}
}
if count != 1 {
t.Errorf("expected @yao/robot-host once, got %d times", count)
}
}
func TestAddRobotNoTeam(t *testing.T) {
appRoot := t.TempDir()
srv := mockServer(nil)
defer srv.Close()
client := registry.New(srv.URL)
mgr := New(client, appRoot, &common.AutoConfirmPrompter{})
_, err := mgr.Add("@test/my-robot", AddOptions{})
if err == nil {
t.Fatal("expected error for missing team")
}
if !strings.Contains(err.Error(), "--team") {
t.Errorf("expected team error, got: %v", err)
}
}
func TestAddRobotWithDeps(t *testing.T) {
appRoot := t.TempDir()
agentZip := buildAgentZip("@test", "robot-host", "1.0.0")
mcpZip := buildMCPZip("@test", "image-gen", "1.0.0")
robotJSON := &RobotJSON{
DisplayName: "Test Robot",
RobotConfig: json.RawMessage(`{
"resources": {
"phases": {
"host": "test.robot-host"
}
}
}`),
MCPServers: []string{"test.image-gen"},
}
robotZip := buildRobotZip("@test", "my-robot", "1.0.0", robotJSON, nil)
srv := mockServer(map[string][]byte{
"robots/@test/my-robot": robotZip,
"assistants/@test/robot-host": agentZip,
"mcps/@test/image-gen": mcpZip,
})
defer srv.Close()
client := registry.New(srv.URL)
mgr := New(client, appRoot, &common.AutoConfirmPrompter{})
robot, err := mgr.Add("@test/my-robot", AddOptions{TeamID: "team-123"})
if err != nil {
t.Fatalf("Add robot failed: %v", err)
}
if robot.DisplayName != "Test Robot" {
t.Errorf("expected display_name 'Test Robot', got %q", robot.DisplayName)
}
// Verify lockfile
lf, _ := common.LoadLockfile(appRoot)
pkg, ok := lf.GetPackage("@test/my-robot")
if !ok {
t.Fatal("expected @test/my-robot in lockfile")
}
if pkg.Type != common.TypeRobot {
t.Errorf("expected type robot, got %s", pkg.Type)
}
if pkg.TeamID != "team-123" {
t.Errorf("expected team_id team-123, got %s", pkg.TeamID)
}
// Verify dependencies were installed
if _, ok := lf.GetPackage("@test/robot-host"); !ok {
t.Error("expected @test/robot-host dependency installed")
}
if _, ok := lf.GetPackage("@test/image-gen"); !ok {
t.Error("expected @test/image-gen dependency installed")
}
// Verify assistant directory was created
agentDir := filepath.Join(appRoot, "assistants", "test", "robot-host")
if _, err := os.Stat(agentDir); err != nil {
t.Error("expected assistant directory created")
}
}
func TestAddRobotNoDeps(t *testing.T) {
appRoot := t.TempDir()
robotJSON := &RobotJSON{
DisplayName: "Simple Robot",
}
robotZip := buildRobotZip("@test", "simple-bot", "1.0.0", robotJSON, nil)
srv := mockServer(map[string][]byte{
"robots/@test/simple-bot": robotZip,
})
defer srv.Close()
client := registry.New(srv.URL)
mgr := New(client, appRoot, &common.AutoConfirmPrompter{})
robot, err := mgr.Add("@test/simple-bot", AddOptions{TeamID: "team-1"})
if err != nil {
t.Fatalf("Add simple robot failed: %v", err)
}
if robot.DisplayName != "Simple Robot" {
t.Errorf("expected 'Simple Robot', got %q", robot.DisplayName)
}
}
func TestYaoIDToPackageID(t *testing.T) {
tests := []struct {
input string
want string
}{
{"yao.robot-host", "@yao/robot-host"},
{"ark.image.text2img", "@ark/image.text2img"},
{"bad", ""},
{"", ""},
}
for _, tt := range tests {
got := yaoIDToPackageID(tt.input)
if got != tt.want {
t.Errorf("yaoIDToPackageID(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestAgentYaoIDToPackageID(t *testing.T) {
tests := []struct {
input string
want string
}{
{"yao.keeper.fetch", "@yao/keeper"},
{"yao.keeper", "@yao/keeper"},
{"bad", ""},
}
for _, tt := range tests {
got := agentYaoIDToPackageID(tt.input)
if got != tt.want {
t.Errorf("agentYaoIDToPackageID(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
func TestIsBuiltIn(t *testing.T) {
if !isBuiltIn("__yao.default-host") {
t.Error("expected __yao.default-host to be built-in")
}
if isBuiltIn("yao.robot-host") {
t.Error("expected yao.robot-host NOT to be built-in")
}
}