Enhance testing framework with Registry Client SDK integration
- Update .gitignore to exclude registry data files. - Modify Makefile to include new unit-test-registry target for running tests against the Yao Registry service. - Add comprehensive Registry Client SDK tests in GitHub workflows, ensuring proper setup and execution of tests with Docker service dependencies. - Implement steps for user creation, service restart, and test execution, along with reporting results back to the pull request.
This commit is contained in:
parent
ccc25e7079
commit
691802ae62
7 changed files with 1590 additions and 3 deletions
166
.github/workflows/pr-test.yml
vendored
166
.github/workflows/pr-test.yml
vendored
|
|
@ -1367,3 +1367,169 @@ jobs:
|
||||||
issue_number: issue_number,
|
issue_number: issue_number,
|
||||||
body: '✨DONE✨ db: ${{ matrix.db }} redis: ${{ matrix.redis }} mongo: ${{ matrix.mongo }} passed.'
|
body: '✨DONE✨ db: ${{ matrix.db }} redis: ${{ matrix.redis }} mongo: ${{ matrix.mongo }} passed.'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Registry Client SDK Tests (requires Yao Registry Docker service)
|
||||||
|
# =============================================================================
|
||||||
|
RegistryTest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
yao-registry:
|
||||||
|
image: yaoapp/registry:latest
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: ["1.25"]
|
||||||
|
if: >
|
||||||
|
${{ github.event.workflow_run.event == 'pull_request' &&
|
||||||
|
github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: "Download artifact"
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name == "pr"
|
||||||
|
})[0];
|
||||||
|
var download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: matchArtifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
var fs = require('fs');
|
||||||
|
fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: "Read NR & SHA"
|
||||||
|
run: |
|
||||||
|
unzip pr.zip
|
||||||
|
cat NR
|
||||||
|
cat SHA
|
||||||
|
echo HEAD=$(cat SHA) >> $GITHUB_ENV
|
||||||
|
echo NR=$(cat NR) >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: "Comment on PR"
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { NR } = process.env
|
||||||
|
var issue_number = NR;
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue_number,
|
||||||
|
body: '🤖 Registry Client SDK Tests running...'
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Checkout Kun
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/kun
|
||||||
|
path: kun
|
||||||
|
|
||||||
|
- name: Checkout Xun
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/xun
|
||||||
|
path: xun
|
||||||
|
|
||||||
|
- name: Checkout Gou
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/gou
|
||||||
|
path: gou
|
||||||
|
|
||||||
|
- name: Checkout V8Go
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/v8go
|
||||||
|
path: v8go
|
||||||
|
|
||||||
|
- name: Unzip libv8
|
||||||
|
run: |
|
||||||
|
files=$(find ./v8go -name "libv8*.zip")
|
||||||
|
for file in $files; do
|
||||||
|
dir=$(dirname "$file")
|
||||||
|
echo "Extracting $file to directory $dir"
|
||||||
|
unzip -o -d $dir $file
|
||||||
|
rm -rf $dir/__MACOSX
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Checkout Demo App
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/yao-dev-app
|
||||||
|
path: app
|
||||||
|
|
||||||
|
- name: Checkout Extension
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/yao-extensions-dev
|
||||||
|
path: extension
|
||||||
|
|
||||||
|
- name: Move Dependencies
|
||||||
|
run: |
|
||||||
|
mv kun ../
|
||||||
|
mv xun ../
|
||||||
|
mv gou ../
|
||||||
|
mv v8go ../
|
||||||
|
mv app ../
|
||||||
|
mv extension ../
|
||||||
|
|
||||||
|
- name: Checkout pull request HEAD commit
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ env.HEAD }}
|
||||||
|
|
||||||
|
- name: Setup Go ${{ matrix.go }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Create Registry Test User
|
||||||
|
run: |
|
||||||
|
docker exec ${{ job.services.yao-registry.id }} \
|
||||||
|
registry user add --password yaoagents yaoagents
|
||||||
|
|
||||||
|
- name: Restart Registry Service
|
||||||
|
run: |
|
||||||
|
docker restart ${{ job.services.yao-registry.id }}
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||||
|
echo "Registry is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for registry... ($i)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Run Registry Client Tests
|
||||||
|
env:
|
||||||
|
YAO_REGISTRY_URL: http://localhost:8080
|
||||||
|
run: make unit-test-registry
|
||||||
|
|
||||||
|
- name: Codecov Report
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
- name: "Comment on PR - Registry Tests Done"
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const { NR } = process.env
|
||||||
|
var issue_number = NR;
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: issue_number,
|
||||||
|
body: '✅ Registry Client SDK Tests passed!'
|
||||||
|
});
|
||||||
|
|
|
||||||
104
.github/workflows/unit-test.yml
vendored
104
.github/workflows/unit-test.yml
vendored
|
|
@ -1031,3 +1031,107 @@ jobs:
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Registry Client SDK Tests (requires Yao Registry Docker service)
|
||||||
|
# =============================================================================
|
||||||
|
registry-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
yao-registry:
|
||||||
|
image: yaoapp/registry:latest
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: ["1.25"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Kun
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.REPO_KUN }}
|
||||||
|
path: kun
|
||||||
|
|
||||||
|
- name: Checkout Xun
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.REPO_XUN }}
|
||||||
|
path: xun
|
||||||
|
|
||||||
|
- name: Checkout Gou
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.REPO_GOU }}
|
||||||
|
path: gou
|
||||||
|
|
||||||
|
- name: Checkout V8Go
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/v8go
|
||||||
|
path: v8go
|
||||||
|
|
||||||
|
- name: Unzip libv8
|
||||||
|
run: |
|
||||||
|
files=$(find ./v8go -name "libv8*.zip")
|
||||||
|
for file in $files; do
|
||||||
|
dir=$(dirname "$file")
|
||||||
|
echo "Extracting $file to directory $dir"
|
||||||
|
unzip -o -d $dir $file
|
||||||
|
rm -rf $dir/__MACOSX
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Checkout Demo App
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/yao-dev-app
|
||||||
|
path: app
|
||||||
|
|
||||||
|
- name: Checkout Extension
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: yaoapp/yao-extensions-dev
|
||||||
|
path: extension
|
||||||
|
|
||||||
|
- name: Move Dependencies
|
||||||
|
run: |
|
||||||
|
mv kun ../
|
||||||
|
mv xun ../
|
||||||
|
mv gou ../
|
||||||
|
mv v8go ../
|
||||||
|
mv app ../
|
||||||
|
mv extension ../
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go ${{ matrix.go }}
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Create Registry Test User
|
||||||
|
run: |
|
||||||
|
docker exec ${{ job.services.yao-registry.id }} \
|
||||||
|
registry user add --password yaoagents yaoagents
|
||||||
|
|
||||||
|
- name: Restart Registry Service
|
||||||
|
run: |
|
||||||
|
docker restart ${{ job.services.yao-registry.id }}
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if curl -sf http://localhost:8080/.well-known/yao-registry > /dev/null 2>&1; then
|
||||||
|
echo "Registry is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for registry... ($i)"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Run Registry Client Tests
|
||||||
|
env:
|
||||||
|
YAO_REGISTRY_URL: http://localhost:8080
|
||||||
|
run: make unit-test-registry
|
||||||
|
|
||||||
|
- name: Codecov Report
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -72,3 +72,4 @@ agent/robot/DESIGN-V2.md
|
||||||
tg-session.json
|
tg-session.json
|
||||||
tg-login
|
tg-login
|
||||||
tg-send
|
tg-send
|
||||||
|
registry/data/
|
||||||
|
|
|
||||||
27
Makefile
27
Makefile
|
|
@ -10,9 +10,9 @@ NOW := $(shell date +"%FT%T%z")
|
||||||
OS := $(shell uname)
|
OS := $(shell uname)
|
||||||
|
|
||||||
# ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
# ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||||
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*' | awk '!/\/tests\// || /openapi\/tests/')
|
TESTFOLDER := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|registry' | awk '!/\/tests\// || /openapi\/tests/')
|
||||||
# Core tests (exclude AI-related: agent, aigc, openai, KB, sandbox, and integrations which require external services)
|
# Core tests (exclude AI-related: agent, aigc, openai, KB, sandbox, registry, and integrations which require external services)
|
||||||
TESTFOLDER_CORE := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|agent|kb|sandbox|integrations' | awk '!/\/tests\// || /openapi\/tests/')
|
TESTFOLDER_CORE := $(shell $(GO) list ./... | grep -vE 'examples|openai|aigc|neo|twilio|share*|agent|kb|sandbox|integrations|registry' | awk '!/\/tests\// || /openapi\/tests/')
|
||||||
# Agent tests (agent, aigc) - exclude agent/search/handlers/web (requires external API keys) and robot packages (tested in robot job)
|
# Agent tests (agent, aigc) - exclude agent/search/handlers/web (requires external API keys) and robot packages (tested in robot job)
|
||||||
TESTFOLDER_AGENT := $(shell $(GO) list ./agent/... ./aigc/... | grep -vE 'agent/search/handlers/web|agent/robot/')
|
TESTFOLDER_AGENT := $(shell $(GO) list ./agent/... ./aigc/... | grep -vE 'agent/search/handlers/web|agent/robot/')
|
||||||
# KB tests (kb)
|
# KB tests (kb)
|
||||||
|
|
@ -174,6 +174,27 @@ unit-test-robot:
|
||||||
fi; \
|
fi; \
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Registry Client Test (requires Yao Registry service)
|
||||||
|
.PHONY: unit-test-registry
|
||||||
|
unit-test-registry:
|
||||||
|
echo "mode: count" > coverage.out
|
||||||
|
$(GO) test -v -timeout=2m -covermode=count -coverprofile=profile.out ./registry/... > tmp.out; \
|
||||||
|
cat tmp.out; \
|
||||||
|
if grep -q "^--- FAIL" tmp.out; then \
|
||||||
|
rm tmp.out; \
|
||||||
|
exit 1; \
|
||||||
|
elif grep -q "^FAIL" tmp.out; then \
|
||||||
|
rm tmp.out; \
|
||||||
|
exit 1; \
|
||||||
|
elif grep -q "^panic:" tmp.out; then \
|
||||||
|
rm tmp.out; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
if [ -f profile.out ]; then \
|
||||||
|
cat profile.out | grep -v "mode:" >> coverage.out; \
|
||||||
|
rm profile.out; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Sandbox Unit Test (requires Docker)
|
# Sandbox Unit Test (requires Docker)
|
||||||
.PHONY: unit-test-sandbox
|
.PHONY: unit-test-sandbox
|
||||||
unit-test-sandbox:
|
unit-test-sandbox:
|
||||||
|
|
|
||||||
464
registry/client.go
Normal file
464
registry/client.go
Normal file
|
|
@ -0,0 +1,464 @@
|
||||||
|
// Package registry provides a client SDK for the Yao Registry HTTP API.
|
||||||
|
// It supports push, pull, search, version management, dist-tags,
|
||||||
|
// dependency queries, and package deletion with Basic Auth.
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client talks to a Yao Registry server over HTTP.
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option configures a Client.
|
||||||
|
type Option func(*Client)
|
||||||
|
|
||||||
|
// WithAuth sets Basic Auth credentials for push/delete operations.
|
||||||
|
func WithAuth(username, password string) Option {
|
||||||
|
return func(c *Client) {
|
||||||
|
c.username = username
|
||||||
|
c.password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHTTPClient overrides the default http.Client.
|
||||||
|
func WithHTTPClient(hc *http.Client) Option {
|
||||||
|
return func(c *Client) { c.httpClient = hc }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout sets the HTTP client timeout.
|
||||||
|
func WithTimeout(d time.Duration) Option {
|
||||||
|
return func(c *Client) { c.httpClient.Timeout = d }
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a registry client. serverURL is the base URL without trailing slash.
|
||||||
|
func New(serverURL string, opts ...Option) *Client {
|
||||||
|
c := &Client{
|
||||||
|
baseURL: strings.TrimRight(serverURL, "/"),
|
||||||
|
httpClient: &http.Client{Timeout: 60 * time.Second},
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Response types ---
|
||||||
|
|
||||||
|
// RegistryInfo is returned by the discovery endpoint.
|
||||||
|
type RegistryInfo struct {
|
||||||
|
Registry struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
API string `json:"api"`
|
||||||
|
} `json:"registry"`
|
||||||
|
Types []string `json:"types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerInfo is returned by GET /v1/.
|
||||||
|
type ServerInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushResult is returned after a successful push.
|
||||||
|
type PushResult struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResult is returned after a successful version delete.
|
||||||
|
type DeleteResult struct {
|
||||||
|
Deleted string `json:"deleted"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagResult is returned after setting a dist-tag.
|
||||||
|
type TagResult struct {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagDeleteResult is returned after deleting a dist-tag.
|
||||||
|
type TagDeleteResult struct {
|
||||||
|
Deleted string `json:"deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResult is returned by list and search endpoints.
|
||||||
|
type ListResult struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pagesize"`
|
||||||
|
Packages []json.RawMessage `json:"packages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packument is the full package metadata response.
|
||||||
|
type Packument struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Keywords []string `json:"keywords"`
|
||||||
|
DistTags map[string]string `json:"dist_tags"`
|
||||||
|
Versions map[string]json.RawMessage `json:"versions"`
|
||||||
|
License string `json:"license,omitempty"`
|
||||||
|
Homepage string `json:"homepage,omitempty"`
|
||||||
|
Readme string `json:"readme,omitempty"`
|
||||||
|
Author json.RawMessage `json:"author,omitempty"`
|
||||||
|
Maintainers json.RawMessage `json:"maintainers,omitempty"`
|
||||||
|
Repository json.RawMessage `json:"repository,omitempty"`
|
||||||
|
Bugs json.RawMessage `json:"bugs,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionDetail is returned for a single version query.
|
||||||
|
type VersionDetail struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Dependencies []Dependency `json:"dependencies"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
Artifacts []Artifact `json:"artifacts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency represents a package dependency.
|
||||||
|
type Dependency struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependencyList wraps the dependencies response.
|
||||||
|
type DependencyList struct {
|
||||||
|
Dependencies []json.RawMessage `json:"dependencies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DependentList wraps the dependents response.
|
||||||
|
type DependentList struct {
|
||||||
|
Dependents []json.RawMessage `json:"dependents"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artifact represents a platform-specific release artifact.
|
||||||
|
type Artifact struct {
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Variant string `json:"variant"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIError is returned when the server responds with an error.
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
return fmt.Sprintf("registry: HTTP %d: %s", e.StatusCode, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Discovery ---
|
||||||
|
|
||||||
|
// Discover calls GET /.well-known/yao-registry.
|
||||||
|
func (c *Client) Discover() (*RegistryInfo, error) {
|
||||||
|
var info RegistryInfo
|
||||||
|
if err := c.get("/.well-known/yao-registry", nil, &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info calls GET /v1/.
|
||||||
|
func (c *Client) Info() (*ServerInfo, error) {
|
||||||
|
var info ServerInfo
|
||||||
|
if err := c.get("/v1/", nil, &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- List & Search ---
|
||||||
|
|
||||||
|
// List calls GET /v1/:type with optional filters.
|
||||||
|
func (c *Client) List(pkgType string, scope string, query string, page, pageSize int) (*ListResult, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
if scope != "" {
|
||||||
|
params.Set("scope", scope)
|
||||||
|
}
|
||||||
|
if query != "" {
|
||||||
|
params.Set("q", query)
|
||||||
|
}
|
||||||
|
if page > 0 {
|
||||||
|
params.Set("page", fmt.Sprintf("%d", page))
|
||||||
|
}
|
||||||
|
if pageSize > 0 {
|
||||||
|
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||||
|
}
|
||||||
|
var result ListResult
|
||||||
|
if err := c.get("/v1/"+pkgType, params, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search calls GET /v1/search.
|
||||||
|
func (c *Client) Search(q string, pkgType string, page, pageSize int) (*ListResult, error) {
|
||||||
|
params := url.Values{"q": {q}}
|
||||||
|
if pkgType != "" {
|
||||||
|
params.Set("type", pkgType)
|
||||||
|
}
|
||||||
|
if page > 0 {
|
||||||
|
params.Set("page", fmt.Sprintf("%d", page))
|
||||||
|
}
|
||||||
|
if pageSize > 0 {
|
||||||
|
params.Set("pagesize", fmt.Sprintf("%d", pageSize))
|
||||||
|
}
|
||||||
|
var result ListResult
|
||||||
|
if err := c.get("/v1/search", params, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Package metadata ---
|
||||||
|
|
||||||
|
// GetPackument calls GET /v1/:type/:scope/:name.
|
||||||
|
func (c *Client) GetPackument(pkgType, scope, name string) (*Packument, error) {
|
||||||
|
var p Packument
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s", pkgType, scope, name)
|
||||||
|
if err := c.get(path, nil, &p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion calls GET /v1/:type/:scope/:name/:version.
|
||||||
|
func (c *Client) GetVersion(pkgType, scope, name, version string) (*VersionDetail, error) {
|
||||||
|
var v VersionDetail
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||||
|
if err := c.get(path, nil, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dependencies ---
|
||||||
|
|
||||||
|
// GetDependencies calls GET /v1/:type/:scope/:name/:version/dependencies.
|
||||||
|
func (c *Client) GetDependencies(pkgType, scope, name, version string, recursive bool) (*DependencyList, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/%s/dependencies", pkgType, scope, name, version)
|
||||||
|
params := url.Values{}
|
||||||
|
if recursive {
|
||||||
|
params.Set("recursive", "true")
|
||||||
|
}
|
||||||
|
var dl DependencyList
|
||||||
|
if err := c.get(path, params, &dl); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDependents calls GET /v1/:type/:scope/:name/dependents.
|
||||||
|
func (c *Client) GetDependents(pkgType, scope, name string) (*DependentList, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/dependents", pkgType, scope, name)
|
||||||
|
var dl DependentList
|
||||||
|
if err := c.get(path, nil, &dl); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Push & Pull ---
|
||||||
|
|
||||||
|
// Push uploads a .yao.zip package via PUT /v1/:type/:scope/:name/:version.
|
||||||
|
func (c *Client) Push(pkgType, scope, name, version string, zipData []byte) (*PushResult, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||||
|
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(zipData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/zip")
|
||||||
|
c.setAuth(req)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
return nil, parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result PushResult
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull downloads a .yao.zip via GET /v1/:type/:scope/:name/:version/pull.
|
||||||
|
// The version parameter can be a semver or a dist-tag name.
|
||||||
|
func (c *Client) Pull(pkgType, scope, name, version string) ([]byte, string, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/%s/pull", pkgType, scope, name, version)
|
||||||
|
resp, err := c.httpClient.Get(c.baseURL + path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, "", parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
digest := resp.Header.Get("X-Digest")
|
||||||
|
return data, digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tags ---
|
||||||
|
|
||||||
|
// SetTag calls PUT /v1/:type/:scope/:name/tags/:tag.
|
||||||
|
func (c *Client) SetTag(pkgType, scope, name, tag, version string) (*TagResult, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/tags/%s", pkgType, scope, name, tag)
|
||||||
|
body, _ := json.Marshal(map[string]string{"version": version})
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, c.baseURL+path, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
c.setAuth(req)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result TagResult
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTag calls DELETE /v1/:type/:scope/:name/tags/:tag.
|
||||||
|
func (c *Client) DeleteTag(pkgType, scope, name, tag string) (*TagDeleteResult, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/tags/%s", pkgType, scope, name, tag)
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.setAuth(req)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result TagDeleteResult
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete ---
|
||||||
|
|
||||||
|
// DeleteVersion calls DELETE /v1/:type/:scope/:name/:version.
|
||||||
|
func (c *Client) DeleteVersion(pkgType, scope, name, version string) (*DeleteResult, error) {
|
||||||
|
path := fmt.Sprintf("/v1/%s/%s/%s/%s", pkgType, scope, name, version)
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, c.baseURL+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.setAuth(req)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result DeleteResult
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helpers ---
|
||||||
|
|
||||||
|
func (c *Client) setAuth(req *http.Request) {
|
||||||
|
if c.username != "" {
|
||||||
|
req.SetBasicAuth(c.username, c.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) get(path string, params url.Values, out interface{}) error {
|
||||||
|
u := c.baseURL + path
|
||||||
|
if len(params) > 0 {
|
||||||
|
u += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
resp, err := c.httpClient.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return parseError(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewDecoder(resp.Body).Decode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseError(resp *http.Response) error {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
var errResp struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" {
|
||||||
|
return &APIError{StatusCode: resp.StatusCode, Message: errResp.Error}
|
||||||
|
}
|
||||||
|
return &APIError{StatusCode: resp.StatusCode, Message: string(body)}
|
||||||
|
}
|
||||||
761
registry/client_test.go
Normal file
761
registry/client_test.go
Normal file
|
|
@ -0,0 +1,761 @@
|
||||||
|
package registry_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yaoapp/yao/registry"
|
||||||
|
"github.com/yaoapp/yao/registry/testdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testScope = "@test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func serverURL() string {
|
||||||
|
if u := os.Getenv("YAO_REGISTRY_URL"); u != "" {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
return "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient() *registry.Client {
|
||||||
|
return registry.New(serverURL(),
|
||||||
|
registry.WithAuth("yaoagents", "yaoagents"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPublicClient() *registry.Client {
|
||||||
|
return registry.New(serverURL())
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup removes a version and ignores 404 errors.
|
||||||
|
func cleanup(c *registry.Client, pkgType, scope, name, version string) {
|
||||||
|
c.DeleteVersion(pkgType, scope, name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Discovery ---
|
||||||
|
|
||||||
|
func TestDiscover(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
info, err := c.Discover()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Discover failed: %v", err)
|
||||||
|
}
|
||||||
|
if info.Registry.Version == "" {
|
||||||
|
t.Error("expected non-empty registry version")
|
||||||
|
}
|
||||||
|
if info.Registry.API == "" {
|
||||||
|
t.Error("expected non-empty API path")
|
||||||
|
}
|
||||||
|
if len(info.Types) == 0 {
|
||||||
|
t.Error("expected at least one supported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfo(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
info, err := c.Info()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Info failed: %v", err)
|
||||||
|
}
|
||||||
|
if info.Name == "" {
|
||||||
|
t.Error("expected non-empty name")
|
||||||
|
}
|
||||||
|
if info.Version == "" {
|
||||||
|
t.Error("expected non-empty version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Assistant CRUD ---
|
||||||
|
|
||||||
|
func TestAssistantCRUD(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "assistants"
|
||||||
|
name := "test-assistant"
|
||||||
|
|
||||||
|
zip10, err := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "1.0.0",
|
||||||
|
Description: "Test assistant for unit tests",
|
||||||
|
Keywords: []string{"test", "assistant"},
|
||||||
|
License: "MIT",
|
||||||
|
Author: &testdata.ManifestAuthor{Name: "Test", Email: "test@test.com"},
|
||||||
|
}, map[string]string{
|
||||||
|
"prompts/main.md": "You are a test assistant.",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "1.0.0")
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "1.1.0")
|
||||||
|
|
||||||
|
// Push v1.0.0
|
||||||
|
result, err := c.Push(pkgType, testScope, name, "1.0.0", zip10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push 1.0.0 failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Version != "1.0.0" {
|
||||||
|
t.Errorf("expected version 1.0.0, got %s", result.Version)
|
||||||
|
}
|
||||||
|
if result.Digest == "" {
|
||||||
|
t.Error("expected non-empty digest")
|
||||||
|
}
|
||||||
|
if result.Type != pkgType {
|
||||||
|
t.Errorf("expected type %s, got %s", pkgType, result.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push v1.1.0
|
||||||
|
zip11, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "1.1.0",
|
||||||
|
Description: "Updated test assistant",
|
||||||
|
}, nil)
|
||||||
|
result, err = c.Push(pkgType, testScope, name, "1.1.0", zip11)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push 1.1.0 failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Version != "1.1.0" {
|
||||||
|
t.Errorf("expected version 1.1.0, got %s", result.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get packument
|
||||||
|
pack, err := c.GetPackument(pkgType, testScope, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPackument failed: %v", err)
|
||||||
|
}
|
||||||
|
if pack.Type != pkgType {
|
||||||
|
t.Errorf("expected type %s, got %s", pkgType, pack.Type)
|
||||||
|
}
|
||||||
|
if pack.Scope != testScope {
|
||||||
|
t.Errorf("expected scope %s, got %s", testScope, pack.Scope)
|
||||||
|
}
|
||||||
|
if pack.Name != name {
|
||||||
|
t.Errorf("expected name %s, got %s", name, pack.Name)
|
||||||
|
}
|
||||||
|
if len(pack.Versions) < 2 {
|
||||||
|
t.Errorf("expected at least 2 versions, got %d", len(pack.Versions))
|
||||||
|
}
|
||||||
|
if pack.DistTags["latest"] != "1.1.0" {
|
||||||
|
t.Errorf("expected latest=1.1.0, got %s", pack.DistTags["latest"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get single version
|
||||||
|
ver, err := c.GetVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if ver.Version != "1.0.0" {
|
||||||
|
t.Errorf("expected version 1.0.0, got %s", ver.Version)
|
||||||
|
}
|
||||||
|
if ver.Digest == "" {
|
||||||
|
t.Error("expected non-empty digest in version detail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull
|
||||||
|
data, digest, err := c.Pull(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pull failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("expected non-empty pull data")
|
||||||
|
}
|
||||||
|
if digest == "" {
|
||||||
|
t.Error("expected non-empty digest header from pull")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull by latest tag
|
||||||
|
dataLatest, _, err := c.Pull(pkgType, testScope, name, "latest")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pull latest failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(dataLatest) == 0 {
|
||||||
|
t.Error("expected non-empty pull data for latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete v1.0.0
|
||||||
|
del, err := c.DeleteVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteVersion 1.0.0 failed: %v", err)
|
||||||
|
}
|
||||||
|
if del.Deleted != "1.0.0" {
|
||||||
|
t.Errorf("expected deleted=1.0.0, got %s", del.Deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify v1.0.0 is gone
|
||||||
|
_, err = c.GetVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error after deleting version 1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete v1.1.0
|
||||||
|
_, err = c.DeleteVersion(pkgType, testScope, name, "1.1.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteVersion 1.1.0 failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MCP Tool CRUD ---
|
||||||
|
|
||||||
|
func TestMCPToolCRUD(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "mcps"
|
||||||
|
name := "test-mcp-tool"
|
||||||
|
|
||||||
|
zipData, err := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "mcp",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "2.0.0",
|
||||||
|
Description: "Test MCP tool for SDK tests",
|
||||||
|
Keywords: []string{"test", "mcp"},
|
||||||
|
Engines: map[string]string{"yao": ">=0.10.0"},
|
||||||
|
}, map[string]string{
|
||||||
|
"tools.json": `[{"name":"echo","description":"Echo tool"}]`,
|
||||||
|
"scripts/run.js": "function main(args) { return args; }",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "2.0.0")
|
||||||
|
|
||||||
|
result, err := c.Push(pkgType, testScope, name, "2.0.0", zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Scope != testScope {
|
||||||
|
t.Errorf("expected scope %s, got %s", testScope, result.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull the MCP tool
|
||||||
|
data, _, err := c.Pull(pkgType, testScope, name, "2.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pull failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("expected non-empty data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version detail
|
||||||
|
ver, err := c.GetVersion(pkgType, testScope, name, "2.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if ver.Size <= 0 {
|
||||||
|
t.Error("expected positive size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
_, err = c.DeleteVersion(pkgType, testScope, name, "2.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Robot CRUD ---
|
||||||
|
|
||||||
|
func TestRobotCRUD(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "robots"
|
||||||
|
name := "test-robot"
|
||||||
|
|
||||||
|
zipData, err := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "robot",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "0.5.0",
|
||||||
|
Description: "Test robot for SDK tests",
|
||||||
|
}, map[string]string{
|
||||||
|
"robot.json": `{"name":"test-robot","model":"gpt-4o"}`,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "0.5.0")
|
||||||
|
|
||||||
|
result, err := c.Push(pkgType, testScope, name, "0.5.0", zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Name != name {
|
||||||
|
t.Errorf("expected name %s, got %s", name, result.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packument
|
||||||
|
pack, err := c.GetPackument(pkgType, testScope, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPackument failed: %v", err)
|
||||||
|
}
|
||||||
|
if pack.DistTags["latest"] != "0.5.0" {
|
||||||
|
t.Errorf("expected latest=0.5.0, got %s", pack.DistTags["latest"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
_, err = c.DeleteVersion(pkgType, testScope, name, "0.5.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dist-Tags ---
|
||||||
|
|
||||||
|
func TestDistTags(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "assistants"
|
||||||
|
name := "test-tags"
|
||||||
|
|
||||||
|
zip10, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: name, Version: "1.0.0",
|
||||||
|
}, nil)
|
||||||
|
zip20, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: name, Version: "2.0.0",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "1.0.0")
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "2.0.0")
|
||||||
|
|
||||||
|
c.Push(pkgType, testScope, name, "1.0.0", zip10)
|
||||||
|
c.Push(pkgType, testScope, name, "2.0.0", zip20)
|
||||||
|
|
||||||
|
// Set a custom tag
|
||||||
|
tagResult, err := c.SetTag(pkgType, testScope, name, "stable", "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetTag failed: %v", err)
|
||||||
|
}
|
||||||
|
if tagResult.Tag != "stable" {
|
||||||
|
t.Errorf("expected tag=stable, got %s", tagResult.Tag)
|
||||||
|
}
|
||||||
|
if tagResult.Version != "1.0.0" {
|
||||||
|
t.Errorf("expected version=1.0.0, got %s", tagResult.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tag in packument
|
||||||
|
pack, err := c.GetPackument(pkgType, testScope, name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetPackument failed: %v", err)
|
||||||
|
}
|
||||||
|
if pack.DistTags["stable"] != "1.0.0" {
|
||||||
|
t.Errorf("expected stable=1.0.0, got %s", pack.DistTags["stable"])
|
||||||
|
}
|
||||||
|
if pack.DistTags["latest"] != "2.0.0" {
|
||||||
|
t.Errorf("expected latest=2.0.0, got %s", pack.DistTags["latest"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull by custom tag
|
||||||
|
data, _, err := c.Pull(pkgType, testScope, name, "stable")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pull by stable tag failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("expected data from pull by tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the custom tag
|
||||||
|
delTag, err := c.DeleteTag(pkgType, testScope, name, "stable")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteTag failed: %v", err)
|
||||||
|
}
|
||||||
|
if delTag.Deleted != "stable" {
|
||||||
|
t.Errorf("expected deleted=stable, got %s", delTag.Deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tag removed
|
||||||
|
pack, _ = c.GetPackument(pkgType, testScope, name)
|
||||||
|
if _, ok := pack.DistTags["stable"]; ok {
|
||||||
|
t.Error("expected stable tag to be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
c.DeleteVersion(pkgType, testScope, name, "2.0.0")
|
||||||
|
c.DeleteVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dependencies ---
|
||||||
|
|
||||||
|
func TestDependencies(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
|
||||||
|
// Create a base MCP tool (no deps)
|
||||||
|
mcpZip, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "mcp", Scope: testScope, Name: "dep-base", Version: "1.0.0",
|
||||||
|
Description: "Base MCP dependency",
|
||||||
|
}, map[string]string{"tools.json": `[]`})
|
||||||
|
|
||||||
|
// Create an assistant that depends on the MCP tool
|
||||||
|
astZip, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: "dep-consumer", Version: "1.0.0",
|
||||||
|
Description: "Assistant that depends on MCP tool",
|
||||||
|
Dependencies: []testdata.ManifestDep{
|
||||||
|
{Type: "mcp", Scope: testScope, Name: "dep-base", Version: "^1.0.0"},
|
||||||
|
},
|
||||||
|
}, map[string]string{"prompts/main.md": "Hello"})
|
||||||
|
|
||||||
|
defer cleanup(c, "mcps", testScope, "dep-base", "1.0.0")
|
||||||
|
defer cleanup(c, "assistants", testScope, "dep-consumer", "1.0.0")
|
||||||
|
|
||||||
|
_, err := c.Push("mcps", testScope, "dep-base", "1.0.0", mcpZip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push MCP dep-base failed: %v", err)
|
||||||
|
}
|
||||||
|
_, err = c.Push("assistants", testScope, "dep-consumer", "1.0.0", astZip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push assistant dep-consumer failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query dependencies
|
||||||
|
deps, err := c.GetDependencies("assistants", testScope, "dep-consumer", "1.0.0", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDependencies failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(deps.Dependencies) == 0 {
|
||||||
|
t.Error("expected at least 1 dependency")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query dependents of the MCP tool
|
||||||
|
dependents, err := c.GetDependents("mcps", testScope, "dep-base")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDependents failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(dependents.Dependents) == 0 {
|
||||||
|
t.Error("expected at least 1 dependent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
c.DeleteVersion("assistants", testScope, "dep-consumer", "1.0.0")
|
||||||
|
c.DeleteVersion("mcps", testScope, "dep-base", "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- List & Search ---
|
||||||
|
|
||||||
|
func TestListAndSearch(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "assistants"
|
||||||
|
name := "test-searchable"
|
||||||
|
|
||||||
|
zipData, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "1.0.0",
|
||||||
|
Description: "A searchable test assistant",
|
||||||
|
Keywords: []string{"searchable", "e2e"},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "1.0.0")
|
||||||
|
|
||||||
|
_, err := c.Push(pkgType, testScope, name, "1.0.0", zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List assistants
|
||||||
|
list, err := c.List(pkgType, "", "", 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List failed: %v", err)
|
||||||
|
}
|
||||||
|
if list.Total < 1 {
|
||||||
|
t.Errorf("expected at least 1 package, got %d", list.Total)
|
||||||
|
}
|
||||||
|
if list.Page != 1 {
|
||||||
|
t.Errorf("expected page=1, got %d", list.Page)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List with scope filter
|
||||||
|
listScoped, err := c.List(pkgType, testScope, "", 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List with scope failed: %v", err)
|
||||||
|
}
|
||||||
|
if listScoped.Total < 1 {
|
||||||
|
t.Errorf("expected at least 1 package in scope %s, got %d", testScope, listScoped.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
search, err := c.Search("searchable", "", 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Search failed: %v", err)
|
||||||
|
}
|
||||||
|
if search.Total < 1 {
|
||||||
|
t.Errorf("expected at least 1 search result, got %d", search.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search with type filter
|
||||||
|
searchTyped, err := c.Search("searchable", pkgType, 1, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Search with type failed: %v", err)
|
||||||
|
}
|
||||||
|
if searchTyped.Total < 1 {
|
||||||
|
t.Errorf("expected at least 1 typed search result, got %d", searchTyped.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
c.DeleteVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Options coverage ---
|
||||||
|
|
||||||
|
func TestClientOptions(t *testing.T) {
|
||||||
|
hc := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
c := registry.New(serverURL(),
|
||||||
|
registry.WithAuth("u", "p"),
|
||||||
|
registry.WithHTTPClient(hc),
|
||||||
|
registry.WithTimeout(30*time.Second),
|
||||||
|
)
|
||||||
|
// Verify the client works (at least doesn't panic)
|
||||||
|
_, err := c.Discover()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Discover with custom options failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIErrorString(t *testing.T) {
|
||||||
|
err := ®istry.APIError{StatusCode: 404, Message: "not found"}
|
||||||
|
s := err.Error()
|
||||||
|
if s != "registry: HTTP 404: not found" {
|
||||||
|
t.Errorf("unexpected error string: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNetworkError(t *testing.T) {
|
||||||
|
c := registry.New("http://127.0.0.1:19999")
|
||||||
|
|
||||||
|
_, err := c.Discover()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for Discover")
|
||||||
|
}
|
||||||
|
_, err = c.Info()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for Info")
|
||||||
|
}
|
||||||
|
_, err = c.List("assistants", "", "", 1, 20)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for List")
|
||||||
|
}
|
||||||
|
_, err = c.Search("q", "", 1, 20)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for Search")
|
||||||
|
}
|
||||||
|
_, err = c.GetPackument("assistants", "@x", "y")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for GetPackument")
|
||||||
|
}
|
||||||
|
_, err = c.GetVersion("assistants", "@x", "y", "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for GetVersion")
|
||||||
|
}
|
||||||
|
_, err = c.GetDependencies("assistants", "@x", "y", "1.0.0", true)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for GetDependencies")
|
||||||
|
}
|
||||||
|
_, err = c.GetDependents("assistants", "@x", "y")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for GetDependents")
|
||||||
|
}
|
||||||
|
_, _, err = c.Pull("assistants", "@x", "y", "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for Pull")
|
||||||
|
}
|
||||||
|
_, err = c.Push("assistants", "@x", "y", "1.0.0", []byte("data"))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for Push")
|
||||||
|
}
|
||||||
|
_, err = c.SetTag("assistants", "@x", "y", "t", "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for SetTag")
|
||||||
|
}
|
||||||
|
_, err = c.DeleteTag("assistants", "@x", "y", "t")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for DeleteTag")
|
||||||
|
}
|
||||||
|
_, err = c.DeleteVersion("assistants", "@x", "y", "1.0.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected network error for DeleteVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveDependencies(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
|
||||||
|
base, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "mcp", Scope: testScope, Name: "recurse-base", Version: "1.0.0",
|
||||||
|
}, nil)
|
||||||
|
mid, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: "recurse-mid", Version: "1.0.0",
|
||||||
|
Dependencies: []testdata.ManifestDep{
|
||||||
|
{Type: "mcp", Scope: testScope, Name: "recurse-base", Version: "^1.0.0"},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
top, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "robot", Scope: testScope, Name: "recurse-top", Version: "1.0.0",
|
||||||
|
Dependencies: []testdata.ManifestDep{
|
||||||
|
{Type: "assistant", Scope: testScope, Name: "recurse-mid", Version: "^1.0.0"},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
defer cleanup(c, "mcps", testScope, "recurse-base", "1.0.0")
|
||||||
|
defer cleanup(c, "assistants", testScope, "recurse-mid", "1.0.0")
|
||||||
|
defer cleanup(c, "robots", testScope, "recurse-top", "1.0.0")
|
||||||
|
|
||||||
|
c.Push("mcps", testScope, "recurse-base", "1.0.0", base)
|
||||||
|
c.Push("assistants", testScope, "recurse-mid", "1.0.0", mid)
|
||||||
|
c.Push("robots", testScope, "recurse-top", "1.0.0", top)
|
||||||
|
|
||||||
|
deps, err := c.GetDependencies("robots", testScope, "recurse-top", "1.0.0", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDependencies recursive failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(deps.Dependencies) == 0 {
|
||||||
|
t.Error("expected recursive dependencies")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DeleteVersion("robots", testScope, "recurse-top", "1.0.0")
|
||||||
|
c.DeleteVersion("assistants", testScope, "recurse-mid", "1.0.0")
|
||||||
|
c.DeleteVersion("mcps", testScope, "recurse-base", "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Error handling ---
|
||||||
|
|
||||||
|
func TestPushWithoutAuth(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
zipData, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: "noauth", Version: "1.0.0",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
_, err := c.Push("assistants", testScope, "noauth", "1.0.0", zipData)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when pushing without auth")
|
||||||
|
}
|
||||||
|
apiErr, ok := err.(*registry.APIError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected APIError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
if apiErr.StatusCode != 401 {
|
||||||
|
t.Errorf("expected 401, got %d", apiErr.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNonExistentPackage(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
_, err := c.GetPackument("assistants", testScope, "does-not-exist")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-existent package")
|
||||||
|
}
|
||||||
|
apiErr, ok := err.(*registry.APIError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected APIError, got %T", err)
|
||||||
|
}
|
||||||
|
if apiErr.StatusCode != 404 {
|
||||||
|
t.Errorf("expected 404, got %d", apiErr.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullNonExistentVersion(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
_, _, err := c.Pull("assistants", testScope, "does-not-exist", "9.9.9")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-existent version pull")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidType(t *testing.T) {
|
||||||
|
c := newPublicClient()
|
||||||
|
_, err := c.List("invalidtype", "", "", 1, 20)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid type")
|
||||||
|
}
|
||||||
|
apiErr, ok := err.(*registry.APIError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected APIError, got %T", err)
|
||||||
|
}
|
||||||
|
if apiErr.StatusCode != 400 {
|
||||||
|
t.Errorf("expected 400, got %d", apiErr.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteNonExistentTag(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
name := "tag-noexist"
|
||||||
|
zipData, _ := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "assistant", Scope: testScope, Name: name, Version: "1.0.0",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
defer cleanup(c, "assistants", testScope, name, "1.0.0")
|
||||||
|
|
||||||
|
c.Push("assistants", testScope, name, "1.0.0", zipData)
|
||||||
|
|
||||||
|
_, err := c.DeleteTag("assistants", testScope, name, "nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error deleting non-existent tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete latest tag
|
||||||
|
_, err = c.DeleteTag("assistants", testScope, name, "latest")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error deleting latest tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DeleteVersion("assistants", testScope, name, "1.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Release type CRUD ---
|
||||||
|
|
||||||
|
func TestReleaseCRUD(t *testing.T) {
|
||||||
|
c := newClient()
|
||||||
|
pkgType := "releases"
|
||||||
|
name := "test-release"
|
||||||
|
|
||||||
|
zipData, err := testdata.BuildZip(&testdata.Manifest{
|
||||||
|
Type: "release",
|
||||||
|
Scope: testScope,
|
||||||
|
Name: name,
|
||||||
|
Version: "1.0.0",
|
||||||
|
Description: "Test release binary placeholder",
|
||||||
|
}, map[string]string{
|
||||||
|
"bin/yao": "#!/bin/sh\necho hello",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cleanup(c, pkgType, testScope, name, "1.0.0")
|
||||||
|
|
||||||
|
result, err := c.Push(pkgType, testScope, name, "1.0.0", zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Push release failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.Type != pkgType {
|
||||||
|
t.Errorf("expected type %s, got %s", pkgType, result.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version detail
|
||||||
|
ver, err := c.GetVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetVersion failed: %v", err)
|
||||||
|
}
|
||||||
|
if ver.Version != "1.0.0" {
|
||||||
|
t.Errorf("expected version 1.0.0, got %s", ver.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull
|
||||||
|
data, _, err := c.Pull(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Pull failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("expected non-empty pull data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
_, err = c.DeleteVersion(pkgType, testScope, name, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Delete failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
registry/testdata/build.go
vendored
Normal file
70
registry/testdata/build.go
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Package testdata provides helpers to build .yao.zip test fixtures in memory.
|
||||||
|
package testdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manifest mirrors the pkg.yao structure for test fixture construction.
|
||||||
|
type Manifest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Dependencies []ManifestDep `json:"dependencies,omitempty"`
|
||||||
|
Engines map[string]string `json:"engines,omitempty"`
|
||||||
|
Keywords []string `json:"keywords,omitempty"`
|
||||||
|
License string `json:"license,omitempty"`
|
||||||
|
Author *ManifestAuthor `json:"author,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManifestDep represents a dependency entry in pkg.yao.
|
||||||
|
type ManifestDep struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManifestAuthor holds author information.
|
||||||
|
type ManifestAuthor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildZip creates an in-memory .yao.zip with a package/pkg.yao manifest
|
||||||
|
// and an optional set of extra files (path relative to package/) -> content.
|
||||||
|
func BuildZip(manifest *Manifest, extraFiles map[string]string) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
w := zip.NewWriter(&buf)
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(manifest, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, err := w.Create("package/pkg.yao")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, content := range extraFiles {
|
||||||
|
f, err := w.Create("package/" + name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := f.Write([]byte(content)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue