Complete BZZZ functionality port to CHORUS

🎭 CHORUS now contains full BZZZ functionality adapted for containers

Core systems ported:
- P2P networking (libp2p with DHT and PubSub)
- Task coordination (COOEE protocol)
- HMMM collaborative reasoning
- SHHH encryption and security
- SLURP admin election system
- UCXL content addressing
- UCXI server integration
- Hypercore logging system
- Health monitoring and graceful shutdown
- License validation with KACHING

Container adaptations:
- Environment variable configuration (no YAML files)
- Container-optimized logging to stdout/stderr
- Auto-generated agent IDs for container deployments
- Docker-first architecture

All proven BZZZ P2P protocols, AI integration, and collaboration
features are now available in containerized form.

Next: Build and test container deployment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-02 20:02:37 +10:00
parent 7c6cbd562a
commit 543ab216f9
224 changed files with 86331 additions and 186 deletions

338
pkg/protocol/integration.go Normal file
View File

@@ -0,0 +1,338 @@
package protocol
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"chorus.services/bzzz/pkg/config"
"chorus.services/bzzz/pkg/dht"
"chorus.services/bzzz/p2p"
"github.com/libp2p/go-libp2p/core/peer"
)
// ProtocolManager manages the BZZZ v2 protocol components
type ProtocolManager struct {
config *config.Config
node *p2p.Node
resolver *Resolver
enabled bool
// Local peer information
localPeer *PeerCapability
}
// NewProtocolManager creates a new protocol manager
func NewProtocolManager(cfg *config.Config, node *p2p.Node) (*ProtocolManager, error) {
if cfg == nil || node == nil {
return nil, fmt.Errorf("config and node are required")
}
pm := &ProtocolManager{
config: cfg,
node: node,
enabled: cfg.V2.Enabled,
}
// Only initialize if v2 protocol is enabled
if pm.enabled {
if err := pm.initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize protocol manager: %w", err)
}
}
return pm, nil
}
// initialize sets up the protocol components
func (pm *ProtocolManager) initialize() error {
// Create resolver
resolverOpts := []ResolverOption{
WithCacheTTL(pm.config.V2.URIResolution.CacheTTL),
WithMaxPeersPerResult(pm.config.V2.URIResolution.MaxPeersPerResult),
}
// Set default strategy
switch pm.config.V2.URIResolution.DefaultStrategy {
case "exact":
resolverOpts = append(resolverOpts, WithDefaultStrategy(StrategyExact))
case "priority":
resolverOpts = append(resolverOpts, WithDefaultStrategy(StrategyPriority))
case "load_balance":
resolverOpts = append(resolverOpts, WithDefaultStrategy(StrategyLoadBalance))
default:
resolverOpts = append(resolverOpts, WithDefaultStrategy(StrategyBestMatch))
}
pm.resolver = NewResolver(pm.node.Host().Peerstore(), resolverOpts...)
// Initialize local peer information
pm.localPeer = &PeerCapability{
PeerID: pm.node.ID(),
Agent: pm.config.Agent.ID,
Role: pm.config.Agent.Role,
Capabilities: pm.config.Agent.Capabilities,
Models: pm.config.Agent.Models,
Specialization: pm.config.Agent.Specialization,
LastSeen: time.Now(),
Status: "ready",
Metadata: make(map[string]string),
}
// Add project information if available
if project := pm.getProjectFromConfig(); project != "" {
pm.localPeer.Metadata["project"] = project
}
// Register local peer
pm.resolver.RegisterPeer(pm.node.ID(), pm.localPeer)
return nil
}
// IsEnabled returns whether the v2 protocol is enabled
func (pm *ProtocolManager) IsEnabled() bool {
return pm.enabled
}
// ResolveURI resolves a bzzz:// URI to peer addresses
func (pm *ProtocolManager) ResolveURI(ctx context.Context, uriStr string) (*ResolutionResult, error) {
if !pm.enabled {
return nil, fmt.Errorf("v2 protocol not enabled")
}
return pm.resolver.ResolveString(ctx, uriStr)
}
// RegisterPeer registers a peer's capabilities
func (pm *ProtocolManager) RegisterPeer(peerID peer.ID, capabilities *PeerCapability) {
if !pm.enabled {
return
}
pm.resolver.RegisterPeer(peerID, capabilities)
// Announce to DHT if enabled
if pm.node.IsDHTEnabled() {
pm.announcePeerToDHT(context.Background(), capabilities)
}
}
// UpdateLocalPeerStatus updates the local peer's status
func (pm *ProtocolManager) UpdateLocalPeerStatus(status string) {
if !pm.enabled {
return
}
pm.localPeer.Status = status
pm.localPeer.LastSeen = time.Now()
pm.resolver.RegisterPeer(pm.node.ID(), pm.localPeer)
}
// GetLocalPeer returns the local peer information
func (pm *ProtocolManager) GetLocalPeer() *PeerCapability {
return pm.localPeer
}
// GetAllPeers returns all known peers
func (pm *ProtocolManager) GetAllPeers() map[peer.ID]*PeerCapability {
if !pm.enabled {
return make(map[peer.ID]*PeerCapability)
}
return pm.resolver.GetPeerCapabilities()
}
// HandlePeerCapabilityMessage handles incoming peer capability messages
func (pm *ProtocolManager) HandlePeerCapabilityMessage(peerID peer.ID, data []byte) error {
if !pm.enabled {
return nil // Silently ignore if v2 not enabled
}
var capability PeerCapability
if err := json.Unmarshal(data, &capability); err != nil {
return fmt.Errorf("failed to unmarshal capability message: %w", err)
}
capability.PeerID = peerID
capability.LastSeen = time.Now()
pm.resolver.RegisterPeer(peerID, &capability)
return nil
}
// AnnounceCapabilities announces the local peer's capabilities
func (pm *ProtocolManager) AnnounceCapabilities() error {
if !pm.enabled {
return nil
}
// Update local peer information
pm.localPeer.LastSeen = time.Now()
// Announce to DHT if enabled
if pm.node.IsDHTEnabled() {
return pm.announcePeerToDHT(context.Background(), pm.localPeer)
}
return nil
}
// announcePeerToDHT announces a peer's capabilities to the DHT
func (pm *ProtocolManager) announcePeerToDHT(ctx context.Context, capability *PeerCapability) error {
dht := pm.node.DHT()
if dht == nil {
return fmt.Errorf("DHT not available")
}
// Register peer with role-based and capability-based keys
if capability.Role != "" {
dht.RegisterPeer(capability.PeerID, capability.Agent, capability.Role, capability.Capabilities)
if err := dht.AnnounceRole(ctx, capability.Role); err != nil {
// Log error but don't fail
}
}
// Announce each capability
for _, cap := range capability.Capabilities {
if err := dht.AnnounceCapability(ctx, cap); err != nil {
// Log error but don't fail
}
}
// Announce general peer presence
if err := dht.Provide(ctx, "bzzz:peer"); err != nil {
// Log error but don't fail
}
return nil
}
// FindPeersByRole finds peers with a specific role
func (pm *ProtocolManager) FindPeersByRole(ctx context.Context, role string) ([]*PeerCapability, error) {
if !pm.enabled {
return nil, fmt.Errorf("v2 protocol not enabled")
}
// First try DHT if available
if pm.node.IsDHTEnabled() {
dhtPeers, err := pm.node.DHT().FindPeersByRole(ctx, role)
if err == nil && len(dhtPeers) > 0 {
// Convert DHT peer info to capabilities
var capabilities []*PeerCapability
for _, dhtPeer := range dhtPeers {
cap := &PeerCapability{
PeerID: dhtPeer.ID,
Agent: dhtPeer.Agent,
Role: dhtPeer.Role,
LastSeen: dhtPeer.LastSeen,
Metadata: make(map[string]string),
}
capabilities = append(capabilities, cap)
}
return capabilities, nil
}
}
// Fall back to local resolver
var result []*PeerCapability
for _, peer := range pm.resolver.GetPeerCapabilities() {
if peer.Role == role || role == "*" {
result = append(result, peer)
}
}
return result, nil
}
// ValidateURI validates a bzzz:// URI
func (pm *ProtocolManager) ValidateURI(uriStr string) error {
if !pm.enabled {
return fmt.Errorf("v2 protocol not enabled")
}
_, err := ParseBzzzURI(uriStr)
return err
}
// CreateURI creates a bzzz:// URI with the given components
func (pm *ProtocolManager) CreateURI(agent, role, project, task, path string) (*BzzzURI, error) {
if !pm.enabled {
return nil, fmt.Errorf("v2 protocol not enabled")
}
// Use configured defaults if components are empty
if agent == "" {
agent = pm.config.V2.SemanticAddressing.DefaultAgent
}
if role == "" {
role = pm.config.V2.SemanticAddressing.DefaultRole
}
if project == "" {
project = pm.config.V2.SemanticAddressing.DefaultProject
}
return NewBzzzURI(agent, role, project, task, path), nil
}
// GetFeatureFlags returns the current feature flags
func (pm *ProtocolManager) GetFeatureFlags() map[string]bool {
return pm.config.V2.FeatureFlags
}
// IsFeatureEnabled checks if a specific feature is enabled
func (pm *ProtocolManager) IsFeatureEnabled(feature string) bool {
if !pm.enabled {
return false
}
enabled, exists := pm.config.V2.FeatureFlags[feature]
return exists && enabled
}
// Close shuts down the protocol manager
func (pm *ProtocolManager) Close() error {
if pm.resolver != nil {
return pm.resolver.Close()
}
return nil
}
// getProjectFromConfig extracts project information from configuration
func (pm *ProtocolManager) getProjectFromConfig() string {
// Try to infer project from agent ID or other configuration
if pm.config.Agent.ID != "" {
parts := strings.Split(pm.config.Agent.ID, "-")
if len(parts) > 0 {
return parts[0]
}
}
// Default project if none can be inferred
return "bzzz"
}
// GetStats returns protocol statistics
func (pm *ProtocolManager) GetStats() map[string]interface{} {
stats := map[string]interface{}{
"enabled": pm.enabled,
"local_peer": pm.localPeer,
"known_peers": len(pm.resolver.GetPeerCapabilities()),
}
if pm.node.IsDHTEnabled() {
dht := pm.node.DHT()
stats["dht_enabled"] = true
stats["dht_bootstrapped"] = dht.IsBootstrapped()
stats["dht_size"] = dht.GetDHTSize()
stats["dht_connected_peers"] = len(dht.GetConnectedPeers())
} else {
stats["dht_enabled"] = false
}
return stats
}

