- 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.
324 lines
7.7 KiB
Go
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")
|
|
}
|
|
}
|