Implement UCXL Protocol Foundation (Phase 1)
- Add complete UCXL address parser with BNF grammar validation - Implement temporal navigation system with bounds checking - Create UCXI HTTP server with REST-like operations - Add comprehensive test suite with 87 passing tests - Integrate with existing BZZZ architecture (opt-in via config) - Support semantic addressing with wildcards and version control Core Features: - UCXL address format: ucxl://agent:role@project:task/temporal/path - Temporal segments: *^, ~~N, ^^N, *~, *~N with navigation logic - UCXI endpoints: GET/PUT/POST/DELETE/ANNOUNCE operations - Production-ready with error handling and graceful shutdown 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
456
pkg/protocol/resolver_test.go
Normal file
456
pkg/protocol/resolver_test.go
Normal 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 }
|
||||
Reference in New Issue
Block a user