551
pkg/protocol/resolver.go Normal file
View File

@@ -0,0 +1,551 @@
package protocol
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
)
// PeerCapability represents the capabilities of a peer
type PeerCapability struct {
PeerID peer.ID `json:"peer_id"`
Agent string `json:"agent"`
Role string `json:"role"`
Capabilities []string `json:"capabilities"`
Models []string `json:"models"`
Specialization string `json:"specialization"`
Project string `json:"project"`
LastSeen time.Time `json:"last_seen"`
Status string `json:"status"` // "online", "busy", "offline"
Metadata map[string]string `json:"metadata"`
}
// PeerAddress represents a resolved peer address
type PeerAddress struct {
PeerID peer.ID `json:"peer_id"`
Addresses []string `json:"addresses"`
Priority int `json:"priority"`
Metadata map[string]interface{} `json:"metadata"`
}
// ResolutionResult represents the result of address resolution
type ResolutionResult struct {
URI *BzzzURI `json:"uri"`
Peers []*PeerAddress `json:"peers"`
ResolvedAt time.Time `json:"resolved_at"`
ResolutionTTL time.Duration `json:"ttl"`
Strategy string `json:"strategy"`
}
// ResolutionStrategy defines how to resolve addresses
type ResolutionStrategy string
const (
StrategyExact ResolutionStrategy = "exact" // Exact match only
StrategyBestMatch ResolutionStrategy = "best_match" // Best available match
StrategyLoadBalance ResolutionStrategy = "load_balance" // Load balance among matches
StrategyPriority ResolutionStrategy = "priority" // Highest priority first
)
// Resolver handles semantic address resolution
type Resolver struct {
// Peer capability registry
capabilities map[peer.ID]*PeerCapability
capMutex sync.RWMutex
// Address resolution cache
cache map[string]*ResolutionResult
cacheMutex sync.RWMutex
cacheTTL time.Duration
// Configuration
defaultStrategy ResolutionStrategy
maxPeersPerResult int
// Peerstore for address information
peerstore peerstore.Peerstore
}
// NewResolver creates a new semantic address resolver
func NewResolver(peerstore peerstore.Peerstore, opts ...ResolverOption) *Resolver {
r := &Resolver{
capabilities: make(map[peer.ID]*PeerCapability),
cache: make(map[string]*ResolutionResult),
cacheTTL: 5 * time.Minute,
defaultStrategy: StrategyBestMatch,
maxPeersPerResult: 5,
peerstore: peerstore,
}
for _, opt := range opts {
opt(r)
}
// Start background cleanup
go r.startCleanup()
return r
}
// ResolverOption configures the resolver
type ResolverOption func(*Resolver)
// WithCacheTTL sets the cache TTL
func WithCacheTTL(ttl time.Duration) ResolverOption {
return func(r *Resolver) {
r.cacheTTL = ttl
}
}
// WithDefaultStrategy sets the default resolution strategy
func WithDefaultStrategy(strategy ResolutionStrategy) ResolverOption {
return func(r *Resolver) {
r.defaultStrategy = strategy
}
}
// WithMaxPeersPerResult sets the maximum peers per result
func WithMaxPeersPerResult(max int) ResolverOption {
return func(r *Resolver) {
r.maxPeersPerResult = max
}
}
// RegisterPeer registers a peer's capabilities
func (r *Resolver) RegisterPeer(peerID peer.ID, capability *PeerCapability) {
r.capMutex.Lock()
defer r.capMutex.Unlock()
capability.PeerID = peerID
capability.LastSeen = time.Now()
r.capabilities[peerID] = capability
// Clear relevant cache entries
r.invalidateCache()
}
// UnregisterPeer removes a peer from the registry
func (r *Resolver) UnregisterPeer(peerID peer.ID) {
r.capMutex.Lock()
defer r.capMutex.Unlock()
delete(r.capabilities, peerID)
// Clear relevant cache entries
r.invalidateCache()
}
// UpdatePeerStatus updates a peer's status
func (r *Resolver) UpdatePeerStatus(peerID peer.ID, status string) {
r.capMutex.Lock()
defer r.capMutex.Unlock()
if cap, exists := r.capabilities[peerID]; exists {
cap.Status = status
cap.LastSeen = time.Now()
}
}
// Resolve resolves a bzzz:// URI to peer addresses
func (r *Resolver) Resolve(ctx context.Context, uri *BzzzURI, strategy ...ResolutionStrategy) (*ResolutionResult, error) {
if uri == nil {
return nil, fmt.Errorf("nil URI")
}
// Determine strategy
resolveStrategy := r.defaultStrategy
if len(strategy) > 0 {
resolveStrategy = strategy[0]
}
// Check cache first
cacheKey := r.getCacheKey(uri, resolveStrategy)
if result := r.getFromCache(cacheKey); result != nil {
return result, nil
}
// Perform resolution
result, err := r.resolveURI(ctx, uri, resolveStrategy)
if err != nil {
return nil, err
}
// Cache result
r.cacheResult(cacheKey, result)
return result, nil
}
// ResolveString resolves a bzzz:// URI string to peer addresses
func (r *Resolver) ResolveString(ctx context.Context, uriStr string, strategy ...ResolutionStrategy) (*ResolutionResult, error) {
uri, err := ParseBzzzURI(uriStr)
if err != nil {
return nil, fmt.Errorf("failed to parse URI: %w", err)
}
return r.Resolve(ctx, uri, strategy...)
}
// resolveURI performs the actual URI resolution
func (r *Resolver) resolveURI(ctx context.Context, uri *BzzzURI, strategy ResolutionStrategy) (*ResolutionResult, error) {
r.capMutex.RLock()
defer r.capMutex.RUnlock()
var matchingPeers []*PeerCapability
// Find matching peers
for _, cap := range r.capabilities {
if r.peerMatches(cap, uri) {
matchingPeers = append(matchingPeers, cap)
}
}
if len(matchingPeers) == 0 {
return &ResolutionResult{
URI: uri,
Peers: []*PeerAddress{},
ResolvedAt: time.Now(),
ResolutionTTL: r.cacheTTL,
Strategy: string(strategy),
}, nil
}
// Apply resolution strategy
selectedPeers := r.applyStrategy(matchingPeers, strategy)
// Convert to peer addresses
var peerAddresses []*PeerAddress
for i, cap := range selectedPeers {
if i >= r.maxPeersPerResult {
break
}
addr := &PeerAddress{
PeerID: cap.PeerID,
Priority: r.calculatePriority(cap, uri),
Metadata: map[string]interface{}{
"agent": cap.Agent,
"role": cap.Role,
"specialization": cap.Specialization,
"status": cap.Status,
"last_seen": cap.LastSeen,
},
}
// Get addresses from peerstore
if r.peerstore != nil {
addrs := r.peerstore.Addrs(cap.PeerID)
for _, ma := range addrs {
addr.Addresses = append(addr.Addresses, ma.String())
}
}
peerAddresses = append(peerAddresses, addr)
}
return &ResolutionResult{
URI: uri,
Peers: peerAddresses,
ResolvedAt: time.Now(),
ResolutionTTL: r.cacheTTL,
Strategy: string(strategy),
}, nil
}
// peerMatches checks if a peer matches the URI criteria
func (r *Resolver) peerMatches(cap *PeerCapability, uri *BzzzURI) bool {
// Check if peer is online
if cap.Status == "offline" {
return false
}
// Check agent match
if !IsWildcard(uri.Agent) && !componentMatches(uri.Agent, cap.Agent) {
return false
}
// Check role match
if !IsWildcard(uri.Role) && !componentMatches(uri.Role, cap.Role) {
return false
}
// Check project match (if specified in metadata)
if !IsWildcard(uri.Project) {
if project, exists := cap.Metadata["project"]; exists {
if !componentMatches(uri.Project, project) {
return false
}
}
}
// Check task capabilities (if peer has relevant capabilities)
if !IsWildcard(uri.Task) {
taskMatches := false
for _, capability := range cap.Capabilities {
if componentMatches(uri.Task, capability) {
taskMatches = true
break
}
}
if !taskMatches {
// Also check specialization
if !componentMatches(uri.Task, cap.Specialization) {
return false
}
}
}
return true
}
// applyStrategy applies the resolution strategy to matching peers
func (r *Resolver) applyStrategy(peers []*PeerCapability, strategy ResolutionStrategy) []*PeerCapability {
switch strategy {
case StrategyExact:
// Return only exact matches (already filtered)
return peers
case StrategyPriority:
// Sort by priority (calculated based on specificity and status)
return r.sortByPriority(peers)
case StrategyLoadBalance:
// Sort by load (prefer less busy peers)
return r.sortByLoad(peers)
case StrategyBestMatch:
fallthrough
default:
// Sort by best match score
return r.sortByMatch(peers)
}
}
// sortByPriority sorts peers by priority score
func (r *Resolver) sortByPriority(peers []*PeerCapability) []*PeerCapability {
// Simple priority: online > working > busy, then by last seen
result := make([]*PeerCapability, len(peers))
copy(result, peers)
// Sort by status priority and recency
for i := 0; i < len(result)-1; i++ {
for j := i + 1; j < len(result); j++ {
iPriority := r.getStatusPriority(result[i].Status)
jPriority := r.getStatusPriority(result[j].Status)
if iPriority < jPriority ||
(iPriority == jPriority && result[i].LastSeen.Before(result[j].LastSeen)) {
result[i], result[j] = result[j], result[i]
}
}
}
return result
}
// sortByLoad sorts peers by current load (prefer less busy)
func (r *Resolver) sortByLoad(peers []*PeerCapability) []*PeerCapability {
result := make([]*PeerCapability, len(peers))
copy(result, peers)
// Sort by status (ready > working > busy)
for i := 0; i < len(result)-1; i++ {
for j := i + 1; j < len(result); j++ {
iLoad := r.getLoadScore(result[i].Status)
jLoad := r.getLoadScore(result[j].Status)
if iLoad > jLoad {
result[i], result[j] = result[j], result[i]
}
}
}
return result
}
// sortByMatch sorts peers by match quality
func (r *Resolver) sortByMatch(peers []*PeerCapability) []*PeerCapability {
result := make([]*PeerCapability, len(peers))
copy(result, peers)
// Simple sorting - prefer online status and recent activity
for i := 0; i < len(result)-1; i++ {
for j := i + 1; j < len(result); j++ {
if r.getMatchScore(result[i]) < r.getMatchScore(result[j]) {
result[i], result[j] = result[j], result[i]
}
}
}
return result
}
// Helper functions for scoring
func (r *Resolver) getStatusPriority(status string) int {
switch status {
case "ready":
return 3
case "working":
return 2
case "busy":
return 1
default:
return 0
}
}
func (r *Resolver) getLoadScore(status string) int {
switch status {
case "ready":
return 0 // Lowest load
case "working":
return 1
case "busy":
return 2 // Highest load
default:
return 3
}
}
func (r *Resolver) getMatchScore(cap *PeerCapability) int {
score := 0
// Status contribution
score += r.getStatusPriority(cap.Status) * 10
// Recency contribution (more recent = higher score)
timeSince := time.Since(cap.LastSeen)
if timeSince < time.Minute {
score += 5
} else if timeSince < time.Hour {
score += 3
} else if timeSince < 24*time.Hour {
score += 1
}
// Capability count contribution
score += len(cap.Capabilities)
return score
}
// calculatePriority calculates priority for a peer address
func (r *Resolver) calculatePriority(cap *PeerCapability, uri *BzzzURI) int {
priority := 0
// Exact matches get higher priority
if cap.Agent == uri.Agent {
priority += 4
}
if cap.Role == uri.Role {
priority += 3
}
if cap.Specialization == uri.Task {
priority += 2
}
// Status-based priority
priority += r.getStatusPriority(cap.Status)
return priority
}
// Cache management
func (r *Resolver) getCacheKey(uri *BzzzURI, strategy ResolutionStrategy) string {
return fmt.Sprintf("%s:%s", uri.String(), strategy)
}
func (r *Resolver) getFromCache(key string) *ResolutionResult {
r.cacheMutex.RLock()
defer r.cacheMutex.RUnlock()
if result, exists := r.cache[key]; exists {
// Check if result is still valid
if time.Since(result.ResolvedAt) < result.ResolutionTTL {
return result
}
// Remove expired entry
delete(r.cache, key)
}
return nil
}
func (r *Resolver) cacheResult(key string, result *ResolutionResult) {
r.cacheMutex.Lock()
defer r.cacheMutex.Unlock()
r.cache[key] = result
}
func (r *Resolver) invalidateCache() {
r.cacheMutex.Lock()
defer r.cacheMutex.Unlock()
// Clear entire cache on capability changes
r.cache = make(map[string]*ResolutionResult)
}
// startCleanup starts background cache cleanup
func (r *Resolver) startCleanup() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
r.cleanupCache()
}
}
func (r *Resolver) cleanupCache() {
r.cacheMutex.Lock()
defer r.cacheMutex.Unlock()
now := time.Now()
for key, result := range r.cache {
if now.Sub(result.ResolvedAt) > result.ResolutionTTL {
delete(r.cache, key)
}
}
}
// GetPeerCapabilities returns all registered peer capabilities
func (r *Resolver) GetPeerCapabilities() map[peer.ID]*PeerCapability {
r.capMutex.RLock()
defer r.capMutex.RUnlock()
result := make(map[peer.ID]*PeerCapability)
for id, cap := range r.capabilities {
result[id] = cap
}
return result
}
// GetPeerCapability returns a specific peer's capabilities
func (r *Resolver) GetPeerCapability(peerID peer.ID) (*PeerCapability, bool) {
r.capMutex.RLock()
defer r.capMutex.RUnlock()
cap, exists := r.capabilities[peerID]
return cap, exists
}
// Close shuts down the resolver
func (r *Resolver) Close() error {
// Clear all data
r.capMutex.Lock()
r.capabilities = make(map[peer.ID]*PeerCapability)
r.capMutex.Unlock()
r.cacheMutex.Lock()
r.cache = make(map[string]*ResolutionResult)
r.cacheMutex.Unlock()
return nil
}

View File

@@ -0,0 +1,456 @@
package protocol
import (
"context"
"testing"
"time"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/core/test"
)
func TestNewResolver(t *testing.T) {
// Create a mock peerstore
mockPeerstore := &mockPeerstore{}
resolver := NewResolver(mockPeerstore)
if resolver == nil {
t.Fatal("resolver is nil")
}
if resolver.peerstore != mockPeerstore {
t.Error("peerstore not set correctly")
}
if resolver.defaultStrategy != StrategyBestMatch {
t.Errorf("expected default strategy %v, got %v", StrategyBestMatch, resolver.defaultStrategy)
}
if resolver.maxPeersPerResult != 5 {
t.Errorf("expected max peers per result 5, got %d", resolver.maxPeersPerResult)
}
}
func TestResolverWithOptions(t *testing.T) {
mockPeerstore := &mockPeerstore{}
resolver := NewResolver(mockPeerstore,
WithCacheTTL(10*time.Minute),
WithDefaultStrategy(StrategyPriority),
WithMaxPeersPerResult(10),
)
if resolver.cacheTTL != 10*time.Minute {
t.Errorf("expected cache TTL 10m, got %v", resolver.cacheTTL)
}
if resolver.defaultStrategy != StrategyPriority {
t.Errorf("expected strategy %v, got %v", StrategyPriority, resolver.defaultStrategy)
}
if resolver.maxPeersPerResult != 10 {
t.Errorf("expected max peers 10, got %d", resolver.maxPeersPerResult)
}
}
func TestRegisterPeer(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
capability := &PeerCapability{
Agent: "claude",
Role: "frontend",
Capabilities: []string{"react", "javascript"},
Models: []string{"claude-3"},
Specialization: "frontend",
Status: "ready",
Metadata: make(map[string]string),
}
resolver.RegisterPeer(peerID, capability)
// Verify peer was registered
caps := resolver.GetPeerCapabilities()
if len(caps) != 1 {
t.Errorf("expected 1 peer, got %d", len(caps))
}
registeredCap, exists := caps[peerID]
if !exists {
t.Error("peer not found in capabilities")
}
if registeredCap.Agent != capability.Agent {
t.Errorf("expected agent %s, got %s", capability.Agent, registeredCap.Agent)
}
if registeredCap.PeerID != peerID {
t.Error("peer ID not set correctly")
}
}
func TestUnregisterPeer(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
capability := &PeerCapability{
Agent: "claude",
Role: "frontend",
}
// Register then unregister
resolver.RegisterPeer(peerID, capability)
resolver.UnregisterPeer(peerID)
caps := resolver.GetPeerCapabilities()
if len(caps) != 0 {
t.Errorf("expected 0 peers after unregister, got %d", len(caps))
}
}
func TestUpdatePeerStatus(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
capability := &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "ready",
}
resolver.RegisterPeer(peerID, capability)
resolver.UpdatePeerStatus(peerID, "busy")
caps := resolver.GetPeerCapabilities()
updatedCap := caps[peerID]
if updatedCap.Status != "busy" {
t.Errorf("expected status 'busy', got '%s'", updatedCap.Status)
}
}
func TestResolveURI(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
// Register some test peers
peerID1 := test.RandPeerIDFatal(t)
peerID2 := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID1, &PeerCapability{
Agent: "claude",
Role: "frontend",
Capabilities: []string{"react", "javascript"},
Status: "ready",
Metadata: map[string]string{"project": "chorus"},
})
resolver.RegisterPeer(peerID2, &PeerCapability{
Agent: "claude",
Role: "backend",
Capabilities: []string{"go", "api"},
Status: "ready",
Metadata: map[string]string{"project": "chorus"},
})
// Test exact match
uri, err := ParseBzzzURI("bzzz://claude:frontend@chorus:react")
if err != nil {
t.Fatalf("failed to parse URI: %v", err)
}
ctx := context.Background()
result, err := resolver.Resolve(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve URI: %v", err)
}
if len(result.Peers) != 1 {
t.Errorf("expected 1 peer in result, got %d", len(result.Peers))
}
if result.Peers[0].PeerID != peerID1 {
t.Error("wrong peer returned")
}
}
func TestResolveURIWithWildcards(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID1 := test.RandPeerIDFatal(t)
peerID2 := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID1, &PeerCapability{
Agent: "claude",
Role: "frontend",
Capabilities: []string{"react"},
Status: "ready",
})
resolver.RegisterPeer(peerID2, &PeerCapability{
Agent: "claude",
Role: "backend",
Capabilities: []string{"go"},
Status: "ready",
})
// Test wildcard match
uri, err := ParseBzzzURI("bzzz://claude:*@*:*")
if err != nil {
t.Fatalf("failed to parse URI: %v", err)
}
ctx := context.Background()
result, err := resolver.Resolve(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve URI: %v", err)
}
if len(result.Peers) != 2 {
t.Errorf("expected 2 peers in result, got %d", len(result.Peers))
}
}
func TestResolveURIWithOfflinePeers(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID, &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "offline", // This peer should be filtered out
})
uri, err := ParseBzzzURI("bzzz://claude:frontend@*:*")
if err != nil {
t.Fatalf("failed to parse URI: %v", err)
}
ctx := context.Background()
result, err := resolver.Resolve(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve URI: %v", err)
}
if len(result.Peers) != 0 {
t.Errorf("expected 0 peers (offline filtered), got %d", len(result.Peers))
}
}
func TestResolveString(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID, &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "ready",
})
ctx := context.Background()
result, err := resolver.ResolveString(ctx, "bzzz://claude:frontend@*:*")
if err != nil {
t.Fatalf("failed to resolve string: %v", err)
}
if len(result.Peers) != 1 {
t.Errorf("expected 1 peer, got %d", len(result.Peers))
}
}
func TestResolverCaching(t *testing.T) {
resolver := NewResolver(&mockPeerstore{}, WithCacheTTL(1*time.Second))
peerID := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID, &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "ready",
})
ctx := context.Background()
uri := "bzzz://claude:frontend@*:*"
// First resolution should hit the resolver
result1, err := resolver.ResolveString(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve: %v", err)
}
// Second resolution should hit the cache
result2, err := resolver.ResolveString(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve: %v", err)
}
// Results should be identical (from cache)
if result1.ResolvedAt != result2.ResolvedAt {
// This is expected behavior - cache should return same timestamp
}
// Wait for cache to expire
time.Sleep(2 * time.Second)
// Third resolution should miss cache and create new result
result3, err := resolver.ResolveString(ctx, uri)
if err != nil {
t.Fatalf("failed to resolve: %v", err)
}
if result3.ResolvedAt.Before(result1.ResolvedAt.Add(1 * time.Second)) {
t.Error("cache should have expired and created new result")
}
}
func TestResolutionStrategies(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
// Register peers with different priorities
peerID1 := test.RandPeerIDFatal(t)
peerID2 := test.RandPeerIDFatal(t)
resolver.RegisterPeer(peerID1, &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "ready",
})
resolver.RegisterPeer(peerID2, &PeerCapability{
Agent: "claude",
Role: "frontend",
Status: "busy",
})
ctx := context.Background()
uri, _ := ParseBzzzURI("bzzz://claude:frontend@*:*")
// Test different strategies
strategies := []ResolutionStrategy{
StrategyBestMatch,
StrategyPriority,
StrategyLoadBalance,
StrategyExact,
}
for _, strategy := range strategies {
result, err := resolver.Resolve(ctx, uri, strategy)
if err != nil {
t.Errorf("failed to resolve with strategy %s: %v", strategy, err)
}
if len(result.Peers) == 0 {
t.Errorf("no peers found with strategy %s", strategy)
}
if result.Strategy != string(strategy) {
t.Errorf("strategy not recorded correctly: expected %s, got %s", strategy, result.Strategy)
}
}
}
func TestPeerMatching(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
capability := &PeerCapability{
Agent: "claude",
Role: "frontend",
Capabilities: []string{"react", "javascript"},
Status: "ready",
Metadata: map[string]string{"project": "chorus"},
}
tests := []struct {
name string
uri *BzzzURI
expected bool
}{
{
name: "exact match",
uri: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "react"},
expected: true,
},
{
name: "wildcard agent",
uri: &BzzzURI{Agent: "*", Role: "frontend", Project: "chorus", Task: "react"},
expected: true,
},
{
name: "capability match",
uri: &BzzzURI{Agent: "claude", Role: "frontend", Project: "*", Task: "javascript"},
expected: true,
},
{
name: "no match - wrong agent",
uri: &BzzzURI{Agent: "gpt", Role: "frontend", Project: "chorus", Task: "react"},
expected: false,
},
{
name: "no match - wrong role",
uri: &BzzzURI{Agent: "claude", Role: "backend", Project: "chorus", Task: "react"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := resolver.peerMatches(capability, tt.uri)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
func TestGetPeerCapability(t *testing.T) {
resolver := NewResolver(&mockPeerstore{})
peerID := test.RandPeerIDFatal(t)
capability := &PeerCapability{
Agent: "claude",
Role: "frontend",
}
// Test before registration
_, exists := resolver.GetPeerCapability(peerID)
if exists {
t.Error("peer should not exist before registration")
}
// Register and test
resolver.RegisterPeer(peerID, capability)
retrieved, exists := resolver.GetPeerCapability(peerID)
if !exists {
t.Error("peer should exist after registration")
}
if retrieved.Agent != capability.Agent {
t.Errorf("expected agent %s, got %s", capability.Agent, retrieved.Agent)
}
}
// Mock peerstore implementation for testing
type mockPeerstore struct{}
func (m *mockPeerstore) PeerInfo(peer.ID) peer.AddrInfo { return peer.AddrInfo{} }
func (m *mockPeerstore) Peers() peer.IDSlice { return nil }
func (m *mockPeerstore) Addrs(peer.ID) []peerstore.Multiaddr { return nil }
func (m *mockPeerstore) AddrStream(context.Context, peer.ID) <-chan peerstore.Multiaddr { return nil }
func (m *mockPeerstore) SetAddr(peer.ID, peerstore.Multiaddr, time.Duration) {}
func (m *mockPeerstore) SetAddrs(peer.ID, []peerstore.Multiaddr, time.Duration) {}
func (m *mockPeerstore) UpdateAddrs(peer.ID, time.Duration, time.Duration) {}
func (m *mockPeerstore) ClearAddrs(peer.ID) {}
func (m *mockPeerstore) PeersWithAddrs() peer.IDSlice { return nil }
func (m *mockPeerstore) PubKey(peer.ID) peerstore.PubKey { return nil }
func (m *mockPeerstore) SetPubKey(peer.ID, peerstore.PubKey) error { return nil }
func (m *mockPeerstore) PrivKey(peer.ID) peerstore.PrivKey { return nil }
func (m *mockPeerstore) SetPrivKey(peer.ID, peerstore.PrivKey) error { return nil }
func (m *mockPeerstore) Get(peer.ID, string) (interface{}, error) { return nil, nil }
func (m *mockPeerstore) Put(peer.ID, string, interface{}) error { return nil }
func (m *mockPeerstore) GetProtocols(peer.ID) ([]peerstore.Protocol, error) { return nil, nil }
func (m *mockPeerstore) SetProtocols(peer.ID, ...peerstore.Protocol) error { return nil }
func (m *mockPeerstore) SupportsProtocols(peer.ID, ...peerstore.Protocol) ([]peerstore.Protocol, error) { return nil, nil }
func (m *mockPeerstore) RemovePeer(peer.ID) {}
func (m *mockPeerstore) Close() error { return nil }

326
pkg/protocol/uri.go Normal file
View File

@@ -0,0 +1,326 @@
package protocol
import (
"fmt"
"net/url"
"regexp"
"strings"
)
// BzzzURI represents a parsed bzzz:// URI with semantic addressing
// Grammar: bzzz://[agent]:[role]@[project]:[task]/[path][?query][#fragment]
type BzzzURI struct {
// Core addressing components
Agent string // Agent identifier (e.g., "claude", "any", "*")
Role string // Agent role (e.g., "frontend", "backend", "architect")
Project string // Project context (e.g., "chorus", "bzzz")
Task string // Task identifier (e.g., "implement", "review", "test", "*")
// Resource path
Path string // Resource path (e.g., "/src/main.go", "/docs/api.md")
// Standard URI components
Query string // Query parameters
Fragment string // Fragment identifier
// Original raw URI string
Raw string
}
// URI grammar constants
const (
BzzzScheme = "bzzz"
// Special identifiers
AnyAgent = "any"
AnyRole = "any"
AnyProject = "any"
AnyTask = "any"
Wildcard = "*"
)
// Validation patterns
var (
// Component validation patterns
agentPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
rolePattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
projectPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
taskPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
pathPattern = regexp.MustCompile(`^/[a-zA-Z0-9\-_/\.]*$|^$`)
// Full URI pattern for validation
bzzzURIPattern = regexp.MustCompile(`^bzzz://([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)@([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)(/[a-zA-Z0-9\-_/\.]*)?(\?[^#]*)?(\#.*)?$`)
)
// ParseBzzzURI parses a bzzz:// URI string into a BzzzURI struct
func ParseBzzzURI(uri string) (*BzzzURI, error) {
if uri == "" {
return nil, fmt.Errorf("empty URI")
}
// Basic scheme validation
if !strings.HasPrefix(uri, BzzzScheme+"://") {
return nil, fmt.Errorf("invalid scheme: expected '%s'", BzzzScheme)
}
// Use Go's standard URL parser for basic parsing
parsedURL, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse URI: %w", err)
}
if parsedURL.Scheme != BzzzScheme {
return nil, fmt.Errorf("invalid scheme: expected '%s', got '%s'", BzzzScheme, parsedURL.Scheme)
}
// Parse the authority part (user:pass@host:port becomes agent:role@project:task)
userInfo := parsedURL.User
if userInfo == nil {
return nil, fmt.Errorf("missing agent:role information")
}
username := userInfo.Username()
password, hasPassword := userInfo.Password()
if !hasPassword {
return nil, fmt.Errorf("missing role information")
}
agent := username
role := password
// Parse host:port as project:task
hostPort := parsedURL.Host
if hostPort == "" {
return nil, fmt.Errorf("missing project:task information")
}
// Split host:port to get project:task
parts := strings.Split(hostPort, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid project:task format: expected 'project:task'")
}
project := parts[0]
task := parts[1]
// Create BzzzURI instance
bzzzURI := &BzzzURI{
Agent: agent,
Role: role,
Project: project,
Task: task,
Path: parsedURL.Path,
Query: parsedURL.RawQuery,
Fragment: parsedURL.Fragment,
Raw: uri,
}
// Validate components
if err := bzzzURI.Validate(); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
return bzzzURI, nil
}
// Validate validates all components of the BzzzURI
func (u *BzzzURI) Validate() error {
// Validate agent
if u.Agent == "" {
return fmt.Errorf("agent cannot be empty")
}
if !agentPattern.MatchString(u.Agent) {
return fmt.Errorf("invalid agent format: '%s'", u.Agent)
}
// Validate role
if u.Role == "" {
return fmt.Errorf("role cannot be empty")
}
if !rolePattern.MatchString(u.Role) {
return fmt.Errorf("invalid role format: '%s'", u.Role)
}
// Validate project
if u.Project == "" {
return fmt.Errorf("project cannot be empty")
}
if !projectPattern.MatchString(u.Project) {
return fmt.Errorf("invalid project format: '%s'", u.Project)
}
// Validate task
if u.Task == "" {
return fmt.Errorf("task cannot be empty")
}
if !taskPattern.MatchString(u.Task) {
return fmt.Errorf("invalid task format: '%s'", u.Task)
}
// Validate path (optional)
if u.Path != "" && !pathPattern.MatchString(u.Path) {
return fmt.Errorf("invalid path format: '%s'", u.Path)
}
return nil
}
// String returns the canonical string representation of the BzzzURI
func (u *BzzzURI) String() string {
uri := fmt.Sprintf("%s://%s:%s@%s:%s", BzzzScheme, u.Agent, u.Role, u.Project, u.Task)
if u.Path != "" {
uri += u.Path
}
if u.Query != "" {
uri += "?" + u.Query
}
if u.Fragment != "" {
uri += "#" + u.Fragment
}
return uri
}
// Normalize normalizes the URI components for consistent addressing
func (u *BzzzURI) Normalize() {
// Convert empty wildcards to standard wildcard
if u.Agent == "" {
u.Agent = Wildcard
}
if u.Role == "" {
u.Role = Wildcard
}
if u.Project == "" {
u.Project = Wildcard
}
if u.Task == "" {
u.Task = Wildcard
}
// Normalize to lowercase for consistency
u.Agent = strings.ToLower(u.Agent)
u.Role = strings.ToLower(u.Role)
u.Project = strings.ToLower(u.Project)
u.Task = strings.ToLower(u.Task)
// Clean path
if u.Path != "" && !strings.HasPrefix(u.Path, "/") {
u.Path = "/" + u.Path
}
}
// IsWildcard checks if a component is a wildcard or "any"
func IsWildcard(component string) bool {
return component == Wildcard || component == AnyAgent || component == AnyRole ||
component == AnyProject || component == AnyTask
}
// Matches checks if this URI matches another URI (with wildcard support)
func (u *BzzzURI) Matches(other *BzzzURI) bool {
if other == nil {
return false
}
// Check each component with wildcard support
if !componentMatches(u.Agent, other.Agent) {
return false
}
if !componentMatches(u.Role, other.Role) {
return false
}
if !componentMatches(u.Project, other.Project) {
return false
}
if !componentMatches(u.Task, other.Task) {
return false
}
// Path matching (exact or wildcard)
if u.Path != "" && other.Path != "" && u.Path != other.Path {
return false
}
return true
}
// componentMatches checks if two components match (with wildcard support)
func componentMatches(a, b string) bool {
// Exact match
if a == b {
return true
}
// Wildcard matching
if IsWildcard(a) || IsWildcard(b) {
return true
}
return false
}
// GetSelectorPriority returns a priority score for URI matching (higher = more specific)
func (u *BzzzURI) GetSelectorPriority() int {
priority := 0
// More specific components get higher priority
if !IsWildcard(u.Agent) {
priority += 8
}
if !IsWildcard(u.Role) {
priority += 4
}
if !IsWildcard(u.Project) {
priority += 2
}
if !IsWildcard(u.Task) {
priority += 1
}
// Path specificity adds priority
if u.Path != "" && u.Path != "/" {
priority += 1
}
return priority
}
// ToAddress returns a simplified address representation for P2P routing
func (u *BzzzURI) ToAddress() string {
return fmt.Sprintf("%s:%s@%s:%s", u.Agent, u.Role, u.Project, u.Task)
}
// ValidateBzzzURIString validates a bzzz:// URI string without parsing
func ValidateBzzzURIString(uri string) error {
if uri == "" {
return fmt.Errorf("empty URI")
}
if !bzzzURIPattern.MatchString(uri) {
return fmt.Errorf("invalid bzzz:// URI format")
}
return nil
}
// NewBzzzURI creates a new BzzzURI with the given components
func NewBzzzURI(agent, role, project, task, path string) *BzzzURI {
uri := &BzzzURI{
Agent: agent,
Role: role,
Project: project,
Task: task,
Path: path,
}
uri.Normalize()
return uri
}
// ParseAddress parses a simplified address format (agent:role@project:task)
func ParseAddress(addr string) (*BzzzURI, error) {
// Convert simplified address to full URI
fullURI := BzzzScheme + "://" + addr
return ParseBzzzURI(fullURI)
}

509
pkg/protocol/uri_test.go Normal file
View File

@@ -0,0 +1,509 @@
package protocol
import (
"testing"
)
func TestParseBzzzURI(t *testing.T) {
tests := []struct {
name string
uri string
expectError bool
expected *BzzzURI
}{
{
name: "valid basic URI",
uri: "bzzz://claude:frontend@chorus:implement/src/main.go",
expected: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "/src/main.go",
Raw: "bzzz://claude:frontend@chorus:implement/src/main.go",
},
},
{
name: "URI with wildcards",
uri: "bzzz://any:*@*:test",
expected: &BzzzURI{
Agent: "any",
Role: "*",
Project: "*",
Task: "test",
Raw: "bzzz://any:*@*:test",
},
},
{
name: "URI with query and fragment",
uri: "bzzz://claude:backend@bzzz:debug/api/handler.go?type=error#line123",
expected: &BzzzURI{
Agent: "claude",
Role: "backend",
Project: "bzzz",
Task: "debug",
Path: "/api/handler.go",
Query: "type=error",
Fragment: "line123",
Raw: "bzzz://claude:backend@bzzz:debug/api/handler.go?type=error#line123",
},
},
{
name: "URI without path",
uri: "bzzz://any:architect@project:review",
expected: &BzzzURI{
Agent: "any",
Role: "architect",
Project: "project",
Task: "review",
Raw: "bzzz://any:architect@project:review",
},
},
{
name: "invalid scheme",
uri: "http://claude:frontend@chorus:implement",
expectError: true,
},
{
name: "missing role",
uri: "bzzz://claude@chorus:implement",
expectError: true,
},
{
name: "missing task",
uri: "bzzz://claude:frontend@chorus",
expectError: true,
},
{
name: "empty URI",
uri: "",
expectError: true,
},
{
name: "invalid format",
uri: "bzzz://invalid",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseBzzzURI(tt.uri)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if result == nil {
t.Errorf("result is nil")
return
}
// Compare components
if result.Agent != tt.expected.Agent {
t.Errorf("Agent: expected %s, got %s", tt.expected.Agent, result.Agent)
}
if result.Role != tt.expected.Role {
t.Errorf("Role: expected %s, got %s", tt.expected.Role, result.Role)
}
if result.Project != tt.expected.Project {
t.Errorf("Project: expected %s, got %s", tt.expected.Project, result.Project)
}
if result.Task != tt.expected.Task {
t.Errorf("Task: expected %s, got %s", tt.expected.Task, result.Task)
}
if result.Path != tt.expected.Path {
t.Errorf("Path: expected %s, got %s", tt.expected.Path, result.Path)
}
if result.Query != tt.expected.Query {
t.Errorf("Query: expected %s, got %s", tt.expected.Query, result.Query)
}
if result.Fragment != tt.expected.Fragment {
t.Errorf("Fragment: expected %s, got %s", tt.expected.Fragment, result.Fragment)
}
})
}
}
func TestBzzzURIValidation(t *testing.T) {
tests := []struct {
name string
uri *BzzzURI
expectError bool
}{
{
name: "valid URI",
uri: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "/src/main.go",
},
expectError: false,
},
{
name: "empty agent",
uri: &BzzzURI{
Agent: "",
Role: "frontend",
Project: "chorus",
Task: "implement",
},
expectError: true,
},
{
name: "invalid agent format",
uri: &BzzzURI{
Agent: "invalid@agent",
Role: "frontend",
Project: "chorus",
Task: "implement",
},
expectError: true,
},
{
name: "wildcard components",
uri: &BzzzURI{
Agent: "*",
Role: "any",
Project: "*",
Task: "*",
},
expectError: false,
},
{
name: "invalid path",
uri: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "invalid-path", // Should start with /
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.uri.Validate()
if tt.expectError && err == nil {
t.Errorf("expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
func TestBzzzURINormalize(t *testing.T) {
uri := &BzzzURI{
Agent: "Claude",
Role: "Frontend",
Project: "CHORUS",
Task: "Implement",
Path: "src/main.go", // Missing leading slash
}
uri.Normalize()
expected := &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "/src/main.go",
}
if uri.Agent != expected.Agent {
t.Errorf("Agent: expected %s, got %s", expected.Agent, uri.Agent)
}
if uri.Role != expected.Role {
t.Errorf("Role: expected %s, got %s", expected.Role, uri.Role)
}
if uri.Project != expected.Project {
t.Errorf("Project: expected %s, got %s", expected.Project, uri.Project)
}
if uri.Task != expected.Task {
t.Errorf("Task: expected %s, got %s", expected.Task, uri.Task)
}
if uri.Path != expected.Path {
t.Errorf("Path: expected %s, got %s", expected.Path, uri.Path)
}
}
func TestBzzzURIMatches(t *testing.T) {
tests := []struct {
name string
uri1 *BzzzURI
uri2 *BzzzURI
expected bool
}{
{
name: "exact match",
uri1: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
uri2: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
expected: true,
},
{
name: "wildcard agent match",
uri1: &BzzzURI{Agent: "*", Role: "frontend", Project: "chorus", Task: "implement"},
uri2: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
expected: true,
},
{
name: "any role match",
uri1: &BzzzURI{Agent: "claude", Role: "any", Project: "chorus", Task: "implement"},
uri2: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
expected: true,
},
{
name: "no match",
uri1: &BzzzURI{Agent: "claude", Role: "backend", Project: "chorus", Task: "implement"},
uri2: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
expected: false,
},
{
name: "nil comparison",
uri1: &BzzzURI{Agent: "claude", Role: "frontend", Project: "chorus", Task: "implement"},
uri2: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.uri1.Matches(tt.uri2)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
func TestBzzzURIString(t *testing.T) {
tests := []struct {
name string
uri *BzzzURI
expected string
}{
{
name: "basic URI",
uri: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "/src/main.go",
},
expected: "bzzz://claude:frontend@chorus:implement/src/main.go",
},
{
name: "URI with query and fragment",
uri: &BzzzURI{
Agent: "claude",
Role: "backend",
Project: "bzzz",
Task: "debug",
Path: "/api/handler.go",
Query: "type=error",
Fragment: "line123",
},
expected: "bzzz://claude:backend@bzzz:debug/api/handler.go?type=error#line123",
},
{
name: "URI without path",
uri: &BzzzURI{
Agent: "any",
Role: "architect",
Project: "project",
Task: "review",
},
expected: "bzzz://any:architect@project:review",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.uri.String()
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}
func TestGetSelectorPriority(t *testing.T) {
tests := []struct {
name string
uri *BzzzURI
expected int
}{
{
name: "all specific",
uri: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
Path: "/src/main.go",
},
expected: 8 + 4 + 2 + 1 + 1, // All components + path
},
{
name: "some wildcards",
uri: &BzzzURI{
Agent: "*",
Role: "frontend",
Project: "*",
Task: "implement",
},
expected: 4 + 1, // Role + Task
},
{
name: "all wildcards",
uri: &BzzzURI{
Agent: "*",
Role: "any",
Project: "*",
Task: "*",
},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.uri.GetSelectorPriority()
if result != tt.expected {
t.Errorf("expected %d, got %d", tt.expected, result)
}
})
}
}
func TestParseAddress(t *testing.T) {
tests := []struct {
name string
addr string
expectError bool
expected *BzzzURI
}{
{
name: "valid address",
addr: "claude:frontend@chorus:implement",
expected: &BzzzURI{
Agent: "claude",
Role: "frontend",
Project: "chorus",
Task: "implement",
},
},
{
name: "invalid address",
addr: "invalid-format",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseAddress(tt.addr)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if result.Agent != tt.expected.Agent {
t.Errorf("Agent: expected %s, got %s", tt.expected.Agent, result.Agent)
}
if result.Role != tt.expected.Role {
t.Errorf("Role: expected %s, got %s", tt.expected.Role, result.Role)
}
if result.Project != tt.expected.Project {
t.Errorf("Project: expected %s, got %s", tt.expected.Project, result.Project)
}
if result.Task != tt.expected.Task {
t.Errorf("Task: expected %s, got %s", tt.expected.Task, result.Task)
}
})
}
}
func TestIsWildcard(t *testing.T) {
tests := []struct {
component string
expected bool
}{
{"*", true},
{"any", true},
{"claude", false},
{"frontend", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.component, func(t *testing.T) {
result := IsWildcard(tt.component)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
func TestValidateBzzzURIString(t *testing.T) {
tests := []struct {
name string
uri string
expectError bool
}{
{
name: "valid URI",
uri: "bzzz://claude:frontend@chorus:implement/src/main.go",
expectError: false,
},
{
name: "invalid scheme",
uri: "http://claude:frontend@chorus:implement",
expectError: true,
},
{
name: "empty URI",
uri: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateBzzzURIString(tt.uri)
if tt.expectError && err == nil {
t.Errorf("expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}